Rechercher dans ce blog

mercredi 13 novembre 2013

Les scopes

 

Je vous ai déjà parlé des scopes. Je ne vous en avais pas dit grand chose étant donné le peu d'élements que j'avais alors évoqués. On va revenir maintenant sur ce point important. Quand vous créez une application AngularJS il se crée un scope racine. Lorsque vous créez un contrôleur vous générez un scope enfant du scope racine. Ce scope enfant hérite du scope racine à la mode Javascript, c'est à dire par prototype, donc sans classe. Pour celui (ou celle) qui est habitué à l'héritage classique c'est un peu dépaysant. Un prototype n'est pas une classe mais un objet bien vivant. Donc en Javascript un objet hérite tout simplement d'un autre objet.

Lorsque dans du code Javascript on tombe sur une propriété d'un objet, cette propriété est évidemment recherchée dans cet objet. Si elle n'est pas trouvée on remonte la chaîne des prorotypes jusqu'à la trouver. Si on ne la trouve pas on génère une erreur. Je vous raconte tout ça parce que les scopes justement répondent à ce fonctionnement avec une chaîne de prototypes. Je vais prendre un exemple simple pour vous montrer les conséquences de cela (JSFiddle) :

<style>
  .ng-scope {
  border-style: dotted solid;
    border-width: thin;
    border-color: red;
    margin: 1em;
    padding: 1em;
  }
  label {
    color: red;
  }
</style>
<div ng-app>
  <label for="root">Scope racine</label>
  <input type="texte" id="root" placeholder="Entrez du texte" 
ng-model="message">
  <div ng-controller="Controleur">
    <label for="ctrl">Scope enfant</label>
    <input type="texte" id="ctrl" placeholder="Entrez du texte" 
ng-model="message">
  </div>
  <script>
    function Controleur($scope) {}
  </script>
<div>
J'ai ajouté du style pour visualiser les scopes avec un petit liseré rouge. Vous allez faire le premier test suivant : entrez du texte dans la première zone de saisie. Vous constatez que la seconde se remplit avec le même texte, pourquoi ? J'ai créé un contrôleur mais je n'ai pas mis de code d'initialisation. Au départ dans le scope enfant la variable message n'existe pas. Hors il y a un lien au niveau du contrôle. La variable message n'est pas trouvée au niveau du scope enfant, elle est donc recherchée en remontant la chaîne des prototypes, donc au niveau du scope racine. Ici elle existe parce qu'on l'a crée en faisant notre saisie. Tant que vous tapez du texte dans le premier contrôle le second en est l'image fidèle. Maintenant tapez un peu de texte dans le second contrôle et revenez en entrer dans le premier. Maintenant vous vous rendez compte que les deux contrôles sont devenus indépendants. Le fait de saisir du texte dans le second contrôle a créé la variable message au niveau du scope enfant.

Si vous modifiez le code en initialisant la variable message dans le contrôleur, alors les deux contrôles sont immédiatement indépendants. Il y a une variable message dans le scope racine et une variable du même nom dans le scope enfant :
function Controleur($scope) {
  $scope.message = "mon message";
}
Nous allons poursuivre notre expérimentation des scopes en modifiant légèrement le code précédent (JSFiddle) :
<style>
  .ng-scope {
  border-style: dotted solid;
    border-width: thin;
    border-color: red;
    margin: 1em;
    padding: 1em;
  }
  label {
    color: red;
  }
</style>
<div ng-app>
  <label for="root">Scope racine</label>
  <input type="texte" id="root" placeholder="Entrez du texte" 
ng-model="objet.message">
  <div ng-controller="controleur">
    <label for="ctrl">Scope enfant</label>
    <input type="texte" id="ctrl" placeholder="Entrez du texte"
ng-model="objet.message">
  </div>
  <script>
    function controleur($scope) {}
  </script>
<div>
Si vous ne voyez pas la différence je vous le dis. Au lieu de définir une simple variable message j'utilise une propriété d'un objet objet.message. Qu'est-ce que cela change ? Si vous faites les tests comme précédemment vous ne verrez aucune différence dans la première partie : toute saisie dans le premier contrôle entraine le remplissage automatique du second.

