prestonso • @prestonso • preston.so
DrupalCon Dublin • September 28, 2016
Preston So has been a web developer and designer since 2001, a creative professional since 2004, and a Drupal developer since 2007. As Development Manager of Acquia Labs at Acquia, Preston leads new open-source and product initiatives. Previously, he was Technical Lead of Entertainment Weekly at Time Inc., co-founder of the Southern Colorado Drupal User Group (est. 2008), and sole proprietor of an award-winning freelance design studio. Preston is a sought-after speaker who has presented at conferences on four continents and in two languages about diverse topics such as decoupled Drupal, responsive design, front-end development, and user experience. Most recently, Preston delivered the keynote at DrupalCamp Connecticut 2016 and, in Portuguese, the keynote at DrupalCamp Campinas 2016 in Brazil.
Much of Drupal's robustness is lost.
In Ember, application state is represented by a URL, which has its own respective route object controlling what the user sees.
There are several ways in which the URL can be set in Ember:
Route handlers render templates and load a model which is made available to the template.
Let's get the most up-to-date version of core, 8.2.x.
$ mkdir dublin-drupal
$ cd dublin-drupal
$ git clone git@github.com:drupal/drupal.git
$ cd drupal
$ composer install
dublin-drupal.dd
To speed things up, let's generate some content entities.
$ drush dl devel
$ drush en -y devel
$ drush en -y devel_generate
$ drush genc 20
$ drush gent tags 20
$ drush genu 20
Now let's install JSON API and enable it. JSON API adds a new format, api_json
.
$ drush dl jsonapi
$ drush en -y jsonapi
New in 8.2.x is the notion of configuring REST through configuration entities.
$ atom core/modules/rest/config/optional/rest.resource.entity.node.yml
You can copy the sample rest.resource.entity.node.yml
and import directly at /admin/config/development/configuration/single/import
.
langcode: en
status: true
dependencies:
module:
- basic_auth
- hal
- jsonapi
- node
id: entity.node
plugin_id: 'entity:node'
granularity: resource
configuration:
methods:
- GET
- POST
- PATCH
- DELETE
formats:
- hal_json
- api_json
authentication:
- basic_auth
Let's do the same with users by adding the api_json
format.
langcode: en
status: true
dependencies:
module:
- basic_auth
- hal
- jsonapi
- user
id: entity.user
plugin_id: 'entity:user'
granularity: resource
configuration:
methods:
- GET
- POST
- PATCH
- DELETE
formats:
- hal_json
- api_json
authentication:
- basic_auth
Now, you can navigate to the following paths and see the result.
/api/node/article?_format=api_json
/api/node/page?_format=api_json
/api/user/user?_format=api_json
Last but not least, don't forget to set up cross-origin resource sharing (CORS). The easiest way to do this is through the CORS module.
$ drush dl cors
$ drush en -y cors
For now, let's allow all requests from the origin of our soon-to-be Ember application, since we do not have security concerns.
*|http://localhost:4200
Don't forget to clear cache after all of this work!
$ drush cr
Ember makes it a cinch to get going quickly, because it is opinionated about application structure. Once you boot the server and navigate to the application, you'll see a friendly Tomster.
$ npm install -g ember-cli
$ ember new dublin-ember
$ cd dublin-ember
$ ember server
$ ember generate template application
$ atom app/templates/application.hbs
Let's start by writing the entry point into our application, in app/templates/application.hbs
.
{{! app/templates/application.hbs}}
<h1>Dublin Ember</h1>
{{outlet}}
Our ultimate (and ambitious) goal is to build an app that performs CRUD operations on content entities. Let's start simply with a route to view articles.
$ ember generate route articles
$ atom app/templates/articles.hbs
Note that you can also use the shorthand available in Ember:
$ ember g route articles
Let's add some filler text for now.
{{! app/templates/articles.hbs}}
<h2>List of articles</h2>
Now let's open the corresponding route and add a placeholder model for now:
atom app/routes/articles.hbs
// app/routes/articles.js
import Ember from 'ember';
export default Ember.Route.extend({
model () {
return ['Article #1', 'Article #2', 'Article #3'];
}
});
Within the template, we can now use Handlebars to iterate through the available data.
{{! app/templates/articles.hbs}}
<h2>List of articles</h2>
<ul>
{{#each model as |article|}}
<li>{{article}}</li>
{{/each}}
</ul>
We may wish to reuse this component for other content entity types, and reusable components can help us with this.
$ ember generate component entity-list
Let's generalize our old articles template for all content entities.
{{! app/templates/components/entity-list.hbs}}
<h2>{{title}}</h2>
<ul>
{{#each entities as |entity|}}
<li>{{entity}}</li>
{{/each}}
</ul>
This means that within our articles template, we can invoke the component by replacing the contents with the following.
{{! app/templates/articles.hbs}}
{{entity-list title="List of articles" entities=model}}
Let's flesh out our application a bit more by expanding the home page a bit with links to "pages" and "users".
{{! app/templates/application.hbs}}
<h1>{{#link-to 'index' class="button"}}Dublin Ember{{/link-to}}</h1>
<ul>
<li>{{#link-to 'articles' class="button"}}Articles{{/link-to}}</li>
<li>{{#link-to 'pages' class="button"}}Pages{{/link-to}}</li>
<li>{{#link-to 'users' class="button"}}Users{{/link-to}}</li>
</ul>
{{outlet}}
Now we need to generate the corresponding new routes.
$ ember g route pages
$ ember g route users
And in each of the templates we can now use our entity-list
component.
{{! app/templates/pages.hbs}}
{{entity-list title="List of pages" entities=model}}
{{! app/templates/users.hbs}}
{{entity-list title="List of users" entities=model}}
And let's provide each of these routes with placeholder models. Here's the one for pages ...
// app/routes/pages.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return ['Page #1', 'Page #2', 'Page #3'];
}
});
... and for users.
// app/routes/users.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return ['User #1', 'User #2', 'User #3'];
}
});
In Ember, models allow you to define how your data should be stored on the client side. Recall that the JSON API type in Drupal for articles is node--article
.
$ ember g model node--article
Let's define a few attributes that we know that we'll want to use from articles.
// app/models/node--article.js
import DS from 'ember-data';
export default DS.Model.extend({
nid: DS.attr(),
uuid: DS.attr(),
title: DS.attr(),
created: DS.attr(),
body: DS.attr()
});
Now let's do the same for pages ...
$ ember g model node--page
// app/models/node--page.js
import DS from 'ember-data';
export default DS.Model.extend({
nid: DS.attr(),
uuid: DS.attr(),
title: DS.attr(),
created: DS.attr(),
body: DS.attr()
});
... and users.
$ ember g model user--user
// app/models/user--user.js
import DS from 'ember-data';
export default DS.Model.extend({
uid: DS.attr(),
uuid: DS.attr(),
name: DS.attr(),
mail: DS.attr()
});
There is only one piece missing: the connection to Drupal. Ember uses adapters to communicate with back ends via XHR. Let's create an adapter for our application.
$ ember g adapter application
By default, Ember will generate a JSONAPIAdapter
for use with JSON API back ends. We can customize the adapter according to our back end.
// app/adapters/application.js
import DS from 'ember-data';
export default DS.JSONAPIAdapter.extend({
host: 'http://dublin-drupal.dd:8083',
namespace: 'api'
});
Now let's use our Drupal data store by updating the model hook in each of our route handlers.
// app/routes/articles.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.get('store').findAll('node--article');
}
});
Let's do the same for pages ...
// app/routes/pages.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.get('store').findAll('node--page');
}
});
... and users. That's it, right!?
// app/routes/users.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.get('store').findAll('user--user');
}
});
Not so fast. As you can see, our app isn't working. If you take a look at the Drupal error log, you can see that 404s are happening because the paths are wrong. This means we have to customize our adapter further.
// app/adapters/application.js
import DS from 'ember-data';
export default DS.JSONAPIAdapter.extend({
host: 'http://dublin-drupal.dd:8083',
namespace: 'api',
pathForType: function (type) {
let entityType;
switch (type) {
case 'user--user':
entityType = 'user/user';
break;
case 'node--page':
entityType = 'node/page';
break;
default:
entityType = 'node/article';
}
return entityType + '?_format=api_json';
}
});
Here we are now! Now we can do some interesting things with the templates by referring to attributes.
{{! app/templates/articles.hbs}}
<h2>List of articles</h2>
{{#each model as |article|}}
<h3>{{article.title}}</h3>
<ul>
<li>NID: {{article.nid}}</li>
<li>UUID: {{article.uuid}}</li>
<li>Created: {{article.created}}</li>
</ul>
{{article.body.value}}
{{/each}}
Let's do the same for pages.
{{! app/templates/pages.hbs}}
<h2>List of pages</h2>
{{#each model as |page|}}
<h3>{{page.title}}</h3>
<ul>
<li>NID: {{page.nid}}</li>
<li>UUID: {{page.uuid}}</li>
<li>Created: {{page.created}}</li>
</ul>
{{page.body.value}}
{{/each}}
We want to expose slightly different attributes for users.
{{! app/templates/users.hbs}}
<h2>List of users</h2>
{{#each model as |user|}}
<h3>{{user.name}}</h3>
<ul>
<li>UID: {{user.uid}}</li>
<li>UUID: {{user.uuid}}</li>
<li>E-mail: {{user.mail}}</li>
</ul>
{{/each}}
Success! We've created our own content entity browser. But what about CRUD operations? For time's sake, we'll stick with just creation for now.
Let's create an additional nested route to handle article creation.
$ ember g route articles/new
Let's make a few changes to our "list of articles" template to be more generic.
{{! app/templates/articles.hbs}}
<h2>Articles</h2>
<ul>
<li>{{#link-to 'articles'}}List of articles{{/link-to}}</li>
<li>{{#link-to 'articles.new'}}New article{{/link-to}}</li>
</ul>
{{outlet}}
{{#each model as |article|}}
<h3>{{article.title}}</h3>
<ul>
<li>NID: {{article.nid}}</li>
<li>UUID: {{article.uuid}}</li>
<li>Created: {{article.created}}</li>
</ul>
{{article.body.value}}
{{/each}}
In the nested route template, let's add a creation form.
{{! app/templates/articles/new.hbs}}
<h3>Create a new article</h3>
<form {{action 'save' model on="submit"}}>
{{input value=model.title placeholder="Enter title" name="name"}}<br />
{{input value=model.body.value placeholder="Enter body" name="body"}}<br />
<button>Create article</button>
</form>
And now the handling of the create action.
// app/routes/articles/new.js
import Ember from 'ember';
export default Ember.Route.extend({
model(params) {
return this.store.createRecord('node--article', params);
},
actions: {
save(record) {
console.log(record);
record.save().then(() => this.transitionTo('articles'));
},
willTransition() {
this._super(...arguments);
const record = this.controller.get('model');
record.rollbackAttributes();
}
}
});