Rossi Oddet

Blog d'un artisan développeur

GDG Nantes & Stereolux : Workshop HTML5

Le 16/04 dernier, le Stéréolux et le GDG Nantes ont organisé un Workshop HTML5.

Une présentation du GDG Nantes & Stereolux

L’événement commence par une présentation du GDG Nantes et de Stereolux.

Le GDG Nantes (GDG = Google Developpers Group) est le groupe d’utilisateurs des technologies Google de Nantes. Il organise régulièrement des conférences, des concours et même des apéros :)

Le Stereolux organise des événements (ateliers, conférence, concert, …) autour de la création numérique.

Les deux entités collaborent pour la première fois ensemble sur un événement. On sentait l’attention particulière des leaders de ces entités dans le choix des mots. En effet, si le GDG Nantes a l’habitude d’avoir un public de développeur, il avait en face un public mixte (développeur et créateurs numériques). Et l’inverse pour le Stereolux :)

HTML5 pour les créatifs

Jean-François Garreau nous a ensuite fait une présentation générale d’HTML5. Il a enchainé les démonstrations d’applications web présentant des animations graphiques nous plongeant dans des univers variés (Google Maze, Plink, Rome…).

On retiendra qu’HTML5 n’est pas uniquement l’affaire des développeurs d’applications de gestion mais aussi celle des créateurs d’animations diffusées sur le web.

3 Workshops

Il fallait faire un choix entre les workshops suivants :

  • Responsive design
  • 3D sur le web (WebGL)
  • Applications web dynamiques (avec AngularJS)

J’aurai voulu assister à tous les workshops mais un choix devait être fait…snif… Mon choix s’est porté sur “AngularJS”.

Workshop : Applications web dynamiques avec AngularJS

Cette session était animée par Antoine Richard, un passionné de la dynamique côté navigateur. Il avait prévu nous faire développer et c’est ce qui s’est passé !

Installation de NodeJS

Les programmes d’installation de NodeJS ont été distribués pour les plateformes Linux, Mac et Windows. J’avais déjà NodeJS installé sur ma machine, donc rien à faire pour moi à cette étape.

Vous pouvez récupérer la dernière version de NodeJS : http://nodejs.org/.

Angular en quelques mots

  • Single Page Application
  • Etendre HTML
  • Simple et puissant
  • Par Google

Récupérer les sources

Les sources du Workshop ont été publiées sur Github : https://github.com/antoine-richard/angular-movie-workshop-early2013. Vous pouvez naviguer à travers les branches pour voir les changements entre les différentes étapes.

1
git clone https://github.com/antoine-richard/angular-movie-workshop

Vous allez récupérer 3 fichiers avec l’arborescence suivante :

En affichant le fichier index.html dans un navigateur on obtient :

Manipuler le DOM “sans” javascript

Cette partie a pour objectif d’illustrer la mise en place d’AngularJS dans une application et une modification du DOM sans écriture de code Javascript

Inclure le script angular.js

app/index.html
1
<script src="lib/angular.js"></script>

Déclarer l’attribut ng-app

app/index.html
1
<div id="container" class="intro" ng-app>

L’attribut ng-app permet de d’indiquer l’emplacement de l’application AngularJS. Il est possible d’avoir plusieurs applications dans une même page.

Déclarer un modèle “name” lié au champ de saisie

app/index.html
1
<input type="text" ng-model="name"/>

Utilisation du modèle pour afficher les mots saisis

app/index.html
1
2
3

<p>Hello {{name}} !</p>

Fichier app/index.html complet

app/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Movies App - AngularJS workshop at Stereolux</title>
  <link rel="stylesheet" href="css/movies.css"/>
  <script src="lib/angular.js"></script>
</head>
<body>
  
  <div id="container" class="intro" ng-app>

      <label>What's your name ?</label>
      <input type="text" ng-model="name"/>
      <p>Hello {{name}} !</p>

  </div>

  <p id="footer">AngularJS workshop at Stereolux</p>
      
</body>
</html>

Test

