Débuter avec SproutCore

14 February 2010 par Robin Boutros

SproutCore 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'
            })
        })
    })

Et voici le résultat :

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.

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.

8 commentaires pour “Débuter avec SproutCore”

  1. Laurent dit :

    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…

  2. Jean-françois dit :

    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.

  3. j-Luc dit :

    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.

  4. Stefano dit :

    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.

  5. abdessamad dit :

    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 ;) !!!!

  6. tojosoucre dit :

    est ce qu’on peut installer Sproutcore sur windows et cpmment? SVP me repondre d’urgence

  7. Robin dit :

    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.

  8. cedtao dit :

    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

Laisser un commentaire