Développez un agenda temps-réel avec le framework Meteor

Meteor est un framework Javascript full-stack orienté temps-réel. Il
excelle lorsqu’il s’agit de réaliser une application monopage en peu de
temps, bien que ce ne soit pas le seul usage auquel il est destiné. Je
vais vous montrer, par la pratique, comment l’exploiter pour construire
un agenda temps-réel basique. A vos claviers !

Préparation

Afin de mener à bien notre projet de calendrier, nous allons profiter de l’excellente librairie FullCalendar. Celle-ci gère une très grande partie de l’interface dont nous aurons besoin. Il s’agit d’un plugin jQuery qui affiche un calendrier peuplé avec des événements fournis par une source de données. Cette source peut être un tableau Javascript, une URL vers un flux JSON, un flux Google Calendar ou encore une fonction Javascript. De plus l’API très complète que propose le plugin, nous permettra de réaliser exactement ce que nous voulons, dans un temps restreint.

Afin de rendre la démarche aussi simple que possible nous allons nous limiter dans ce tutoriel aux choses suivantes :
- afficher des événements saisis dans le calendrier
- permettre à un utilisateur enregistré de créer un nouvel événement
- la description d’un événement se fait via une boîte de dialogue, après avoir sélectionné une période sur le calendrier
- un événement peut être public (visible de tous) ou privé (visible par son créateur uniquement)
- un événement saisi s’affichera immédiatement sur l’écran de tous les utilisateurs (la magie de Meteor)

Création de l’application

Rentrons dans le vif du sujet et commençons par installer Meteor :

curl https://install.meteor.com | /bin/sh

Il n’y a rien d’autre à faire et nous pouvons dès maintenant créer notre projet avec la commande suivante :

meteor create mycalendar

Vous pourrez voir quelques fichiers d’exemple dans votre nouveau répertoire. Supprimez-les, ils ne seront pas utiles (mais conservez le répertoire .meteor).

Ajout des packages utiles

Meteor met à disposition un certain nombre de packages permettant d’apporter rapidement des fonctionnalités de base (et non basiques) à son application. Celles qui nous intéresserons dans ce tutoriel sont le système de gestion de comptes utilisateur et le framework Bootstrap.

Le système de gestion de comptes va nous permettre de gérer facilement des comptes utilisateur sans développement particulier. Meteor a découpé ce système en plusieurs packages afin de permettre aux développeurs de ne prendre que ce qui leur importe. En ce qui nous concerne, nous utiliserons les packages accounts-password et accounts-ui. Le premier apporte l’authentification par mot de passe et le deuxième nous permet de disposer d’un template prêt à l’usage pour l’affichage d’un petit widget permettant la création de compte et l’authentification des utilisateurs.

meteor add accounts-password
meteor add accounts-ui

Comme vous pouvez le constater, il est très facile d’ajouter des packages. Nous allons faire de même pour bootstrap qui va nous simplifier la création de l’interface en apportant des classes CSS et le Javascript nécessaire pour notre boîte de dialogue.

meteor add bootstrap

Nous disposons de tous les packages nécessaires, attardons nous maintenant sur l’arborescence de notre projet.

Structure de l’application

Une application Meteor est une application web. Elle sera donc composée d’HTML, de CSS et de Javascript. Ce qui est toutefois un petit peu particulier est la manière dont sont traités les fichiers d’un projet. Lorsque vous lancez la commande meteor pour démarrer votre application, Meteor va compiler votre arborescence en deux versions de votre application, l’une sera lancée dans un conteneur Node.js côté serveur et l’autre sera envoyée au client qui l’exécutera dès réception. Il faut donc séparer ce qui doit être séparé et partager uniquement ce qui peut l’être.

Un fichier peut avoir trois visibilités : côté serveur, côté client ou les deux. Ce qui va déterminer cette visibilité sera leur emplacement dans le projet. Ensuite, la plupart des fichiers sont interprétés et fusionnés par type de contenu (HTML, CSS, Javascript), toujours en fonction de leur visibilité. Enfin, l’ordre de chargement de ces fichiers repose actuellement sur leur nommage et celui de leurs répertoires parents.

Pour exemple, je vais prendre l’arborescence finale de notre projet que vous pouvez reproduire dès à présent :

client
|---css
|   |---style.css
|---lib
|   |---config.js
|   |---fullcalendar.css
|   |---fullcalendar.js
|   |---jquery-ui.custom.min.js
|---app.js
|---calendar.html
lib
|---collections.js
server
|---publications.js