Ce que nous saisissons est automatiquement copié à la suite de “Hello”.

Cet exemple simple permet de mettre en évidence la philosophie d’AngularJS. La manipulation du DOM que nous faisons habituellement en javascript est dans cet exemple effectuée de façon déclarative.

Maintenant un peu de cinéma !

Après avoir écrit nos premières lignes d’AngularJS, nous passons au développement d’une application permettant de visualiser une liste de films, le détail de chaque film et des informations sur les acteurs principaux.

Ce développement va être fait en plusieurs étapes :

Step-0 : Utilisation d’un template

Récupérer les sources

1
git clone https://github.com/antoine-richard/angular-movie-workshop-early2013 -b step-0

Il y a 2 parties :

  • app : l’application que nous allons développer
  • server : un backend qui expose des services REST

Lancer le serveur

Se positionner à la racine des sources récupérées et lancer la commande :

1
node server/web-server.js

L’application initiale est accessible via l’adresse : http://localhost:8000/app/index.html

A cette étape, les données affichées sont statiques dans la page HTML :

app/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
...
<div id="container">

  <div>

      <h1>Movies</h1>

      <ul>
          <li>
              <span>Reservoir Dogs</span>
              <a href="#">Actors</a>
              <a href="#">Sheet</a>
              <span class="year">1992</span>
          </li>
          <li>
              <span>Pulp Fiction</span>
              <a href="#">Actors</a>
              <a href="#">Sheet</a>
              <span class="year">1994</span>
          </li>
      </ul>

  </div>
</div>
...

L’objectif est maintenant d’apporter du dynamisme à cette page (injection des données via javascript).

Suppression des données statiques

app/index.html
1
2
3
4
5

<div id="container">
  <div ng-view></div>
</div>

Les noeuds <h1> et <ul> sont supprimés.

L’attribut ng-view déclare l’emplacement de la vue AngularJS. Nativement, il ne peut y avoir qu’un seul attribut ng-view par application AngularJS.

Créer un fragment HTML représentant la liste des films

Créer un fichier app/partials/movies.html représentant le template des informations supprimées précédemment.

app/partials/movies.html
1
2
3
4
5
6
7
8
9
10
11
12

<h1>Movies</h1>

<ul>
  <li ng-repeat="movie in movies">
      <span>{{movie.title}}</span>
      <a href="#">Actors</a>
      <a href="#">Sheet</a>
      <span class="year">{{movie.year}}</span>
  </li>
</ul>

On remarquera l’utilisation de ng-repeat qui est une directive fournie par AngularJS pour itérer sur un ensemble d’objets. Dans notre cas, la boucle se fera sur la liste des films (variable movies).

{{movie.title}} permet d’afficher la propriété title de l’objet movie.

Modifier le contrôleur

app/js/app.js
1
2
3
4
5
6
7
8
9
10
11
12
angular
  .module('moviesApp', [])
  .config(function($routeProvider) {
      $routeProvider.when('/movies', { controller: 'MoviesCtrl', templateUrl: 'partials/movies.html' });
  })
  .controller('MoviesCtrl', function($scope) {
      $scope.movies = [
          { title: "Reservoir Dogs", year: 1992 },
          { title: "Pulp Fiction", year: 1994 },
          { title: "Jackie Brown", year: 1997 }
      ];
  });

$routeProvider permet de faire le lien entre une URL, un contrôleur et un template.

$scope.movies permet d’ajouter une variable qui porte le nom movies et qui sera visible dans les vues. C’est sur cette variable qu’est appliqué la directive ng-repeat à la précédente étape.

Test

http://localhost:8000/app/index.html#/movies

Step-1 : Externalisation du contrôleur + Appel AJAX

L’objectif de cette étape est de mettre à jour la vue non plus via un tableau javascript “en dur” mais avec un appel serveur asynchrone. Nous allons profiter pour externaliser les contrôleurs de l’application.

Inclusion du nouveau fichier à créer

app/index.html
1
<script src="js/controllers.js"></script>

Suppression du contrôleur dans le fichier app/js/app.js