C'est la suite qui devient intéressante : si vous entrez du texte dans le second contrôle le premier le suit fidèlement ! Il n'y a aucun moyen de découpler les deux contrôles, sauf si vous commencez votre saisie au niveau du second ! Comment cela se fait-il ? Etant donné qu'il n'y a aucune initialisation dans le code l'objet est créé à la première saisie. Si vous la faite au niveau du scope parent c'est lui qui contient l'objet. Quand vous faites ensuite une saisie au niveau du scope enfant comme l'objet n'est pas trouvé à ce niveau on remonte le chercher dans le scope racine. Par contre si vous commencez la saisie dans le scope enfant l'objet est créé à ce niveau. Lorsque vous faites ensuite une saisie au niveau du scope racine un nouvel objet est créé la aussi et vos deux contrôles sont bien indépendants !

Nous avons vu qu'un scope est généré pour chaque contrôleur. Il y a des directives qui aussi génèrent des scopes, comme ngRepeat. Voici un exemple visualisé (JSFiddle) :
<style>
  .ng-scope {
  border-style: dotted solid;
    border-width: thin;
    border-color: red;
    margin: 1em;
    padding: 1em;
  }
  label {
    color: red;
  }
</style>
<div ng-app ng-controller="controleur">
  <form ng-submit="ajouter()">
    Nom : <input type="text" name="nom" ng-model="nom">
    <button type="submit">Ajouter</button>
  </form>
  <br>
  <ul>
    <li ng-repeat="nom in noms">{{nom}}</li>
  </ul>
  <script>
    function controleur($scope) {
      $scope.noms = new Array();
      $scope.ajouter = function() {
        $scope.noms.push($scope.nom);
        $scope.nom = '';
      };
    }
  </script>
</div>

Nom :

  • {{nom}}
Lorsque vous ajoutez des noms dans la liste vous pouvez voir la création concomitante d'un scope. C'est nécessaire parce que sinon la variable nom serait ambigue parce qu'elle n'a pas la même valeur selon le contexte de la répétition.

dimanche 3 novembre 2013

Visibilité


Il arrive fréquemment qu'on veuille masquer ou au contraire afficher un élément d'une page selon le contexte. On le fait en général en CSS avec :
display:none
pour rendre invisible. C'est ce que fait AngularJS avec les directives ngShow et ngHide. Voici un exemple (JSFiddle) :
<div ng-app>
  Cochez pour voir la suite : 
  <input type="checkbox" ng-model="voir"><br/>
  <p ng-show="voir">Je suis la suite !</p>
  Cochez pour ne pas voir la suite : 
  <input type="checkbox" ng-model="cacher"><br/>
  <p ng-hide="cacher">Je suis la suite !</p> 
</div>
J'ai créé un lien entre les cases à cocher et les paragraphes avec la directive ngModel. Ainsi lorsque la valeur la case change elle est répercutée sur la valeur de la classe dans les paragraphes. Au départ on a ce code qui est généré :
<div class="ng-scope" ng-app="">
  Cochez pour voir la suite : 
  <input class="ng-pristine ng-valid" ng-model="voir" 
type="checkbox"><br>
  <p style="display: none;" ng-show="voir">Je suis la suite !</p>
  Cochez pour ne pas voir la suite : 
  <input class="ng-pristine ng-valid" ng-model="cacher" 
type="checkbox"><br>
  <p ng-hide="cacher">Je suis la suite !</p> 
</div>
On se rend compte qu'une règle de style display: none a été ajoutée pour le premier paragraphe qui doit être invisible. Si vous changez la valeur des coches vous verrez le style s'adapter automatiquement.

Classes


La directive ngClass permet de déclarer de façon dynamique des classes pour des éléments HTML. Il suffit d'utliser une liaison pour la valeur de la classe en utilisant une expression. Voici un exemple (JSFiddle) :
<div ng-app ng-controller="controleur">
  <button ng-click="voirDanger()">Danger</button>
  <button ng-click="voirWarning()">Alerte</button>
  <button ng-click="voirInfo()">Information</button>
  <div ng-class="maClasse">{{message}}</div>
  <script>
    function controleur($scope) {
      $scope.voirDanger = function() {
        $scope.message = 'On est en danger !';
        $scope.maClasse='rouge';
      };
      $scope.voirWarning = function() {
        $scope.message = 'Ce n\'est qu'une alerte.';
        $scope.maClasse='orange';
      };
      $scope.voirInfo = function() {
        $scope.message = 'Une petite information.';
        $scope.maClasse='bleu';
      };
    }
  </script>
</div>
J'ai prévu deux propriétés pour le scope : message qui contient le message à afficher et maClasse qui contient la classe à utiliser pour le message. Le tout est géré par un simple contrôleur qui comporte 3 méthodes, une pour chacun des aspects désirés. Les messages sont déclenchés par des boutons avec la directive ngClick.