Pour commencer, nous avons un répertoire client. Celui-ci contient tout ce qui est livré au client, ces fichiers ne se trouveront donc pas dans la version serveur de votre application. On y trouve le style CSS, le fichier HTML qui regroupera dans notre cas tout le HTML nécessaire, les fichiers nécessaires à l’exécution de FullCalendar, un fichier config.js qui nous contiendra un peu de configuration de l’interface et enfin app.js dans lequel nous placerons l’essentiel de notre code.

Le répertoire server contient tout ce qui est visible uniquement par le serveur. En aucun cas un client peut accéder à un fichier de ce répertoire. C’est généralement l’endroit où mettre la logique la plus sensible. Dans le cadre de notre tutoriel, nous y plaçons un fichier publications.js qui contiendra la définition des flux de données qui seront vus et modifiés par les clients.

Enfin, le répertoire lib est un répertoire qui est partagé par le client et le serveur. Il peut donc accueillir du code commun. C’est le parfait endroit pour définir des collections, la structure de données qui est au coeur de Meteor.

Meteor charge les fichiers et répertoires dans un ordre alphabétique. Le nom est donc un choix important. De plus, les fichiers d’un répertoire lib seront chargés avant tous les autres. Profitez-donc de ces répertoires pour y placer de la configuration ou tout élément à portée globale.

La donnée c’est la clé

La pierre angulaire d’un agenda sont les événements qui le composent. Nous allons donc devoir définir cette donnée dans notre application. Meteor utilise MongoDb comme base de données par défaut, nous travaillerons donc sur des collections. Une collection MongoDb est une structure de données qui regroupe un ensemble de documents. Dans notre cas, ces événements seront des événements.

Nous allons définir une collection events dans le fichier lib/collections.js :

/* Contenu du fichier lib/collections.js */
Events = new Meteor.Collection('events');

Ainsi, nous pourrons utiliser l’objet Events pour effectuer des requêtes sur la collection d’événements. Meteor possède une particularité qui est que chaque client reçoit une copie locale de la base de données sur laquelle il peut travailler dans la limite des opérations autorisées. Nous allons donc devoir définir ces limites. Pour cela, renseignons le fichier server/publications.js :

/* Contenu du fichier server/publications.js */
Meteor.publish('events', function() {
  return Events.find({
    $or: [{
      isPrivate: false
    },
    {
      isPrivate: true,
      ownerId: this.userId
    }]
  });
});

Events.allow({
  insert: function (userId, doc) {
    if (userId) {
      return true;
    }
    return false;
  }
});

La première partie du code définit une publication. C’est un flux de données qui est envoyé à chaque client. Nous avons ainsi configuré ce flux pour ne faire voir à un client que les événements publics ou les événements privés qui appartiennent à ce même client.

Enfin, nous autorisons, via la méthode allow(), les insertions en base de données d’un client si celui-ci est authentifié.

Interface

Afin de nous faciliter une grande partie du travail sur l’interface, nous avons recours à la librairie FullCalendar. Après avoir téléchargé et décompressé l’archive, prenez les principaux fichiers dont nous aurons besoin et placez les dans le dossier client/lib pour obtenir le résultat suivant :

client/lib/fullcalendar.css
client/lib/fullcalendar.js
client/lib/jquery-ui.custom.min.js

La librairie est prête à être utilisée. Nous passons donc à la configuration du package accounts-ui qui permet d’obtenir sans le moindre effort les éléments nécessaires pour la gestion de comptes utilisateur. Nous passerons pour cela par un fichier config.js, également placé dans le dossier client/lib.

/* Contenu du fichier client/lib/config.js */
Accounts.ui.config({
  passwordSignupFields: 'USERNAME_AND_EMAIL'
});

Accounts.ui.config() prend un objet de configuration permettant de définir certains aspects du comportement du widget. L’option passwordSignupFields ainsi définie indique de rendre obligatoire le nom d’utilisateur et l’adresse email à l’inscription.

Une fine touche de style

Afin de rendre notre projet un peu plus présentable, nous allons ajouter un peu de style CSS. Nous n’aurons besoin que de quelques lignes car les styles inclus dans le framework Bootstrap et ceux de la librairie FullCalendar font déjà le plus gros du travail.