app/js/app.js
1
2
3
4
5
var app = angular.module('moviesApp', []);

app.config(function($routeProvider) {
  $routeProvider.when('/movies', { controller: 'MoviesCtrl', templateUrl: 'partials/movies.html' });
});

Création du fichier app/js/controllers.js

app/js/controllers.js
1
2
3
4
5
app.controller('MoviesCtrl', ['$http', '$scope', function($http, $scope) {
  $http.get('/server/data/movies.json/$').success(function(movies) {
      $scope.movies = movies;
  });
}]);

$http fourni par AngularJS permet de faire un appel HTTP asynchrone.

A noter qu’il est possible d’utiliser $resource pour effectuer des appels REST. Son utilisation doit être préférée à $http lorsque le serveur à interroger est RESTful. $http est un service de plus bas niveau que $resource.

Test

http://localhost:8000/app/index.html#/movies

Step-2 : Filtre et Tri

Dans cette étape, l’écran principal va être enrichi pour permettre un filtre de données par titre et un tri suivant plusieurs critères.

Ajout d’un filtre et un tri

app/partials/movies.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

<h1>Movies</h1>

<div class="filter-box">
  <div>
      <label>Filter</label>
      <input type="text" ng-model="query.title">
  </div>
  <div>
      <label>Sort by</label>
      <select ng-model="order" ng-init="order='year'">
          <option value="year">Year</option>
          <option value="title">Title</option>
      </select>
  </div>
</div>

<ul>
  <li ng-repeat="movie in movies | filter:query | orderBy:order">
      <span>{{movie.title}}</span>
      <a href="#">Actors</a>
      <a href="#">Sheet</a>
      <span class="year">{{movie.year}}</span>
  </li>
</ul>

ng-model=”query.title” permet de faire une correspondance entre la saisie de la partie filter et la valeur de la propriété title du filtre.

movie in movies | filter:query le symbole | permet de définir une fonction à appliquer sur un ensemble de données, ici movies. Le mot clé filter permet de filtrer des données suivant un ensemble de critères. Ici l’objet query est un conteneur qui contient l’ensemble de clé/valeur représentant le filtre à appliquer. Dans notre exemple, il contient title=XXXXXX est la valeur saisie.

La combinaison des deux éléments précédents permet ainsi d’appliquer un filtre sur les films affichés en fonction du texte saisi.

ng-model=”order” déclare un modèle qui porte le nom order et qui contient la valeur sélectionnée dans la liste déroulante.

ng-init=”order=’year’” déclare la valeur initiale (à l’affichage de la vue) du modèle order. Cela permet d’appliquer un tri par défaut.

| orderBy:order applique un tri suivant une propriété (valeur du modèle order).

Test

Préparation de l’étape suivante

Ajoutons ensuite les 2 fichiers suivants pour préparer l’étape “Step-3”.

  • app/partials/actors.html : template d’affichage des acteurs d’un film
app/partials/actors.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<h2>
  Pulp Fiction
  <a href="#/movies">Home</a>
</h2>
<h3>
  Actors
  <a href="#/movies/44">Movie</a>
</h3>

<ul>
  <li>
      <span>Bruce Willis</span>
      <span class="role">Butch Coolidge</span>
  </li>
  <li>
      <span>Samuel L. Jackson</span>
      <span class="role">Jules Winnfield</span>
  </li>
  <li>
      <span>John Travolta</span>
      <span class="role">Vincent Vega</span>
  </li>
</ul>
  • app/partials/movie.html : template d’affichage du détail d’un film.
app/partials/movie.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<h2>
  Pulp Fiction
  <a href="#/movies">Home</a>
</h2>
<h3>
  Info sheet
  <a href="#/movies/44/actors">Actors</a>
</h3>

<ul>
  <li>
      <span class="info">Year</span>
      <span>1994</span>
  </li>
  <li>
      <span class="info">Duration</span>
      <span>154 minutes</span>
  </li>
  <li>
      <span class="info">Budget</span>
      <span>$8.5 million</span>
  </li>