Validation de formulaire


La validation d'un formulaire côté client est une tâche qui a été grandement améliorée avec l'arrivée du HTML 5. Mais d'une part il y a encore des navigateurs en service qui ne tiennent pas compte de ces améliorations. D'autre part il arrive souvent qu'on doivent ajouter des fonctionnalités. AngularJS apporte une réponse à ces deux problématiques. Pour la première c'est automatique et même les vieux navigateurs sont assistés pour les nouvelles possibilités du HTML 5. Pour le reste nous allons le voir. Voici l'exemple d'un formulaire d'inscription avec trois champs de saisie obligatoires pour le nom, le prénom et l'adresse Email (JSFiddle) :
<div ng-app>
  <style>
    .ng-invalid { border-color: #B94A48; }
    .rouge { color: red; }
    .vert { color: green; }
  </style>
  <h1>Inscription</h1>
  <form name="ajoutUtilisateur" ng-controller="controleur" 
ng-submit="soumission(ajoutUtilisateur.$valid)">
    <label for="nom">Nom :</label>
    <input type="text" id="nom" placeholder="Votre nom"
ng-model="user.nom" required>
    <label for="nom">Prénom :</label>
    <input type="text" id="prenom" placeholder="Votre prénom"
ng-model="user.prenom" required>
    <label for="email">Email :</label>
    <input type="email" id="email" placeholder="Votre Email" 
ng-model="user.email" required>
    <button type="submit">Envoyer</button>
    <div ng-class="messageClass" ng-show="message">{{message}}
</div>
  </form>
  <script>
    function controleur($scope) {
      $scope.message = '';
      $scope.soumission = function (valid) {
        if(valid) {
          $scope.messageClass='vert';
          $scope.message = 'Merci ' + $scope.user.nom + 
' votre inscription est validée !';
        }
        else {
          $scope.messageClass='rouge';
          $scope.message = 'Désolé mais il y a des 
données non valides !';
        }
      };
    }
  </script>
</div>

Lorsque AngularJS rencontre une balise form il crée un objet FormController. Parmi toutes les propriétés intéressantes de cet objet il y a $valid qui est vraie si tous les contrôles du formulaire ont une valeur correcte. Lors de la soumission du formulaire je transmets donc en paramètre cette valeur au contrôleur qui va effectuer son traitement en fonction d'elle.

Lorsqu'un contrôle n'est pas valide AngularJS lui adjoint la classe ng-invalid, on peut utiliser cette classe pour ajouter du style, comme je l'ai fait ici en prévoyant une règle pour la bordure. AngularJS prévoit ainsi 4 classes :
  • ng-invalid : la valeur n'est pas valide
  • ng-valid : la valeur est valide
  • ng-pristine : l'utilisateur n'a pas encore utilisé ce contrôle
  • ng-dirty : l'utilisateur a déjà utilisé ce contrôle
Evidemment dans un environnement réel il faudrait envoyer ces informations au serveur pour les mémoriser. Nous verrons cet aspect plus tard.

Un petit TP 


On va faire un nouveau point de ce qu'on a vu avec un petit exercice. Le but est de remplir un tableau à partir d'un formulaire. On prévoit deux zones de texte et des boutons radio pour entrer les informations. On veut que les deux zones de texte aient une bordure rouge tant que la valeur n'est pas valide et verte ensuite, et qu'elles soient à saisie obligatoire. On veut aussi un message en cas de soumission avec des champs non valides (mais avec un navigateur qui gère bien le HTML 5 ça ne devrait pas arriver) :

Ajout d'utilisateurs




{{message}}
Utilisateurs
Nom Prénom Rôle
{{utilisateur.nom | uppercase}} {{utilisateur.prenom}} {{utilisateur.role}}

Vous trouverez ma solution dans ce JSFiddle. Vous avez peut-être remarqué que ça ne sert à rien de prévoir un bouton radio checked étant donné qu'avec la liaison de données c'est le scope qui gère tout. Il faut donc judicieusement le renseigner.

samedi 2 novembre 2013

Les contrôleurs


Nous avons vu quelques exemples d'utilisation d'AngularJS avec une approche un peu "magique". Tout se passe dans les coulisses, on s'est contenté d'utiliser des directives et des expressions. On a vu également qu'il se construit un scope pour renseigner la vue. Au niveau de cette vue nous avons adopté une démarche qui ne respecte pas trop le MVC parce que nous avons initialisé des valeurs, ce qui est normalement la tâche du contrôleur au niveau du modèle. Nous allons donc mettre maintenant en oeuvre un contrôleur pour nous rendre plus conforme au modèle MVC avec un premier exemple simple (JSFiddle) :
<div ng-app ng-controller="controleur">
  <p>{{texte}}</p>
  <script>
    function controleur($scope) {
      $scope.texte = 'Bonjour !';
    }
  </script>
</div>
Le contrôleur est une fonction Javascript qui comporte un paramètre. Celui-ci est important parce qu'il correspond à une référence du scope, on fait donc une injection de dépendance. Le contrôleur ici crée et initialise la propriété texte du scope. Une expression dans la vue permet ensuite l'affichage de cette valeur. Le contrôleur est déclaré avec la directive ngController, cette déclaration a aussi pour effet d'implicitement créer un scope pour ce contrôleur. L'emplacement de la directive ngController n'est pas anodin parce que le scope correspondant va concerner tout ce qui se trouve à l'interieur de la balise correspondante.

Pour avoir une visualisation de cette organisation voici un petit schéma inspiré de la documentation :


 

La directive ngChange


Nous avons vu ci-dessus un premier exemple élémentaire de contrôleur pour comprendre l'architecture du système. On a vu que le contrôleur peut créer le modèle et renseigner le scope. Une autre fonction du contrôleur est aussi d'ajouter des comportements. Imaginez que vous avez une zone de saisie de texte et que vous voulez connaître en permanence le nombre de mots contenus dans le texte saisi. Voici comment réaliser cela (JSFiddle) :
<div ng-app ng-controller="controleur">
  <textarea ng-model="texte" ng-change="changement()"></textarea>
  Nombre de mots : {{nombre}}
  <script>
    function controleur($scope) {
      $scope.nombre = 0;
      $scope.changement = function() {
        $scope.nombre = $scope.texte.split(/\b\w+\b/).length-1;
      };
    }
  </script>
</div>
Le code utilise une nouvelle directive : ngChange. Elle permet la mise en place d'une écoute de l'évenement de changement dans le contrôle. Donc dès que le texte change au niveau du contrôle textarea on va appeler la fonction désignée changement(). Cette fonction compte le nombre de mots et met à jour la propriété nombre du scope, l'expression {{nombre}} est alors actualisée.

Pour avoir une visualisation de cette organisation voici un petit schéma inspiré de la documentation :


Un petit TP


On va faire un nouveau point de ce qu'on a vu avec un petit exercice. Le but est de construire encore un tableau mais cette fois de personnes avec leur nom et leur âge. On prévoit deux zones de texte pour entrer les informations. On veut que le tableau s'actualise à mesure :

Nom : Age :
Personnes
Nom Age
{{personne.nom}} {{personne.age}}

Vous trouverez ma solution dans ce JSFiddle. La seule difficulté réside dans le fait que cette fois la méthode du contrôleur comporte des paramètres. La déclaration ngSubmit permet d'intercepter la soumission du formulaire et de l'orienter sur la méthode du contrôleur. Nous n'avons rien prévu dans notre code concernant la validité des données saisies, si ce n'est de préciser le type des zones, en particulier number pour l'âge. Si vous regardez le code généré vous allez trouver de nouvelles classes ng-valid, ng-dirty et ng-valid-number. Ce sont des classes de travail d'AngularJS. On se rend compte qu'elles ont à voir avec la validation des données. Entrez autre chose qu'un nombre pour l'âge et vous verrez que la valeur ne s'inscrit pas dans le tableau. La classe ng-valid-number se transforme alors en ng-invalid-number. Tout ça est géré par le contrôleur de formulaire d'AngularJS.

Les liaisons (ngModel)


Nous allons maintenant introduire un peu d'interaction à nos pages. Imaginez que vous entrez une valeur dans une zone de texte et que cette valeur est utilisée en temps réel pour modifier le DOM. C'est ce que nous allons réaliser à présent.

Directive ngModel


La directive ngModel permet de créer une liaison avec une zone de saisie. Voici un exemple (JSFiddle) :
<div ng-app>
  <input id="nom" type="text" ng-model="nom">
  <h1>Bonjour {{nom}} !</h1>
</div>
Avec la directive ngModel on a fait une liaison entre la zone de texte et une variable située dans le modèle (en fait le scope comme nous le verrons bientôt) qui a le même nom que celui prévu pour la directive. Autrement dit il est créé une variable nom dans le modèle. L'expression située dans la balise h1 est liée à cette variable et affiche sa valeur et se raffraichit dès que celle-ci change. Au final l'expression est mise à jour instantanément lorsque la valeur de la zone de texte est modifiée.

Case à cocher


La directive ngModel peut s'appliquer à tout contrôle. Voici un exemple avec des cases à cocher (JSFiddle) :
<div ng-app>
  <form ng-init="valeur1=true; valeur2=false">
    Valeur1 : <input type="checkbox" ng-model="valeur1"> 
{{valeur1}}<br>
    Valeur2 : <input type="checkbox" ng-model="valeur2">
{{valeur2}}
  </form>
</div>
On initialise les variables valeur1 et valeur2 avec la directive ngInit. Remarquez que la liaison est bidirectionnelle parce que la case qui concerne la valeur1 se retrouve cochée au départ. Ensuite la modification de la valeur de chaque case est immédiatement reportée dans la valeur de la variable correspondante.

Select


La directive ngModel peut s'appliquer aussi à un contrôle select, c'est juste un peu plus délicat à mettre en oeuvre. Prenons le cas d'une liste de ville (JSFiddle) :
<div ng-app>
  <div ng-init="villes=['Paris','Lyon','Marseille','Toulouse']">
    <select ng-model="valeur" 
ng-options="villes.indexOf(v) as v for v in villes">
      <option value="">-- Choisissez une ville --</option>
    </select>
    Valeur de l'option : {{valeur}}
  </div>
</div>
On initialise le tableau des noms de villes avec la directive ngInit. Ensuite la directive ngOptions permet de remplir la liste avec les valeurs du tableau des villes. La directive ngModel sert de liaison pour la valeur du choix, ici des nombres.

On vient de voir que la directive ngModel peut s'appliquer aussi à un contrôle select. Voyons un autre exemple, cette fois nous allons utiliser une expression pour changer une classe (JSFiddle) :
<div ng-app>
  <label for="texte">Choisissez la classe :</label>
    <select ng-model="classe" ng-init="classe='noir'">
      <option>noir</option>   
      <option>rouge</option>
      <option>vert</option>
      <option>bleu</option>
      <option>jaune</option>
    </select>
  <button type="button" class="{{classe}}">Classe "{{classe}}"
</button>
</div>
On a une liste avec des noms de classes. La directive ngModel établit une liaison entre la valeur sélectionnée et la classe appliquée au bouton. La valeur initiale de la liste est fixée grâce à la directive ngInit.

Le scope


Je vous ai dit qu'AngularJS respecte le modèle MVC, pour le moment on a rien vu de tel ! On a juste utilisé des procédures "magiques" pour obtenir des résultats. Pour rappel le modèle MVC est composé de ces 3 entités :
  • Le modèle qui est chargée de contenir les données, donc l'état de l'application
  • La vue qui est chargée d'afficher les informations pour l'utilisateur
  • Le contrôleur qui est chargé d'établir la relation entre le modèle et la vue, c'est un peu le chef d'orchestre.
Dans nos exemples jusqu'à présent on n'a pas créé de contrôleur et pourtant ça fonctionne. Pour le modèle on a vu que la directive ngInit permet d'initialiser des valeurs. La vue est générée à partir de la page HTML avec les doubles accolade mais sans qu'on sache vraiment comment ça se passe. On va donc regarder un peu dans la machine pour comprendre comment ça fonctionne...

Reprenons ce code qui fait une liaison entre un contrôle et un affichage :
<div ng-app>
  <input id="nom" type="text" ng-model="nom">
  <h1>Bonjour {{nom}} !</h1>
</div>
Si vous avez la curiosité de regarder le code généré vous allez trouver cela :
<div class="ng-scope" ng-app="">
  <input class="ng-pristine ng-valid" id="nom" ng-model="nom"
ng-init="nom='Toto'" type="text">
  <h1 class="ng-binding">Bonjour Toto !</h1>
</div>
On voit que quelques classes sont apparues : ng-scope, ng-pristine, ng-valid et ng-binding. Ces classes ont été créées lors de la compilation du code par AngularJS. Mais ce qu'on ne voit pas c'est qu'il a aussi été créé un scope. C'est un objet qui a pour fonction de :
  • observer toute modification du modèle
  • offrir un contexte d'exécution pour les expressions
Il est organisé de façon hiérarchique un peu à l'image du DOM. Si nous observons notre exemple au démarrage nous avons la création d'un scope racine. On peut le trouver en utilisant l'extension Batarang des outils de développement de Chrome :
$id: "003"
$parent: null
$root: h
__private__: Object
nom: "Toto"
this: h
__proto__: Object
On voit une propriété nom qui a pour valeur "Toto". Si vous explorez cet objet vous pourrez trouver d'autres informations comme par exemple les watchers. Mais il n'est pas très utile de fouiller tout ça étant donné que les scopes sont automatiquement créés ! On aura l'occasion d'explorer les scopes dans une phase ultérieure avec une application plus fournie. Si vous voulez déjà explorer un peu plus ce domaine vous pouvez consulter la documentation.

vendredi 1 novembre 2013

Les directives


Les directives permettent d'augmenter les possibilités du HTML, ce sont des déclarations de fonctions Javascript. Ces fonctions ajoutent à des balises, attributs, classes et même commentaires HTML, des comportements. AngularJS possède un compilateur qui parcourt le DOM et repère les directives, il exécute alors la fonction correspondante. Nous avons déjà vu la directive ng-app qui sert à l'initialisation d'AngularJS. Nous avons également rencontré ng-bind qui a le même effet que les doubles accolades et sert donc à évaluer des expressions. AngularJS comporte de nombreuses directives, nous allons en voir quelques unes.

Directives ngClick et ngInit


La directive ngClick permet de déclencher un comportement lors d'un clic sur l'élément correspondant. La directive ngInit permet de faire des initialisations avant l'exécution des autres directives. Voici un exemple (JSFiddle) :
<div ng-app>
  <button ng-click="number = number * 2" ng-init="number=1">
Multiplier par 2</button>
  Nombre : {{number}}
</div>
On déclare une variable number et on on l'initialise à 1 avec la directive ngInit et on utilise ensuite la directive ngClick pour faire l'action "multiplier par 2".

Directives ngRepeat et ngShow


La directive ngRepeat permet de créer de nouveaux éléments du DOM. C'est une directive que vous utiliserez souvent. Voici un exemple (JSFiddle) :
<div ng-app>
  <div ng-init="voitures = [
    {marque:'Renault', couleur:'verte'},
    {marque:'Mercedes', couleur:'rouge'},
    {marque:'Rolls', couleur:'or'},
    {marque:'Ferrari', couleur:'rouge'}
  ]"> 
  J'ai {{voitures.length}} voitures :
  <ul>
    <li ng-repeat="voiture in voitures">
      Une {{voiture.marque}} {{voiture.couleur}}.
    </li>
    </ul>
  </div>
