Decoupled Drupal and Ember

Preston So

prestonso@prestonsopreston.so

DrupalCon Dublin • September 28, 2016

Céad míle fáilte!

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.

What we'll cover

  1. What is Ember?
  2. Ember architecture
  3. Setting up Drupal
  4. Writing an Ember application
  5. Epilogue: Ember + Drupal = Drupal?

What is decoupled Drupal?

  • Simply put, decoupled Drupal is the use of Drupal as a data provider by means of a RESTful API. There are two types of architectures in the wild which use this architecture.
  • In fully decoupled Drupal, Drupal serves solely as a JSON API which serves data payloads to other applications. A client-side framework (often shared isomorphically client-server) controls all rendering.
  • In progressively decoupled Drupal, Drupal controls some of the render to provide markup within a single application. JavaScript then takes over client-side rendering.

Problems of decoupled Drupal

Much of Drupal's robustness is lost.

  • Cross-origin requests, security, authentication, and passwords
  • Form validation
  • Content workflow and management
  • Layout and display management
  • Multilingual and localization
  • Accessibility and user experience

Decoupled Drupal 8

  • WSCCI (Web Services and Context Core Initiative) incorporated Web Services into Drupal 8 core.
  • The core REST modules allow for all content entities to be exposed as JSON+HAL, and Views natively supports “REST export” as a new display type.
  • There are many issues with REST in core; please consider contributing to RX (REST experience) tagged issues.

Decoupled Drupal 8

  • RELAXed Web Services (contrib) extends the core REST API to include revisions and file attachments, as well as cross-environment UUIDs.
  • RELAXed's mission aligns with movement in content staging and offline-first applications, and it uses the CouchDB API specification.

JSON API

  • An emerging solution, JSON API, has been promulgated in the Ember and Rails community.
  • JSON API is an "anti-bikeshedding" tool.

What is Ember?

  • The beginnings of Ember
  • Ember design principles
  • Differences with other frameworks
  • Drawbacks of Ember

The beginnings of Ember

  • Ember is the successor of the SproutCore project, which encompassed both an application framework and widget library.
  • The SproutCore framework is the creation of Yehuda Katz, part of the jQuery, Ruby on Rails, and SproutCore core teams.
  • In 2011, the SproutCore 2.0 framework was rechristened Ember.js to reduce confusion between the SproutCore widget library.

Ember design principles

  • Focus on ambitious web applications
  • More productive out of the box
  • Stability without stagnation
  • Future web standards foresight
  • Convention over configuration

Differences with other frameworks

  • Ember has a large footprint and is extremely opinionated, which makes it less ideal for small view-focused applications.
  • The Ember framework is not managed by corporate giants, unlike Angular and React.
  • Ember focuses on "ambitious" web applications which come as close to the native experience as possible.

Drawbacks of Ember

  • Opinionated (convention over configuration)
  • Large file size
  • Backwards compatibility

Ember architecture

  • Ember routes
  • Ember models
  • Ember templates
  • Ember components
  • The Ember software stack

Ember routes

In Ember, application state is represented by a URL, which has its own respective route object controlling what the user sees.

Ember routes

There are several ways in which the URL can be set in Ember:

  • The user boots the app for the very first time.
  • The user modifies the URL either through clicking back or forward or by editing the URL bar.
  • The user clicks on a link in the application.
  • Some other event in the application modifies the URL.

Ember route handlers

Route handlers render templates and load a model which is made available to the template.

Ember templates

  • Templates use Handlebars syntax.
  • Templates are able to display properties that are available in the template's context.
  • A template's context is either a component or a route.
  • Double curly braces can contain helpers and components as well.

Ember models

  • Models represent persistent state.
  • Models typically persist data to a web server, but they can save to anywhere else.
  • When data changes, or new data is added, the model is saved.

Ember components

  • Whereas templates define an interface's appearance, components are responsible for the interface's behavior.
  • Components are made up of a template in Handlebars and a JavaScript file containing the component's behavior.

The Ember software stack

  • Ember itself is only a framework, but there is a larger surrounding software stack which can be used.
  • The Ember core team is responsible for building Ember and these surrounding tools.