</ul>

<img src="/server/data/posters/pulp_fiction.jpg">

Step-3 : Navigation entre différentes vues

Cette étape va permettre la mise en place de plusieurs vues accessibles via des URL spécifiques :

  • /movies : la vue développée jusqu’ici
  • /movies/:movieId (exemple /movies/4) : affichage du détail d’un film, le template à utiliser est app/partials/movie.html
  • /movies/:movieId/actors (exemple /movies/4/actors) : affichage des acteurs principaux d’un film, le template à utiliser est app/partials/actors.html

Configuration de la navigation

app/js/app.js
1
2
3
4
5
6
7
8
9
var app = angular.module('moviesApp', []);

app.config(function($routeProvider) {
  $routeProvider
      .when('/movies',                   { controller: 'MoviesCtrl',            templateUrl: 'partials/movies.html' })
      .when('/movies/:movieId',          { controller: 'MovieDetailCtrl',           templateUrl: 'partials/movie.html'  })
      .when('/movies/:movieId/actors',   { controller: 'MovieActorsCtrl',           templateUrl: 'partials/actors.html' })
      .otherwise(                       { redirectTo: '/movies' });
});

Mise à jour des contrôleurs

app/js/app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
app.controller('MoviesCtrl', ['$http', '$scope', function($http, $scope) {
  $http.get('/server/data/movies.json/$')
  .success(function(movies) {
      $scope.movies = movies;
  });
}]);

app.controller('MovieDetailCtrl', ['$http', '$scope', '$routeParams', function($http, $scope, $routeParams) {
  $http.get('/server/data/movies.json/'+$routeParams.movieId+'/$') // Lightweight movie object
  .success(function(lightweightMovie) {
      $scope.movie = lightweightMovie;
  });
}]);

app.controller('MovieActorsCtrl', ['$http', '$scope', '$routeParams', function($http, $scope, $routeParams) {
  $http.get('/server/data/movies.json/'+$routeParams.movieId) // Heavyweight movie object
  .success(function(fullMovie) {
      $scope.movie = fullMovie;
  });
}]);

Le template d’affichage des acteurs d’un film

app/partials/actors.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

<h2 class="light">
  {{movie.title}}
  <a href="#/movies">Home</a>
</h2>
<h3>
  Actors
  <a href="#/movies/{{movie.id}}">Movie</a>
</h3>

<ul>
  <li ng-repeat="actor in movie.actors">
      <span>{{actor.name}}</span>
      <span class="role">{{actor.role}}</span>
  </li>
</ul>

Le template d’affichage du détail d’un film

app/partials/movie.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

<h2>
  {{movie.title}}
  <a href="#/movies">Home</a>
</h2>
<h3>
  Info sheet
  <a href="#/movies/{{movie.id}}/actors">Actors</a>
</h3>

<ul>
  <li>
      <span class="info">Year</span>
      <span>{{movie.year}}</span>
  </li>
  <li>
      <span class="info">Duration</span>
      <span>{{movie.duration}} minutes</span>
  </li>
  <li>
      <span class="info">Budget</span>
      <span>${{movie.budget}} million</span>
  </li>
</ul>

<img ng-src="{{movie.poster}}">

Mise à jour du template d’affichage de tous les films

app/partials/movies.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

<h1>Movies</h1>

<div class="filter-box">
  <div>
      <label>Filter</label>
      <input type="text" ng-model="query.title">
  </div>
  <div>
      <label>Sort by</label>
      <select ng-model="order" ng-init="order='year'">
          <option value="year">Year</option>
          <option value="title">Title</option>
      </select>
  </div>
</div>

<ul>
  <li ng-repeat="movie in movies | filter:query | orderBy:order">
      <span>{{movie.title}}</span>
      <a href="#/movies/{{movie.id}}/actors">Actors</a>
      <a href="#/movies/{{movie.id}}">Sheet</a>
      <span class="year">{{movie.year}}</span>
  </li>
</ul>

Test

Step-4 : Mise en place d’un service AngularJS

