Débuter avec SproutCore
14 February 2010 par Robin BoutrosSproutCore est un framework Javascript (écrit en ruby) développé par Charles Jolley.
Il est réellement rentré dans la cour des grands lorsque Apple, qui a désormais embauché Charles Jolley et grandement contribué à l'amélioration du framework, a annoncé que le framework serait utilisé pour le service en ligne MobileMe.
Le but avoué de SproutCore est de permettre la création d'applications web orientées desktop facilement et sans plug-in. Il dispose pour cela d'atouts intéressants puisqu'il repose sur un modèle MVC, propose notamment le binding d'objets qui permet de créer des applications complexes sans code inutile et bien plus encore.
De nombreuses heures seront nécessaires pour commencer à maitriser le framework, mais voici un tutoriel qui vous permettra de vous lancer en abordant la création complète d'une petite application SproutCore !
Tutorial de A à Z
Introduction
SproutCore (ça sonne bien, non ?) est un projet encore jeune, et le passage de la version 0.9 à 1.0 a introduit de très nombreux changements au niveau de son utilisation. Cela implique une documentation légère et parfois périmée, de même en ce qui concerne les tutoriels. C’est pourquoi débuter avec ce framework peut se révéler parfois frustrant.
Cet article se propose donc de vous guider dans le développement de votre première application SproutCore.
L’application
L’application qui se veut simple pour commencer aura pour but de vous permettre de tenir une liste des films que vous voulez voir.
Un clic sur le bouton « Ajouter un film » et une nouvelle entrée, que vous pouvez immédiatement éditer, est ajoutée à la liste. Cochez la checkbox lorsque vous avez vu le film en question. C’est aussi simple que ça.
Installation de SproutCore
SproutCore est disponible sous la forme d’une gem Ruby, donc dans la plupart des cas, l’installation se résumera à :
> gem install SproutCore
La gem est mise à jour à intervalles réguliers, si vous souhaitez la toute dernière version, vous pouvez la récupérer sur github. Voyez le site officiel pour plus de détails.
Créer la structure de l’application
Comme nombre de frameworks, SproutCore est fourni avec des générateurs conçus pour vous faciliter la vie.
Pour créer l’arborescence du projet Watchlist, dirigez-vous dans le dossier où vous souhaitez la créer, et tapez :
> sc-init watchlist > cd watchlist
Voilà, votre application a été générée. Le terme "application" peut ici être trompeur. Pour SproutCore, une application est une unique page web. Un projet SproutCore pourra contenir plusieurs applications. C'est souvent le cas sur de gros projets.
Vous pouvez d'ores et déjà tester l'application. Pour cela, lancez simplement le serveur avec la commande suivante :
> sc-server
En principe l'application sera accessible à http://localhost:4020/watchlist
"Welcome to SproutCore!"
Ce n'est pas encore très excitant, mais c'est un début.
Mise en place de l'architecture MVC
SproutCore est un framework qui repose sur une architecture MVC.
Nous allons donc commencer par créer le modèle, très simple dans notre cas, nous verrons ensuite comment créer les vues nécessaires, et nous nous attaquerons enfin au contrôleur qui constitue le gros du travail.
Voilà pour le programme!
Le modèle
Une "watchlist" n'est ni plus ni moins qu'une liste de films, vus ou pas (seen), ces derniers ayant un titre (title), un réalisateur (director), ainsi qu'une date de sortie (year).
Pour créer notre modèle, nous allons faire appel une fois de plus à un générateur :
> sc-gen model Watchlist.Movie
Nous allons définir notre modèle en déclarant nos attributs comme propriétés de notre classe Movie.
Watchlist.Movie = SC.Record.extend( /** @scope Watchlist.Movie.prototype */ { title: SC.Record.attr(String), director: SC.Record.attr(String), year: SC.Record.attr(Number), seen: SC.Record.attr(Boolean) }) ;
Comme nous le verrons en détail plus tard, une application SproutCore est indépendante de la technologie utilisée côté serveur. Le store utilisé pour accéder aux données des modèles a simplement besoin de connaître sa source de données et l'application communiquera via ajax avec le serveur en suivant les principes REST.
Par défaut, cette source de données, ce sont les "fixtures", qui permettent de se détacher des données côté serveur lors du début du développement, ce qui facilite grandement les choses.
Nous allons donc créer quelques entrées pour nous permettre de tester notre modèle.
Watchlist.Movie.FIXTURES = [ { "guid": "movie-1", "title": "Terminator 2", "director": "James Cameron", "year": 1991, "seen": true }, { "guid": "movie-2", "title": "Back to the future 2", "director": "Robert Zemeckis", "year": 1989, "seen": true }, { "guid": "movie-3", "title": "Moulin Rouge!", "director": "Baz Luhrmann", "year": 2001, "seen": false } ];
Après avoir ajouté ces entrées dans le fichier fixtures/movies.js, retournez sur l'application (http://localhost:4020/watchlist) puis rafraîchissez la page.
Rien de particulier ne se passe, mais en réalité, nos trois films ont été créés en mémoire. Pour le vérifier, ouvrez la console de votre navigateur (au hasard Firebug), et tapez :
> movies = Watchlist.store.find(Watchlist.Movie) > movies.getEach("title")
Voici le résultat que vous devriez obtenir si tout se passe bien.
Vous n'aurez pas forcement la dernière ligne cependant, il faut avoir une console coquine pour ça.
Passons maintenant à l'étape suivante pour ajouter un petit côté visuel à tout cela.
Les vues
Dans SproutCore, les vues comme le reste sont définis en Javascript. Une vue est une classe qui, via l'API "page design", va nous permettre de définir la configuration des objets qui constituent notre page, et qui va gérer l'affichage en générant le HTML en conséquence.
Pour commencer, jetons un coup d'œil à la vue par défaut qui se trouve dans resources/main_page.js
Watchlist.mainPage = SC.Page.design({ // SC.Page est un simple conteneur qui permet de gérer nos vues et de les afficher sur demande. mainPane: SC.MainPane.design({ // Toutes les applications SproutCore ont une classe MainPane (ou Pane, dont elle dérive), // c'est la seule vue qui peut directement se rattacher au body de la page. childViews: 'labelView'.w(), labelView: SC.LabelView.design({ layout: { centerX: 0, centerY: 0, width: 200, height: 18 }, textAlign: SC.ALIGN_CENTER, tagName: "h1", value: "Welcome to SproutCore!" }) }) });
Notre but pour cette application est d'avoir une barre header comportant un bouton pour ajouter nos films, 3 colonnes principales pour le contenu, et une barre footer indiquant le nombre de films vus et à voir.
Je vous épargnerai les détails de la mise en place de ces vues, le code reste très lisible.
A noter tout de même:
- on indique les vues définies au mainPane :
childViews: 'titleView directorView yearView topView bottomView'.w()
- définir à chaque fois le layout de la vue :
layout: { top: 0, left: 0, right: 0, bottom:0 },
mainPane: SC.MainPane.design({ childViews: 'titleView directorView yearView topView bottomView'.w(), topView: SC.ToolbarView.design({ layout: { top: 0, left: 0, right: 0, height: 36 }, childViews: 'labelView addButton'.w(), anchorLocation: SC.ANCHOR_TOP, labelView: SC.LabelView.design({ layout: { centerY: 0, height: 24, left: 8, width: 200 }, controlSize: SC.LARGE_CONTROL_SIZE, fontWeight: SC.BOLD_WEIGHT, value: 'My Watchlist' }), addButton: SC.ButtonView.design({ layout: { centerY: 0, height: 24, right: 12, width: 140 }, title: "Ajouter un film" }) }), titleView: SC.ScrollView.design({ hasHorizontalScroller: NO, layout: { top:36, bottom:32, left:0, width:300 }, backgroundColor: 'white', contentView: SC.ListView.design({ rowHeight: 21 }) }), directorView: SC.ScrollView.design({ hasHorizontalScroller: NO, layout: { top:36, bottom:32, left:300, width:300 }, backgroundColor: '#EFEFEF', contentView: SC.ListView.design({ rowHeight: 21 }) }), yearView: SC.ScrollView.design({ hasHorizontalScroller: NO, layout: { top:36, bottom:32, left:600, right:0 }, backgroundColor: '#DFDFDF', contentView: SC.ListView.design({ rowHeight: 21 }) }), bottomView: SC.ToolbarView.design({ layout: { bottom: 0, left: 0, right: 0, height: 32 }, childViews: 'summaryView'.w(), anchorLocation: SC.ANCHOR_BOTTOM, summaryView: SC.LabelView.design({ layout: { centerY: 0, height: 18, left: 20, right: 20 }, textAlign: SC.ALIGN_CENTER, value:'Nombre de films' }) }) })
Le contrôleur
Nous avons les données et une belle (hum hum) interface pour les afficher. Il nous manque le coeur de l'application : le contrôleur. Il va se charger de faire le lien entre le modèle et les vues.
Pour créer le contrôleur, vous commencez à être habitué, on passe par le générateur fourni :
> sc-gen controller Watchlist.MoviesController SC.ArrayController
Remarquez que l'on doit également donner le type du contrôleur. Ici ce sera un ArrayController sachant que notre watchlist est une simple liste de films.
Nous associerons plus tard les vues avec ce contrôleur. Celles-ci fonctionneront comme si elles étaient liées directement avec le tableau de données de l'ArrayController, l'énorme avantage étant que si on décide de changer l'attribut "content" du contrôleur et l'associer avec un autre tableau, les vues associées seront automatiquement mises à jour. Le contrôleur agit donc comme un proxy pour son contenu.
Prochaine étape ? Donner quelque chose à afficher à nos vues.
On veut que notre titleView affiche les titres des films, directorView les réalisateurs, que nos checkbox reflètent si l'on a vu le film ou pas etc.
Pour cela, on va binder nos vues avec 2 propriétés du contrôleur que nous venons de créer:
- arrangedObjects qui contient le tableau de films en mémoire
- selection qui contient la sélection courante
La contentView de notre titleView par exemple deviendra :
contentView: SC.ListView.design({ contentBinding: 'Watchlist.moviesController.arrangedObjects', selectionBinding: 'Watchlist.moviesController.selection', // le contenu de la ListView est bindé sur l'attribut "title" contentValueKey: "title", // la checkbox est associée au booléen "seen" contentCheckboxKey: "seen" })
De même pour la directorView...
contentView: SC.ListView.design({ contentBinding: 'Watchlist.moviesController.arrangedObjects', selectionBinding: 'Watchlist.moviesController.selection', contentValueKey: "director", rowHeight: 21 })
et la yearView
contentView: SC.ListView.design({ contentBinding: 'Watchlist.moviesController.arrangedObjects', selectionBinding: 'Watchlist.moviesController.selection', contentValueKey: "year", rowHeight: 21 })
Vues et contrôleur sont maintenant liés : chacun informe l'autre d'une quelconque modification. Un changement du titre de film par l'utilisateur dans l'interface sera transmis au contrôleur, et inversement, un changement du contenu du contrôleur sera transmis aux vues.
Ok, donc ce qu'il nous reste à faire est charger nos données pour que les vues aient quelque chose à afficher.
Pour cela, ouvrez le fichier main.js à la racine de l'application Watchlist.
Les commentaires nous indiquent ici ce que nous devons faire :
- Instanciation des vues
- Assigner la propriété "content" de notre contrôleur principal.
Watchlist.main = function main() { // Etape 1 : Instanciez vos vues Watchlist.getPath('mainPage.mainPane').append() ; // Etape 2 : assignez la propriété "content" de votre contrôleur // DONE var movies = Watchlist.store.find(Watchlist.Movie); Watchlist.moviesController.set('content', movies); } ;
Maintenant, précipitez-vous dans votre navigateur, rafraîchissez la page, et priez!
Vous devriez avoir la même chose que sur le screenshot suivant:
Actions
Nous avons désormais l'affichage, mais je suppose que vous aimeriez pouvoir agir sur ces données, en créer de nouvelles ou les supprimer.
Cela tombe bien, c'est ce que nous allons faire.
Editer un film
Ajouter l'édition se révèle être un jeu d'enfant : la ListView s'occupe de ça pour nous! Il nous suffit de lui demander en ajoutant la propriété suivante : canEditContent: YES
Par exemple :
// directorView contentView: SC.ListView.design({ contentBinding: 'Watchlist.moviesController.arrangedObjects', selectionBinding: 'Watchlist.moviesController.selection', contentValueKey: "director", rowHeight: 21, canEditContent: YES })
Supprimer un film
La suppression pourrait se montrer tout aussi conciliante... mais malheureusement pour nous ce n'est pas le cas ici : rajoutez la propriété canDeleteContent: YES et testez vous-même (supprimez un film avec la touche Suppr).
Une erreur apparaît dans votre console lorsque vous tentez de supprimer un film : SC.RecordArray is not editable.
Quand vous supprimez un élément, la ListView obéit et tente de supprimer un élément du RecordArray au travers du moviesController comme nous l'avons vu tout à l'heure.
Or le RecordArray n'est pas éditable. D'où l'erreur.
Pour résoudre ce problème, nous devons utiliser ce que l'on appelle un "deleguate" qui va nous permettre d'avoir le contrôle sur comment la ListView, et plus généralement une "collectionView" pourra supprimer des éléments.
Pour cela nous allons écrire la méthode du contrôleur qui sera appelée lorsque notre ListView voudra supprimer un élément.
// controllers/movies.js: Watchlist.moviesController = SC.ArrayController.create( // important, définit le contrôleur en tant que delegate SC.CollectionViewDelegate, /** @scope Watchlist.moviesController.prototype */ { collectionViewDeleteContent: function(view, content, indexes) { var records = indexes.map(function(idx) { return this.objectAt(idx); }, this); records.invoke('destroy'); var selIndex = indexes.get('min')-1; if (selIndex<0) selIndex = 0; this.selectObject(this.objectAt(selIndex)); } });
Ajout de film
L'ajout de film se fera via le bouton que nous avons ajouté à l'interface au début de ce tutoriel. Il nous suffit d'écrire la fonction d'ajout dans notre moviesController et l'appeler lors du clic sur le bouton.
// Ajoutez cette méthode au contrôleur // controllers/movies.js: addMovie: function() { var movie; // Ajoute un film au "store" avec des valeurs par defaut movie = Watchlist.store.createRecord(Watchlist.Movie, { "title": "Sans titre", "director":"n.a.", "seen": false }); // Sélectionne dans l'interface le film ajouté this.selectObject(movie); // Lance automatiquement l'action d'édition du film this.invokeLater(function() { var contentIndex = this.indexOf(movie); var list = Watchlist.mainPage.getPath('mainPane.titleView.contentView'); var listItem = list.itemViewForContentIndex(contentIndex); listItem.beginEditing(); }); return YES; }
Enfin, pour appeler la méthode lors du clic sur le bouton "Ajouter un film", nous devons définir les propriétés action et target.
addButton: SC.ButtonView.design({ layout: { centerY: 0, height: 24, right: 12, width: 140 }, title: "Ajouter un film", target: "Watchlist.moviesController", // indique l'objet que l'on veut appeler action: "addMovie" // indique la méthode de l'objet à appeler })
Si vous avez bien suivi, vous devriez désormais pouvoir ajouter un film d'un clic, l'éditer et le supprimer.
Bye bye fixtures
Il est temps de créer un vrai back-end! Celui-ci devra supporter un simple CRUD (Create, read, update and delete).
- GET /movies – Retourne tous les films
- GET movieURL – Retourne un film donné
- POST /movies – Créer un nouveau film
- PUT movieURL – Mettre à jour un film
- DELETE movieURL – Supprimer un film
Notre application communiquera avec notre serveur grâce à des messages formatés en JSON :
{ "guid": "/movie/123", "title": "titre", "director": "realisateur", "year":2000 "seen": true | false }
Etant donné que ce n'est le sujet du tutoriel, je vais vous fournir le script PHP du serveur ainsi que le script SQL de création de la table movies.
- Script PHP : Renommez le fichier en .php, et modifiez les informations de connexion à la base de données.
- Script MySQL
Vous pouvez par exemple mettre ce script (index.php) à la racine de votre serveur et ajouter un proxy dans le Buildfile à la racine de l'application de manière à rediriger les requêtes vers votre script :
proxy '/movies', :to => 'localhost:80'
Il vous faudra aussi ajouter un .htaccess de manière à ce que les requêtes faites par votre application soient correctement gérées par le script :
Options +FollowSymlinks RewriteEngine on RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ /index.php?url=$1 [L]
Exemple:
Une requête GET http://localhost/movies/1 sera réécrite en http://localhost/index.php?url=/movies/1
Les fixtures c'est beau et utile, mais il faut s'en séparer un jour... Nous allons créer une nouvelle source de données. Pour cela, nous utiliserons un nouveau générateur :
> sc-gen data-source Watchlist.MovieDataSource
Un nouveau fichier a été créé : apps/watchlist/data_sources/movies.js.
Il contient le squelette des méthodes suivantes que nous allons devoir écrire:
- fetch()
- retrieveRecord()
- createRecord()
- updateRecord()
- destroyRecord()
Fetch
Fetch est utilisée par le store lorsqu'on cherche des enregistrements en utilisant une requête. Nous allons donc commencer par écrire cette requête. Elle devra se trouver tout en haut du fichier data_sources/movies.js, en dehors de Watchlist.MovieDataSource = SC.DataSource.extend(); :
sc_require('models/movie'); Watchlist.MOVIES_QUERY = SC.Query.local(Watchlist.Movie, { orderBy: 'seen,title' });
Direction le fichier main.js, nous voulons désormais récupérer les films en utilisant notre requête, de manière à ce que fetch() soit appelée.
Remplacez
var movies = Watchlist.store.find(Watchlist.Movie);
par
var movies = Watchlist.store.find(Watchlist.MOVIES_QUERY);
Modifions maintenant la source de données dans watchlist/core.js
store: SC.Store.create({ commitRecordsAutomatically: YES }).from('Watchlist.MovieDataSource')
Voilà, tout est prêt pour coder fetch().
fetch: function(store, query) { if (query === Watchlist.MOVIES_QUERY) { SC.Request.getUrl('/movies').json() .notify(this, this.didFetchMovies, store, query) .send(); return YES; } return NO; }, // fonction callback qui permet de déverrouiller le store didFetchMovies: function(response, store, query) { if (SC.ok(response)) { store.loadRecords(Watchlist.Movie, response.get('body').content); store.dataSourceDidFetchQuery(query); } else store.dataSourceDidErrorQuery(query, response); },
RetrieveRecord
retrieveRecord: function(store, storeKey) { if (SC.kindOf(store.recordTypeFor(storeKey), Watchlist.Movie)) { var url = store.idFor(storeKey); SC.Request.getUrl(url).json() .notify(this, this.didRetrieveMovie, store, storeKey) .send(); return YES; } else return NO; }, didRetrieveMovie: function(response, store, storeKey) { if (SC.ok(response)) { var dataHash = response.get('body').content; store.dataSourceDidComplete(storeKey, dataHash); } else store.dataSourceDidError(storeKey, response); },
CreateRecord
createRecord: function(store, storeKey) { if (SC.kindOf(store.recordTypeFor(storeKey), Watchlist.Movie)) { SC.Request.postUrl('/movies').json() .notify(this, this.didCreateMovie, store, storeKey) .send(store.readDataHash(storeKey)); return YES; } else return NO; }, didCreateMovie: function(response, store, storeKey) { if (SC.ok(response)) { var url = response.header('Location'); store.dataSourceDidComplete(storeKey, null, url); // update url } else store.dataSourceDidError(storeKey, response); },
Update record
updateRecord: function(store, storeKey) { if (SC.kindOf(store.recordTypeFor(storeKey), Watchlist.Movie)) { SC.Request.putUrl(store.idFor(storeKey)).json() .notify(this, this.didUpdateMovie, store, storeKey) .send(store.readDataHash(storeKey)); return YES; } else return NO ; }, didUpdateMovie: function(response, store, storeKey) { if (SC.ok(response)) { var data = response.get('body'); if (data) data = data.content; // if hash is returned; use it. store.dataSourceDidComplete(storeKey, data) ; } else store.dataSourceDidError(storeKey); },
DestroyRecord
destroyRecord: function(store, storeKey) { if (SC.kindOf(store.recordTypeFor(storeKey), Watchlist.Movie)) { SC.Request.deleteUrl(store.idFor(storeKey)).json() .notify(this, this.didDestroyMovie, store, storeKey) .send(); return YES; } else return NO; }, didDestroyMovie: function(response, store, storeKey) { if (SC.ok(response)) { store.dataSourceDidDestroy(storeKey); } else store.dataSourceDidError(response); }
Conclusion
Voilà! Si vous avez eu le courage de tout suivre, vous avez une petite application totalement fonctionnelle, et vous devriez maintenant être prêt à explorer les possibilités du framework par vous même.
Concernant SproutCore, à vous de vous faire votre idée de la puissance et de l'utilité du framework en l'utilisant.
Cependant, si vous décidez par exemple de porter une application desktop sur le web, SproutCore serait définitivement une option à considérer tant il facilite le travail à ce niveau-là, ne serait-ce qu'en proposant des éléments d'interface très proches du monde desktop, en gérant les incompatibilités entre navigateurs ou enfin en proposant des concepts de programmation très utiles dans ce domaine.








23 March 2010 à 17:23
Bonjour,
Enfin un tuto en français sur sproutcore !
Connaitriez vous des développeurs sproutcore en France ? Je cherche en effet à porter une partie de notre progiciel en appli web…
24 March 2010 à 20:26
Bonjour
Combien y’a t-il de ligne de javascript pour faire cette petite appli? 300 ou 400.
Avec n’ importe quel framework coté serveur on fait la meme application avec une ligne de code.
(ex: generate scaffold watch_list title:string director:string seen:boolean)
Je ne pense pas qu’il y ai beaucoup d’entreprise qui est les moyens de se payer des développeurs (en dehors de celles qui vendent des ordinateurs avec des coques en alu) pour faire des applications avec sproutcore.
30 March 2010 à 10:44
Bonjour,
Je rencontre une difficulté avec ce tutoriel. En fait je suis bloqué au niveau de la suppression d’un film. Comme indiqué j’ajoute dans le fichier controllers/movies.js la déclaration de delegate (si j’enregistre le fichier et recharge la page pas de message d’erreur), puis j’ajoute la fonction “collectionViewDeleteContent”. A partir de ce niveau là j’obtient dans le navigateur un double message d’erreur:
“SyntaxError: Parse error”
“TypeError: Result of expression ‘Watchlist.moviesController’ [undefined] is not an object.”
Je n’arrive pas à comprendre ce qui provoque cette erreur.
31 March 2010 à 16:37
Jean-Francois,
Sais tu qu’il y a des entreprises qui font du J2EE avec du struts et du hibernate et d’autres frameworks verbeux du genre ? Meme en France et meme dans les entreprises qui fabriquent des ordinateurs avec des coques en titane ou en plastique dur.
Je te souhaite bonne chance pour vendre ton application scaffoldee.
Tu n’as peut être rien compris a cet article et peut être devrais tu le relire et ensuite aller relire tous les tutoriels que tu as pu croiser sur rails. Si tu apres ca tu sens que ton message est toujours adapte, change de carriere.
6 April 2010 à 18:36
merci pour ce tuto sa fai un bou de temps que je cherche une framework comme SproutCore
enfin j tester pas mal mais je trouve que celle la me va bien encore merci pour ton tuto sa ma trop aider
!!!!
8 May 2010 à 6:04
est ce qu’on peut installer Sproutcore sur windows et cpmment? SVP me repondre d’urgence
8 May 2010 à 16:33
Il te suffit d’ouvrir le CMD shell de windows en tapant “cmd” dans la barre de recherche du menu demarrer ou faire demarrer > executer, taper “cmd” et valider.
Ensuite tu executes la commande “gem install SproutCore” comme expliqué dans le tutorial. Pour cela il te faudra RubyGems, que tu peux choper sur rubyforge je suppose.
14 May 2010 à 21:02
TOut ca m’a l’air super
J’ai suivi le tutoriel jusqu’au bout.
Dans le localhost:4020 , tout fonctionne
mais lorsque je mets l’application sur un serveur en ligne, ca ne semble pas fonctionner. Il affiche l’arborescence fichiers.
Que faut-il faire pour que ca marche comme en local sur un serveur de production ?
Merci