The Ember software stack

  • Ember CLI
  • Ember Data
  • Ember Inspector
  • Ember FastBoot
  • Liquid Fire

Ember CLI

  • Ember CLI's mission is to bring convention and configuration to build tools.
  • You can quickly generate a new Ember app with the default stack providing many features.

Ember CLI

  • Standard file and directory structure
  • Local development server with live reload
  • A complete testing framework
  • Dependency management through Bower and NPM
  • ES6 modules and ES6/ES7 syntax support with Babel
  • Asset management
  • Blueprints (code generators) for models, controllers, and components
  • Add-ons

Ember Data

  • The majority of Ember applications use Ember Data, which is a data persistence library.
  • Ember Data has many of the features of object-relational mapping (ORM), but you don't need it to use Ember.
  • Ember Data maps client-side models to server-side data.
  • Ember Data can load and save records and relationships without additional configuration, assuming the REST API is using the JSON API specification.

Ember Inspector

  • A Google Chrome or Mozilla Firefox extension, Ember Inspector aims to help debug Ember applications.
  • At any point in the application, you can identify which templates, components, and views are rendered.
  • With Ember Data, Ember Inspector also has access to the records that were loaded for each Ember model.

Other elements of the stack

  • FastBoot is an add-on to Ember CLI which provides server-side rendering for Ember applications (isomorphism).
  • Liquid Fire is an add-on which provides a declarative approach for building animations and transitions.

Setting up Drupal

  • Drupal setup and dependencies
  • JSON API setup
  • Configuring REST with config entities
  • CORS setup

Drupal setup and dependencies

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
						

Drupal setup and dependencies

  • You can set up a Drupal site locally using Acquia Dev Desktop.
  • Give it a name such as dublin-drupal.dd
  • Then, install Drupal normally.

Drupal setup and dependencies

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
            

JSON API setup

Now let's install JSON API and enable it. JSON API adds a new format, api_json.

							$ drush dl jsonapi
$ drush en -y jsonapi
            

Configuring REST with config entities

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
						

Configuring REST with config entities

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
						

Configuring REST with config entities

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
						

Configuring REST with config entities

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

CORS setup

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
						

CORS setup

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
						

CORS setup

Don't forget to clear cache after all of this work!

							$ drush cr
						

Writing an Ember application

  • Ember setup
  • Writing components and routes
  • Models
  • The JSON API adapter
  • Templates
  • Extending our application

Ember setup

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
						

Ember setup

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}}
						

Writing components and routes

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
						

Writing components and routes

Let's add some filler text for now.

							{{! app/templates/articles.hbs}}
<h2>List of articles</h2>
						

Writing components and routes

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'];
  }
});
						

Writing components and routes

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>
						

Writing components and routes

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
						

Writing components and routes

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>
						

Writing components and routes

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}}
						

Writing components and routes

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}}
						

Writing components and routes

Now we need to generate the corresponding new routes.

							$ ember g route pages
$ ember g route users
						

Writing components and routes

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}}
						

Writing components and routes

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'];
  }
});
						

Writing components and routes

... and for users.

							// app/routes/users.js
import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    return ['User #1', 'User #2', 'User #3'];
  }
});
						

Models

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
						

Models

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()
});
						

Models

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()
});
					

Models

... 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()
});
						

The JSON API adapter

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
						

The JSON API adapter

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

The JSON API adapter

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

The JSON API adapter

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

The JSON API adapter

... 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');
  }
});
						

The JSON API adapter

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

Templates

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}}
						

Templates

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}}
						

Templates

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}}
						

Extending our application

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.

Extending our application

Let's create an additional nested route to handle article creation.

							$ ember g route articles/new
						

Extending our application

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}}
						

Extending our application

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>
					

Extending our application

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();
    }
  }
});
						

Epilogue: Ember + Drupal = Drupal?

  • Inversion of control
  • My (own) perspective

Inversion of control

My (own) perspective

Go raibh maith agaibh!

prestonso@prestonsopreston.so