AngularJS permet la mise en place de services réutilisables que les contrôleurs peuvent appeler. Cela permet d’avoir des contrôleurs ne contenant pas de logique complexe.

Nous allons maintenant mettre en place dans l’application une gestion de films favoris.

Inclusion d’un nouveau fichier javascript pour nos services

app/index.html
1
<script src="js/services.js"></script>

Service de gestion de favoris

app/js/services.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.factory('starService', function() {
  
  var starred = [];

  return {
      toggleStar: function(id) {
          starred[id] = !starred[id];
      },
      isStarred: function(id) {
          return starred[id];
      }
  }

});

La méthode factory permet de déclarer un service réutilisable avec un nom (ici starService). Ce service est un singleton.

Il expose deux fonctions :

  • toggleStar(id) pour ajouter en favori
  • isStarred(id) qui détermine si un film est favori

Utilisation du service depuis le contrôleur

app/js/controllers.js
1
2
3
4
5
6
7
8
9
10
11
app.controller('MovieDetailCtrl', ['$http', '$scope', '$routeParams', 'starService', function($http, $scope, $routeParams, starService) {
  $http.get('/server/data/movies.json/'+$routeParams.movieId+'/$') // Lightweight movie object
  .success(function(lightweightMovie) {
      $scope.movie = lightweightMovie;
      $scope.favorite = starService.isStarred(lightweightMovie.id) ? "filled" : "";
  });
  $scope.toggleStar = function(id) {
      starService.toggleStar(id);
      $scope.favorite = starService.isStarred(id) ? "filled" : "";
  }
}]);

Lors du retour de l’appel serveur, le service créé est utilisé pour savoir si le film récupéré a été mis en favori. Si le film est favori la variable favorite prend la valeur filled sinon elle vaudra “”.

La fonction toggleStar mise dans l’objet $scope lui permettra d’être utilisée par les templates.

Mise à jour du template d’affichage du détail d’un film

app/partials/movie.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

<h2>
  {{movie.title}}
  <a href="#/movies">Home</a>
</h2>
<h3>
  Info sheet
  <a href="#/movies/{{movie.id}}/actors">Actors</a>
</h3>

<ul>
  <li>
      <span class="info">Year</span>
      <span>{{movie.year}}</span>
  </li>
  <li>
      <span class="info">Duration</span>
      <span>{{movie.duration}} minutes</span>
  </li>
  <li>
      <span class="info">Budget</span>
      <span>${{movie.budget}} million</span>
  </li>
  <li>
      <span class="info">Favorite</span>
      <span class="star {{favorite}}" ng-click="toggleStar(movie.id)"></span>
  </li>
</ul>

<img ng-src="{{movie.poster}}">


Deux choses à noter :

  • {{favorite}} va prendre la valeur filled ou "" suivant la logique définie dans le contrôleur
  • ng-click permet de définir une action à exécuter lors du clic sur la zone favori. L’action consiste à appeler la fonction toggleStar définie précédemment avec l’identifiant du film.

Test

Step-5 : Animation

AngularJS intègre la possibilité d’appliquer des animations CSS. Cette fonctionnalité n’est accessible actuellement qu’en béta (version 1.1.4).

Récupérer la version béta d’AngularJS et remplacer le fichier app/lib/angular.js

AngularJS v1.1.4 béta

Ajouter le fichier app/css/animation.css

app/css/animation.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.custom-enter-setup, .custom-leave-setup, .custom-move-setup {
  -webkit-transition: .5s linear all;
  -moz-transition: .5s linear all;
  -o-transition: .5s linear all;
  transition: .5s linear all;
  position: relative;
}

.custom-enter-setup {
  left: -10px;
  opacity: 0;
}
.custom-enter-setup.custom-enter-start {
  left: 0;
  opacity: 1;
}

.custom-leave-setup {
  left: 0;
  opacity: 1;
}
.custom-leave-setup.custom-leave-start {
  left: -10px;
  opacity: 0;
}

