I'm trying to select a front-end framework for new apps - who isn't these days? - and in researching I came across Addy Osmani and company's fantastic bit of work: http://addyosmani.github.com/todomvc/
After looking at this and some other stuff I decided to give Ember a try.
So why not start by hooking up a real REST service to this ToDo app? But first I would need to understand how Ember communicates with the server. A little research came up with the JSON Interface section in this post.
Implementation
I'm working with a Grails server, so providing the REST interface was pretty easy, in theory.
In practice, I decided to use a plugin for the REST interaction, and chose the json-rest-api for its simplicity. However I soon found out that it would not work out of the box - it needed upgrading to Grails2.1, and it did not speak the dialect of REST that Ember prefers.
So I set about modifying the plugin to support Ember-style, and maintain its original style as well. The resulting fork of the json-rest-api plugin is here.
Here are steps I took:
create grails app
- grails create-app todo
- add Todo domain class with fields from TodoMVC todo.js
install json-rest-api
- added grails.plugin.location to the BuildConfig
- I already had this project downloaded locally
- using grails.plugin.location means changes to the plugin are automatically picked up
- changes to Todo domain class:
- add static 'expose' to Todo domain class
- add toJSON() and fromJSON() methods - my enhancement to the json-rest-api plugin to support i18n and custom rendering
// Adding Plugin-in: grails-json-rest grails.plugin.location.jsonrest = '/opt/projects/grails-json-rest-api'
class Todo { String title boolean isCompleted static constraints = { title(blank:false, nullable:false,maxSize:64) isCompleted(default:false) } String toString() { StringBuilder sb = new StringBuilder() sb.append("\n id: ").append(id) sb.append("\n Title: ").append(title) sb.append("\n Completed: ").append(isCompleted) sb.toString() } // --- json-rest-api artifacts --- static expose = 'todo' // Expose as REST API using json-rest-api plugin // this will be the entity name on the URL static api = [ // If allowing json-rest-api to use 'as JSON' to render, you may exclude // unwanted fields here (done with its registered ObjectMarshaller) excludedFields: [ "attached", "errors", "properties" ], // You may override how the list() operation performs its search here list : { params -> Todo.list(params) }, count: { params -> Todo.count() } ] /* // This is the standard way to override JSON marshalling for a class // It uses a ClosureOjectMarshaller[sic] to select fields for marshalling // It is less efficient for the plugin which is based on JSONObject, but this will be // used if you do not define a 'toJSON' method. // NOTE: if using this approach, the json-rest-api marshaller will NOT be used, hence the // api.excludedFields if defined will be ignored // Example taken from http://grails.org/Converters+Reference static { grails.converters.JSON.registerObjectMarshaller(Todo) { // you can filter here the key-value pairs to output: return it.properties.findAll {k,v -> k != 'passwd'} } } */ /** * Rending this object into a JSONObject; allows more flexibility and efficiency in how * the object is eventually included in larger JSON structures before ultimate rendering; * MessageSource offered for i18n conversion before exporting for user audience. * @param messageSource * @return */ JSONObject toJSON(def messageSource) { JSONObject json = new JSONObject() json.put('id', id) json.put('title', title) json.put('isCompleted', isCompleted) return json } /** * Custom bind from JSON; this has efficiency since the grails request.JSON object offers * a JSONObject directly * @param json */ void fromJSON (JSONObject json) { [ "title" ].each(JSONUtil.optStr.curry(json, this)) [ "isCompleted" ].each(JSONUtil.optBoolean.curry(json, this)) } }
install functional testing plugin
- grails install-plugin functional-test
- required to test the json-rest-api plugin (my change)
Added logging into Config.groovy
inside the environment {} block; also added similar to development {}
test { grails.logging.jul.usebridge = false log4j = { appenders { rollingFile name:"todo", maxFileSize:"10000KB", maxBackupIndex:10, file:"logs/todo.log",layout:pattern(conversionPattern: '%d{yyyy-MM-dd HH:mm:ss,SSS z} [%t] %-5p[%c]: %m%n') console name:'stdout', layout: pattern(conversionPattern: '%d{dd-MM-yyyy HH:mm:ss,SSS} %5p %c{1} - %m%n') //console name:'stacktrace' } debug 'grails.app','com.gargoylesoftware.htmlunit.WebClient','org.grails.plugins.rest',additivity = true warn 'grails.app.services.grails.buildtestdata','BuildTestDataGrailsPlugin','grails.buildtestdata', 'org.codehaus.groovy','org.grails.plugin','grails.spring','net.sf.ehcache','grails.plugin', 'org.apache','com.gargoylesoftware.htmlunit','org.codehaus.groovy.grails.orm.hibernate','org.hibernate' root { debug 'stdout', 'todo' additivity = true } } }
create Todo functional test
- using a Generic Mixin test class that I added into the json-rest-api project, resulting functional test class looks like:
@Mixin(GenericRestFunctionalTests) class TodoFunctionalTests extends BrowserTestCase { def log = LogFactory.getLog(getClass()) def messageSource void setUp() { super.setUp() } void tearDown() { super.tearDown() } void testList() { genericTestList(new Todo(title:"title.one")) } void testCreate() { genericTestCreate(new Todo(title:"title.one")) } void testShow() { genericTestShow(new Todo(title:"title.one")) } void testUpdate() { genericTestUpdate(new Todo(title:"title.one"), [title:"title.two"]) } void testDelete() { genericTestDelete(new Todo(title:"title.one")) } }
Wiring up the Ember interface
Time to modify the TodoMVC project to hook it up to my Grails app.
- pulled the TodoMVC source into my Grails project
- modified store.js to configure the REST adapter - default is DS.RESTAdapter, but some changes were required:
- modified the namespace to match my context and path that json-rest-api listens to (/api)
- had to extend the built-in RESTSerializer to stop its crazy conversion of my camel-case field names into underscore versions
// Override the default behaviour of the RESTSerializer to not convert // my camelized field names into underscored versions Todos.TodoRESTAdapter = DS.RESTSerializer.extend({ keyForAttributeName: function(type, name) { return name; //return Ember.String.decamelize(name); // this is the default behaviour }, keyForBelongsTo: function(type, name) { var key = this.keyForAttributeName(type, name); if (this.embeddedType(type, name)) { return key; } return key + "Id"; }, keyForHasMany: function(type, name) { var key = this.keyForAttributeName(type, name); if (this.embeddedType(type, name)) { return key; } return this.singularize(key) + "Ids"; } }); Todos.Store = DS.Store.extend({ revision: 11, adapter: DS.RESTAdapter.create({ bulkCommit: false, namespace: "todo/api", serializer: Todos.TodoRESTAdapter }) });
- found out that Ember sends the entity name to the server in the plural form sometimes, and the json-rest-api plugin does not like this; modified the plugin to account for this. See this other post for the breakdown of Ember's REST dialect.
- Ran instead via grails run-app, and added a logger to the 'development' env in Config.groovy in support of this
- (yes I tried fixing Tomcat by disabling caching/antiLocking in the servlet context)
Result
And voila - after modifying the REST plugin, the Grails app was pretty easy to accomodate to the Ember pulls. The resulting app is located here.
Installing
Download the app
git clone https://github.com/kentbutler/todomvc-grails-emberjs.git
Download the fork of the grails-json-rest-api (at least for now)
git clone https://github.com/kentbutler/grails-json-rest-api.git
Place them alongside each other and test the app by opening a console inside the directory and run:
grails test-app -functional
If tests pass then run the app via
grails run-app
If they do not pass, ensure the path to the json-rest-api inside of grails-app/conf/BuildConfig.groovy accurately locates the grails-json-rest-api plugin.
Thank you.Well it was nice post and very helpful information on AngularJS 5 Online Course
ReplyDelete