/* Contenu du fichier client/css/style.css */
body {
  margin-top: 20px;
  font-size: 14px;
}

#login-buttons {
  float: right;
  margin-bottom: 20px;
}

HTML et templates

Maintenant que le terrain a été préparé pour l’interface, intéressons-nous au contenu du fichier client/calendar.html que nous allons dérouler au fur et à mesure.

<head>
  <title>Meteor Calendar</title>
</head>
<body>
  <div class="container">

    <div class="row login">
      <div class="span2 offset10">{{> loginButtons}}</div>
    </div>

    <div class="well">
      <h1>Meteor Calendar</h1>
    </div>

    {{#unless currentUser}}
      {{> warning}}
    {{/unless}}

    {{> calendar}}

    {{> modal}}

  </div>
</body>

Comme vous pouvez le voir, il n’y a pas que de l’HTML ici. Meteor utilise un moteur de template, dérivé de la syntaxe Handlebars (qui a été baptisé Spacebars). Nous l’utilisons pour inclure des templates avec la syntaxe {{> monTemplate}}. Un template est une partie de code HTML qui peut être appelée à divers endroits et à plusieurs reprises. Ils permettent notamment de maintenir une certaine organisation dans le code qui constitue vos vues.

Spacebars nous permet également d’utiliser des structures de contrôle. Dans notre cas nous avons utilisé {{#unless maVariable}} pour effectuer le rendu d’un bloc si currentUser ne retourne aucun utilisateur. currentUser est un élément du framework Meteor qui retourne l’utilisateur qui est actuellement connecté ou null. Nous pouvons l’utiliser puisque nous laissons Meteor gérer les comptes utilisateur à l’aide des packages accounts-password et accounts-ui. Remarquez également l’inclusion du template spécial loginButtons qui est apporté par le package accounts-ui et qui nous permet d’afficher le widget contenant l’interface des boutons de connexion.

Il nous reste maintenant à définir les templates que nous avons fait apparaître dans le <body>.

<template name="warning">
  <div class="alert alert-block">
    <button type="button" class="close" data-dismiss="alert">&times;</button>
    <h4>Warning!</h4>
    <p>You need to sign in to access every functionality.</p>
  </div>
</template>

<template name="calendar">
  <div id="calendar"></div>
</template>

<template name="modal">
  <div id="eventModal" class="modal hide fade">
    <div class="modal-header">
      <button type="button" class="close" data-dismiss="modal">×</button>
      <h3>Create an event</h3>
    </div>
    <div class="modal-body">
      <form class="form-horizontal">
        <div class="control-group">
          <label class="control-label" for="event-title">Title</label>
          <div class="controls">
            <input type="text" name="event-title" id="event-title">
          </div>
        </div>
        <div class="control-group">
          <div class="controls">
            <label class="checkbox" for="event-allday">
              <input type="checkbox" name="event-allday" id="event-allday">
              All day
            </label>
          </div>
        </div>
        <div class="control-group">
          <div class="controls">
            <label class="checkbox" for="event-private">
              <input type="checkbox" name="event-private" id="event-private">
              Make this event private
            </label>
          </div>
        </div>
        <input type="hidden" name="event-start" id="event-start">
        <input type="hidden" name="event-end" id="event-end">
      </form>
    </div>
    <div class="modal-footer">
      <button class="btn" data-dismiss="modal">Cancel</button>
      <button class="btn btn-primary save">Save</button>
    </div>
  </div>
</template>

La définition d’un template passe par la création d’un tag <template> qui possède un attribut name dont la valeur sera utilisée pour faire référence au template. Le premier template que nous avons défini affiche simplement un bloc d’alerte pour indiquer qu’il faut s’authentifier pour profiter de toutes les fonctionnalités de l’application. Le suivant est très simple et ne contient qu’une balise qui servira de conteneur pour le rendu du calendrier. Enfin, le dernier, qui n’est long que par la nécessité d’utiliser une structure HTML spécifique à Bootstrap pour avoir un joli rendu de formulaire, contient le code la boîte de dialogue qui sera utilisée pour définir un nouvel événement. Les informations à renseigner dans ce formulaire seront simplement le titre de l’événement, une indication pour savoir s’il dure toute la journée et une autre pour savoir s’il est privé. La date de début et de fin seront renseignées automatiquement dans des champs cachés car nous les reprendrons de la sélection qui aura été faite dans FullCalendar.

Logique applicative

L’interface est prête, nous pouvons donc nous concentrer sur le coeur de l’application client qui se trouve dans client/app.js.

Pour commencer, souvenez-vous des publications que nous avions définies au niveau serveur. Elles nous permettaient de diffuser un sous-ensemble des données spécifiques à l’utilisateur. Et bien pour que cette diffusion puisse se faire, nous avons besoin de souscrire à ce flux.

Meteor.subscribe('events');

Avec la méthode Meteor.subscribe() nous créons un abonnement à la publication dont le nom est passé en paramètre. Cela nous permet de recevoir les données qui concernent le client.

Nous allons maintenant créer un objet qui va servir de namespace pour les principales méthodes dont aura besoin notre application.

var App = {};

Nous aurons besoin de trois fonctions. L’une pour récupérer les événements selon une date de début et de fin. Une autre pour transformer les événements issus de la base de données en objets exploitables par FullCalendar. Et enfin, une dernière qui sera chargée d’insérer un événement en base de données.

La fonction responsable de la récupération des événements se nommera getEvents() :

App.getEvents = function (start, end) {
  var events = Events.find({
    start: {
      $gte: start
        },
        end: {
          $lte: end
    }
  });
  return events;
};

Nous utilisons l’objet de collection Events pour effectuer une requête find() en lui passant un sélecteur MongoDb qui filtrera sur les documents dont les propriétés start et end sont comprises dans les bornes reçues en paramètre.

La transformations des documents de la base de données en objets événements se fera via une méthode mapDbEventsToEventSource() :

App.mapDbEventsToEventSource = function (eventCursor) {
  var eventArray = [];
  eventCursor.forEach(function (eventData) {
    var title = eventData.title;
    if (eventData.isPrivate) {
      title += ' - PRIVATE';
    }
    var color = '#3a87ad';
    if (eventData.ownerId !== Meteor.userId()) {
      color = '#ad433a';
    }
    var event = {
      title: title,
      start: eventData.start,
      end: eventData.end,
      allDay: eventData.allDay,
      color: color
    };
    eventArray.push(event);
  });
  return eventArray;
};

La fonction prend un curseur de collection en paramètre ce qui lui permet d’itérer sur les documents contenus par celui-ci. Nous récupérons le titre de l’événement. Nous ajoutons à ce titre une indication si l’événement est privé. Nous déterminons ensuite une couleur pour l’événement afin de différencier les événements créés par l’utilisateur connecté des autres. A ces informations s’ajoutent la date de début et de fin de l’événement. Le tout est inséré dans un objet qui est ajouté au tableau qui sera renvoyé à la fin de la fonction.

La dernière fonction qui vient peupler notre objet App se nommera createEvent() pour l’insertion d’un événement en base de données :

App.createEvent = function (title, start, end, allDay, isPrivate) {
  Events.insert({
    title: title,
    start: start,
    end: end,
    allDay: allDay,
    isPrivate: isPrivate,
    ownerId: Meteor.userId()
  });
};

Rien de particulier ici car nous ne faisons que reprendre les informations reçues en paramètre à l’exception de l’identifiant de l’utilisateur que nous obtenons via la méthode Meteor.userId().

Interaction

Nous avons défini en grande partie le fonctionnement de notre application mais il nous reste encore à gérer son affichage. Cela passe par l’initialisation du plugin FullCalendar et celle de la boîte de dialogue nous permettant de saisir les informations liées à un événement. Nous allons faire en sorte d’initialiser le plugin FullCalendar au moment où le template calendar est rendu à l’écran. Nous disposons pour cela d’un callback rendered() sur l’objet de template. Les objets de template sont créés automatiquement par Meteor dans l’objet global Template. Ils y sont rendus accessibles par le nom qui leur a été donné via l’attribut name dans le code HTML.

Template.calendar.rendered = function () {
  jQuery('#calendar').fullCalendar({
    header: {
      left: 'prev,next today',
      center: 'title',
      right: 'month,agendaWeek,agendaDay'
    },
    editable: false,
    events: function (start, end, callback) {
      var events = App.getEvents(start, end);
      var eventSource = App.mapDbEventsToEventSource(events);
      callback(eventSource);
    },
    selectable: true,
    select: function (start, end, allDay) {
      var $form = jQuery('#eventModal form');
          $form.find('#event-start').val(start.toISOString());
      $form.find('#event-end').val(end.toISOString());
          $form.find('#event-allday').prop('checked', allDay);
      jQuery('#eventModal').modal();
    }
  });
};

Nous passons à l’instance fullcalendar un certain nombre d’options pour paramétrer son affichage. Les éléments les plus importants sont les callbacks events() et select(). Le premier permet de définir une fonction pour récupérer les événements à afficher. Comme vous pouvez le voir nous y faisons usage des méthodes que nous avons précédemment définies dans notre objet App. Enfin, le callback select() est déclenché quand l’utilisateur sélectionne une période dans le calendrier. A ce moment-là nous peuplons le formulaire avec les informations dont nous disposons déjà (les dates de début et de fin notamment) et nous affichons la boîte de dialogue. Le comportement de cette boîte de dialogue sera également défini dans un callback rendered() :

Template.modal.rendered = function () {
  jQuery('#eventModal .modal-footer .save').click(function () {
    var $form = jQuery('#eventModal form');
    App.createEvent(
      $form.find('#event-title').val(),
      new Date($form.find('#event-start').val()),
      new Date($form.find('#event-end').val()),
      $form.find('#event-allday').prop('checked'),
      $form.find('#event-private').prop('checked')
    );
    jQuery('#eventModal').modal('hide');
  });
};

Nous réagissons au clic sur le bouton sauvegarder du formulaire. Nous utilisons la méthode App.createEvent() que nous avions définie en lui passant les informations contenues dans le formulaire pour créer un nouvel événement. Notre application est prête, il ne manque plus qu’à la faire réagir aux changements de données en temps-réel.

Un petit coup de réactivité

Un grand nombre des applications web modernes que nous utilisons aujourd’hui sont capables de réagir à un changement en temps-réel (notifications, chat…). Cela peut être effectué de différentes manières, et avec plus ou moins d’efforts. Meteor rend cet effort quasi nul en implémentant la réactivité dans la plupart de ses APIs. La réactivité est ce qui vous permet d’écrire du code dont le résultat est recalculé automatiquement à chaque changement opérant dans ses dépendances. Dans notre cas, nous souhaitons que la liste des événements affichés soit constamment à jour. Cela se fait simplement de la manière suivante :

Deps.autorun(function () {
  var events = Events.find({});
  if (events.count()) {
    jQuery('#calendar').fullCalendar('refetchEvents');
  }
});

Vous pouvez voir que nous effectuons une requête en sélectionnant l’ensemble des événements disponibles. Nous en faisons le compte pour finalement mettre à jour l’interface en appelant l’action refetchEvents sur notre instance FullCalendar. Le bout de code qui entoure le tout est responsable de toute la magie qui va opérer ensuite. La fonction Deps.autorun() permet d’évaluer du code une fois, puis à chaque fois qu’elle est notifiée d’un changement dans ses dépendances. Je n’entrerai pas dans le détail de la chose (j’ai prévu un autre article pour cela), mais ce qu’il faut simplement retenir c’est qu’à chaque fois que le résultat de events.count() change, nous appelons à nouveau l’action refetchEvents qui mettra à jour l’interface. Meteor se débrouille tout seul pour savoir quand des données ont changé, nous n’avons rien à faire d’autre. Voilà, nous avons construit un petit agenda en temps-réel.

Conclusion

On pourrait s’interroger sur l’utilité d’un agenda temps-réel mais ça serait passer à côté de l’essentiel. Vous avez pu voir qu’à aucun moment nous avons été ralentis par les contraintes techniques que pourrait amener la réalisation d’une application temps-réel. Nous l’avons fait, tout simplement. Tout n’est peut-être pas clair comme de l’eau de roche mais j’espère vous avoir donné envie de vous intéresser davantage au framework Meteor, et vous invite donc à vous rendre sur le site officiel. Un article plus complet sur le fonctionnement de Meteor est sur le point de voir le jour, et je ne manquerai pas de mentionner un lien ici quand ça sera fait.

2 commentaires

  1. Bonjour Sören,
    Merci pour ce tuto ! En tentant de le reproduire, j’ai une erreur au moment de l’affichage des events dans calendar.rendered : Exception from Tracker recompute function: TypeError: false is not a function (evaluating ‘callback(eventSource)’) (meteor.js, line 880), alors que l’eventSource est bien un objet rempli. Une idée de ce que j’aurais pu louper ?
    Merci 🙂

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.