</div>
La directive ngShow permet d'afficher ou de ne pas afficher l'élément dans lequel elle est selon une valeur booléenne. Prenons le même exemple que précédemment mais en sélectionnant les voitures rouges (JSFiddle) :
<div ng-app>
  <div ng-init="voitures = [
      {marque:'Renault', couleur:'verte'},
      {marque:'Mercedes', couleur:'rouge'},
      {marque:'Rolls', couleur:'or'},
      {marque:'Ferrari', couleur:'rouge'}
    ]">
    J'ai des voitures rouges :
    <ul>
      <li ng-repeat="voiture in voitures" 
ng-show="voiture.couleur == 'rouge'">
 Une {{voiture.marque}}.
     </li>
    </ul>
  </div>
</div>
La directive ngRepeat possède quelques propriétés spéciales bien pratiques. On a par exemple la propriété $index qui donne la valeur de l'index (JSFiddle) :
<div ng-app>
  <div ng-init="voitures = [
      {marque:'Renault', couleur:'verte'},
      {marque:'Mercedes', couleur:'rouge'},
      {marque:'Rolls', couleur:'or'},
      {marque:'Ferrari', couleur:'rouge'}
    ]">
    J'ai {{voitures.length}} voitures :
    <ul>
    <li ng-repeat="voiture in voitures">
      [{{$index + 1}}] Une {{voiture.marque}} {{voiture.couleur}}.
    </li>
  </ul>
</div>

Un petit TP

On va faire un peu le point de ce qu'on a vu avec un petit exercice. Le but est de construire un tableau de courses à faire avec les noms des articles et leur prix :

J'ai 4 articles à acheter
Article Prix
Farine € 2.30
Sucre € 1.35
Gâteaux € 3.90
Huile € 5.25

Vous avez ma solution sur ce JSFiddle.