.custom-move-setup {
  opacity: 0.5;
}
.custom-move-setup.custom-move-start {
  opacity: 1;
}

Inclure le nouveau fichier CSS

app/index.html
1
2
3
...
<link rel="stylesheet" href="css/animation.css"/>
...

Appliquer une animation à la liste des films

app/partials/movies.html
1
2
3
4
5
6
7
8
9
10
11
12

...
<ul>
  <li ng-repeat="movie in movies | filter:query | orderBy:order" ng-animate="'custom'">
      <span>{{movie.title}}</span>
      <a href="#/movies/{{movie.id}}/actors">Actors</a>
      <a href="#/movies/{{movie.id}}">Sheet</a>
      <span class="year">{{movie.year}}</span>
  </li>
</ul>
...

  • On remarquera l’utilisation de la directive ng-animate pour appliquer une animation à un élément du DOM.

  • custom représente le préfixe des noms des classes CSS qu’AngularJS va appliquer.

  • Les suffixes enter-setup, leave-setup, … que l’on voit dans le fichier app/css/animation.css sont des mots clés qu’AngularJS va utiliser pour appliquer les styles successifs dans l’ordre.

Test

En rechargeant la page principale, on obtient une animation (changement d’opacité et translation de la gauche vers la droite).

Step-6 : Directives

Nous avons jusqu’ici utiliser des directives fournies par AngularJS. Il est temps d’en créer une pour notre application.

Création d’un fichier source de directives

app/js/directives.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
angular.module('moviesApp.directives', [])
.directive('mvActor', function() {
    return {
      restrict: 'E',
      replace: true,
      scope: {
          name: '='
      },
      template: '<span></span>',
      link: function(scope, element, attributes) {
          if (scope.name === 'Samuel L. Jackson') {
              scope.name += ' !';
              element.addClass('samuel');
          }
      }
    };
});

Un nouveau module dont le nom est moviesApp.directives est créé. Il ne définit que la directive :

  • porte le nom mvActor
  • est de type Element (restrict : E). Une directive peut être de type :
    • E : Element <mv-actor></mv-actor>
    • A : Attribut <div mv-actor="samuel" />
    • C : Class <div class="mv-actor:exp;" />
    • M : Comment <!-- directive mv-actor exp -->
  • va remplacer l’élément du DOM sur lequel il est appliqué (replace:true)
  • est paramétrable via une propriété name
  • va se baser sur le template défini pour l’attribut template pour générer l’affichage
  • va exécuter la fonction définie pour la propriété link après la phase de “compilation” de la directive et avant de créer l’affichage de la vue. C’est via cette propriété que l’on peut injecter du dynamisme à une directive.

Cette directive applique un traitement particulier à “Samuel L. Jackson” :

  • Ajout du ! à la suite de son nom
  • Application de la classe samuel qui permet d’afficher sa photo

Inclure le nouveau fichier de script app/js/directives.js

app/index.html
1
<script src="js/directives.js"></script>

Déclaration du module directives comme dépendance de l’application

app/js/app.js
1
2
var app = angular.module('moviesApp', ['moviesApp.directives']);
...

Utilisation de la directive

app/partials/actors.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!--[if lte IE 8]>
    <script>
        document.createElement('mv-actor');
    </script>
<![endif]-->

<h2 class="light">
  {{movie.title}}
  <a href="#/movies">Home</a>
</h2>
<h3>
  Actors
  <a href="#/movies/{{movie.id}}">Movie</a>
</h3>

<ul>
  <li ng-repeat="actor in movie.actors">
      <mv-actor name="actor.name"></mv-actor>
      <span class="role">{{actor.role}}</span>
  </li>
</ul>

Test

Voilà Samuel L. Jackson, un acteur au-dessus des autres, bien mis en valeur :)

Le code complet

1
git clone https://github.com/antoine-richard/angular-movie-workshop-early2013 -b step-7

ou via l’interface web de Github https://github.com/antoine-richard/angular-movie-workshop-early2013/tree/step-7

Le mot de la fin d’Antoine Richard

Liens utiles

Comments