Tutorial Angular PhoneCat connecté à un backend fait en Grails

publié le

Un exemple de projet utilisant Angular à la place des contrôleurs et des vues Grails (gsp).

Angular devenant de plus en plus populaire, j'ai décidé de l'utiliser dans un de mes nouveaux projets. Comme c'est mon premier contact avec ce framework, j'ai suivi l'excellent tutorial sur le site web d'Angular http://docs.angularjs.org/tutorial. Dans ce tutorial, il n'y a pas de backend seulement des fichiers JSON qui émulent une réponse en JSON qui proviendrait d'un contrôleur. J'ai donc décidé de construire un backend avec Grails.

Si vous voulez uniquement avoir un patron de Grails avec Angular, vous pouvez accéder au projet sur GitHub https://github.com/BenoitWickramarachi/grails-angular-phonecat

Si par contre vous voulez en savoir plus sur la manière dont j'ai intégré Angular avec Grails et persisté des fichiers JSON en base, voici les étapes:

Étape 1 Modèle de données:

La première étape est d'extraire la structure du modèle de données à partir des fichiers JSON du tutorial pour ensuite créer chaque objet de domaine Grails:


Phone.groovy:

package com.phonecat

class Phone {

	int age
	String phoneId
	String imageUrl
	String name
	String snippet
	
   static mapping = {
   }
	
    static constraints = {
    }
		
}

Vous pouvez avoir accès au code des autres objets de domaine sur GitHub https://github.com/BenoitWickramarachi/grails-angular-phonecat/tree/master/grails-app/domain/com/phonecat

J'ai fait uniquement 3 modifications par rapport au modèle:

- id pour un objet Phone a été renommé phoneID car l'id est un String et je préfère garder les ids numériques autogénérés par la BD.
- id pour un objet PhoneDetail a aussi été renommé par phoneId pour la même raison.
- Un des champs de l'objet Camera est nommé Primary qui est un mot clé réservé, donc je l'ai renommé en primaryCamera.

En raison de ces modifications, je n'ai pas pu utiliser le convertisseur JSON par défaut de Grails.
J'ai dû en effet créer 3 convertisseurs JSON personnalisés, pour les objets Phone, PhoneDetail et Camera.

Phone:

		JSON.registerObjectMarshaller( Phone ) { Phone phone ->
			return [
					age : phone.age,
					id : phone.phoneId,
					imageUrl : phone.imageUrl,
					name : phone.name,
					snippet : phone.snippet
			]
		}

PhoneDetail:

		JSON.registerObjectMarshaller( PhoneDetail ) { PhoneDetail phoneDetail ->
			return [
				additionalFeatures: phoneDetail.additionalFeatures,
				android: phoneDetail.android,
				availability: phoneDetail.availability,
				battery: phoneDetail.battery,
				camera: phoneDetail.camera,
				connectivity: phoneDetail.connectivity,
				description: phoneDetail.description,
				display: phoneDetail.display,
				hardware: phoneDetail.hardware,
				id: phoneDetail.phoneId,
				images: phoneDetail.images,
				name: phoneDetail.name,
				sizeAndWeight: phoneDetail.sizeAndWeight,
				storage: phoneDetail.storage
				]
			}

Camera:

		JSON.registerObjectMarshaller( Camera ) { Camera camera ->
			return [
					primary : camera.primaryCamera,
					features : camera.features,
			]
		}

Maintenant que le modèle de données est défini, il nous suffit de remplir la base de données avec les données provenant des fichiers JSON du tutorial.

Pour cela j'ai copié le répertoire 'app' sous le repertoire web-app pour que Grails ait accès à toutes les ressources du tutorial (Javascripts, CSS, images,...):


J'ai également copié le répertoire phones (qui contient les fichiers JSON) pour y avoir accès facilement depuis Grails.

Il faut maintenant charger les fichiers JSON dans la base de données. Pour cela je parse chaque fichier dans bootstrap.groovy et convertis leur contenu (String) en objet JSON,
que j'utilise par la suite pour construire chaque objet de domaine de Grails (car un objet JSON est comme un array donc nous pouvons le passer directement en paramètre au constructeur.)

def jsonPhones = grails.converters.JSON.parse(file.text)

J'ai dû effectuer une seule opération manuelle en raison des 2 exceptions (les 2 ids renommés en phoneId, et le champ primary renommé en primaryCamera).
Pour ces propriétés je les set directement, puis je persiste les objets en base:

def jsonPhones = grails.converters.JSON.parse(file.text)
jsonPhones.each {
	Phone phone = new Phone(it)
	phone.phoneId = it.id
	phone.save(flush:true, failOnError:true)
}

J'utilise flush:true pour voir les objets immédiatement en base (pour forcer Hibernate à envoyer directement les requêtes SQL après un save).

Vous pouvez trouver le code complet de Bootstrap sur https://github.com/BenoitWickramarachi/grails-angular-phonecat/blob/master/grails-app/conf/BootStrap.groovy

Maintenant que les données sont bien au chaud dans la base de données, nous pouvons construire un Contrôleur pour interagir avec elle.

Étape 2 Contrôleurs

Le tutorial d'Angular ne comporte que 2 actions:

- Afficher la liste des téléphones: /phones/phones.json
- Afficher les détails d'un téléphone: /phones/motorola-xoom-with-wi-fi.json

Cela signifie 2 URLs à définir dans URLMappings (on aurait pu en définir une seule mais je trouve ça plus clair de séparer les actions lister les téléphones et afficher les informations d'un téléphone).

class UrlMappings {

	static mappings = {
		
		"/phones/phones.json" {
			controller = "phones"
		}
		
		"/phones/${phoneId}.json" {
			controller = "phones"
			action = "showPhone"
		}
		
        "/"(view:"/index")
	}

}

Ensuite, grâce à la magie de Grails, écrire les contrôleurs se résument à quelques lignes de code:

PhonesController.groovy:

package com.phonecat

import grails.converters.JSON

class PhonesController {

	def index() {
		def json = Phone.list() as JSON
		render json
	}
		
    def showPhone() {
		def json = PhoneDetail.findByPhoneId(params.phoneId) as JSON
		render json
    }
	
}

Et c'est à peu près tout, le backend est prêt. Bien sûr nous aurions pu ajouter les opérations CRUD pour l'objet Phone mais je voulais avoir un backend minimal uniquement pour les requêtes du tutorial.

Étape 3 remplacement des vues Grails par le framework Angular

J'avais copié toutes les ressources nécessaires au fonctionnement du tutorial PhoneCat sous le répertoire /web-app de Grails dans une étape précédente, maintenant il reste à déclarer les dépendances dans le plugin de ressources. Pour cela j'ai ajouté un module Angular:

modules = {
    angular {
		resource url: 'css/app.css'
		resource url: 'css/bootstrap.css'
		resource url: 'css/animations.css'
	  
		resource url:'js/jquery.js'
		resource url:'js/angular.js'
		resource url:'js/angular-animate.js'
		resource url:'js/angular-route.js'
		resource url:'js/angular-resource.js'
		resource url:'js/app/app.js'
		resource url:'js/app/animations.js'
		resource url:'js/app/controllers.js'
		resource url:'js/app/filters.js'
		resource url:'js/app/services.js'
    }
}

Vous pouvez constater que, comme Angular va s'occuper du flow nous n'avons aucun contrôleur Grails pour s'occuper des vues. Nous avons seulement une GSP index.gsp mappé à l'action '/':

<!doctype html>
<html lang="en">
<!--[if lt IE 7 ]>  
<!--[if IE 7 ]>     
<!--[if IE 8 ]>     
<!--[if IE 9 ]>     
<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="no-js"><!--<![endif]-->
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
		<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
		<title>Google Phone Gallery</title>	 
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<r:require module="angular"/>
		<r:layoutResources />
	</head>
	
	<body>
	  <div class="view-container" ng-app="phonecatApp">
	    <div ng-view class="view-frame"></div>
	  </div>
	  <r:layoutResources />
	</body>
	
</html>

Conclusion

L'intégration du framework Angular pour remplacer les vues et les contrôleurs de Grails s'est fait facilement.
Les convertisseurs de Grails pour sérialiser les objets en JSON ont grandement facilité la tâche pour transmettre les données à Angular. Il aurait également été simple d'ajouter les actions CRUD grâce à l'API REST de Grails.
J'ai donc décidé d'utiliser Angular dans un projet grandeur nature pour voir comment il se comporte dans un scénario plus complexe.

L'intégralité du projet est sur gitHub, n'hésitez pas à le forker et l'améliorer:

https://github.com/BenoitWickramarachi/grails-angular-phonecat/tree/master/grails-app/domain/com/phonecat

comments powered by Disqus