Decoupled Drupal has long been an approach touted by some in the front-end contingent of the Drupal community to achieve goals such as a better user experience, a more modern front-end developer experience, and clearer separation of concerns. Though I’ve written previously on the risks and rewards of fully decoupling Drupal and a steadily proliferating approach known as progressive decoupling, many of us have already ascertained through our own cost-benefit analyses that decoupling Drupal entirely is the appropriate course of action for our needs.
Not solely because of its open-source ethos and welcoming community, Ember is a prime candidate to underpin Drupal-backed JavaScript applications due to its reach, its coverage, and its ease of use. In this two-part series intended to help you attain escape velocity quickly, we’ll explore the inner workings of Ember, foundational concepts of Ember's ecosystem, how to decouple Drupal with an Ember front end, and what the future could bring in terms of more Ember within Drupal. By the end, we’ll have assembled a simple Drupal content browser powered by Ember.
What is Ember?
The Ember community describes its flagship framework as an "SDK for the web." Ember is an opinionated JavaScript framework which values convention over configuration — that is, a common set of practices rather than explicit settings. For this reason, Ember has significant advantages over other common JavaScript frameworks due to its large extent of standardization, including a codified directory structure for all applications (simplifying on-boarding) and a clear and interoperable approach to templating.
Ember is a successor of the SproutCore project, which encompassed both an application framework and widget library. In 2011, the SproutCore 2.0 application framework was rechristened Ember to disambiguate it from the SproutCore widget library.
The Ember community’s stated focus is on “ambitious” web applications which approximate a native application’s user experience as closely as possible. Ember is distinguished from other JavaScript MV* frameworks due to its high level of opinionatedness, which makes it less ideal for smaller view-focused applications or implementations requiring customizability across every layer of the framework. From the cultural standpoint, much like Drupal, Ember is not as shepherded or as spearheaded by large corporate giants as are Angular and React.
What is JSON API?
JSON API, an emerging specification for REST APIs in JSON that dubs itself an “anti-bikeshedding tool,” has recently gained traction because of its adoption by developers in the Ember and Ruby on Rails communities and its robust approaches to resource relationships and common query operations such as pagination and sorting. Thanks to the indefatigable Mateu Aguiló Bosch, the JSON API module is nearing inclusion in Drupal 8 core as an experimental module.
For the purposes of this application, we will be using JSON API, because Ember’s data framework comes with a built-in adapter (see second part in this series for more on adapters) which fits JSON API like a glove. While there are other adapters available for other approaches, our prerogative in this tutorial is to ramp up as quickly as possible. For more insight into the role of JSON API in Drupal core, refer to Dries Buytaert’s recent API-first update.
The Ember ecosystem
While Ember can be employed on its own as a client-side framework without any need for extensibility, there exists a larger surrounding ecosystem of ancillary tools of varying utility. Some of these are maintained by the Ember core team with others and are considered part of the typical starter toolkit for Ember. Others are community-built plugins, analogous to Drupal’s contributed modules, which confer additional or extended functionality.
- Ember CLI has a stated mission to bring convention and configuration to Ember’s build tools. Not only can you quickly generate a new Ember application having the default stack through Ember CLI’s blueprints, you also have access to other useful tooling such as ES6 transpilation which is frequently more do-it-yourself in other frameworks. Moreover, Ember CLI provides a local development server with built-in live reload, a complete testing framework, asset management, and dependency management.
- Ember Data is a data persistence library which maps client-side models to server-side data. Though you don’t need Ember Data to use Ember, the majority of Ember applications do utilize it to load and save records and relationships. If your REST API adheres to the JSON API specification, Ember Data is capable of performing data operations without additional configuration.
- Ember Inspector is a browser extension available for Google Chrome and Mozilla Firefox which provides helpful functionality to debug Ember applications. At any point during the application’s bootstrap, you can identify which templates and components are being rendered. If you’re using Ember Data, Ember Inspector also has access to the records loaded for each Ember model (see part two for more about models).
- FastBoot is an add-on to Ember CLI which provides server-side prerendering for Ember applications built on a Node.js stack. This enables JavaScript isomorphism — shared code across client and server — which significantly improves performance on initial page load.
- Liquid Fire is an add-on which provides a declarative approach to building animations and transitions into your Ember application.
Setting up Drupal as a data service
In order to build a Drupal-backed Ember application, which I’m naming “Waterfire” here, we will first want to acquire the most current version of Drupal. The easiest way to accomplish this is to clone the GitHub repository, which grants the added ability to track changes diachronically. The default branch will be 8.3.x
; we will need 8.2.x
at minimum, as we’ll see shortly.
$ mkdir waterfire-drupal && cd waterfire-drupal
$ git clone [email protected]:drupal/drupal.git
$ cd drupal
$ composer install
After we’ve installed dependencies using Composer, as seen in the final command above, we can now import the local site through a local development tool such as Acquia Dev Desktop and install Drupal normally (or via Drush). Keeping things simple, I’ve named my Acquia Dev Desktop site waterfire-drupal.dd
.
To keep the pace up, we’ll be introducing Drupal content entities using the Devel module, whose Devel Generate submodule provides a handy set of commands to perform Drupal actions such as adding dummy nodes (drush genc 20
) and users (drush genu 20
).
$ drush dl devel
$ drush en -y devel
$ drush en -y devel_generate
$ drush genc 20
$ drush genu 20
Configuring Drupal for JSON API
Next, we will need to enable the JSON API module in order to access its features, as well as to configure those REST resources representing content entities to be exposed through JSON API rather than the default HAL normalization.
$ drush dl jsonapi
$ drush en -y jsonapi
In order to expose content entities as JSON API, we’ll need to configure REST resources using the corresponding configuration entities, which is the new way to access REST configuration as of Drupal 8.2.x. For the purposes of this walkthrough, I’ll be using Atom, an open-source code editor built in Electron. First, we’ll open an example YAML file available within the core REST module as a template for configuration imports.
$ atom core/modules/rest/config/optional/rest.resource.entity.node.yml
Add jsonapi
as a dependency under module
, and add api_json
, used by the JSON API module, to formats
. At the end, your configuration import should look like the following. For security purposes, you may not want to allow JSON API consumers to perform requests with all of the HTTP methods.
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
This has only allowed us to configure node resources. Eventually, our Waterfire application will also include users, which are also content entities. As such, we can provide an additional configuration import which looks as follows:
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, navigate to /admin/config/development/configuration/single/import
in your Drupal site to submit both of these configuration imports. Afterwards, if you navigate to /api/node/article?_format=api_json
and /api/node/page?_format=api_json
, you’ll see a complete list of articles and pages, respectively. You’ll discern that the URL naming reflects the pattern [entity_type]/[bundle]
. Because users lack bundles, the entity type name is repeated, and the resources are available at /api/user/user?_format=api_json
.
Finally, another crucial step is to allow applications from other domains to implement HTTP methods against our Drupal site through cross-origin resource sharing (CORS). Opt-in CORS support is available via configuration in Drupal 8.2, but Sally Young’s CORS module is the easiest way to accomplish the same task for the more visually oriented (like yours truly!).
$ drush dl cors
$ drush en -y cors
You can then navigate to the “CORS” page under the “Configuration” section of your Drupal site (/admin/config/services/cors
) and add a list of allowed domains. These are pipe-separated rows — the first element being the paths other domains can access, and the second being the domains access is granted to.
api/*|http://localhost:4200
Eventually, our Ember local development environment will serve our application at http://localhost:4200
. For security purposes, we are restricting the paths that other applications can perform requests against to the namespace /api/*
, employed by the JSON API module. Finally, because we have modified configuration extensively, we rebuild our caches.
$ drush cr
Setting up Ember
To set up Ember quickly and leverage the broader Ember stack, we’ll need to have Ember CLI available. We can use NPM to install Ember CLI globally before creating a new Ember application. The ember new
command will scaffold a directory structure and initialize a new Git repository within a folder having the name you supply as an argument.
$ npm install -g ember-cli
$ ember new waterfire-ember
$ cd waterfire-ember
To check that everything has installed correctly, try booting the Ember server using the command below and navigate to http://localhost:4200
. If you see Tomster with a friendly welcome message and a hard hat perched on his head, you’ve successfully created your first Ember application. We’ll need to use this command frequently to interact with our application.
$ ember server
Finally, let’s generate our first template, which is the root template of the application. This means that all other templates will be nested within this all-encompassing application template, which is the entry point to our application and also represents the index or home route. We can then open the application template using Atom to provide some output which replaces Tomster.
$ ember generate template application
$ atom app/templates/application.hbs
Ember templates
Ember uses the Handlebars templating language for its templates, which will look somewhat familiar to users of Twig in Drupal, though the two are substantially different. These templates are responsible for displaying properties made available to the template’s context, which can either be a component or a route. The characteristic double curly braces can also include other helpers and components (we will work with the latter in due course).
In our application template, we can insert some initial markup which represents the header of our new Ember application. {{outlet}}
represents templates which are nested within the current template. In other words, any templates deeper within our application will “shine through the window” and be visible through the outlet. {{! some text }}
represents a Handlebars comment.
{{! app/templates/application.hbs }}
Waterfire
{{outlet}}
Ember routes
In Ember, application state is represented by a URL, which is tied to a route object controlling what the user sees. Ember routes encompass templates as well as route handlers, which render templates and load models made available to the template for use.
Our Waterfire content browser will allow us to browse nodes of type article and basic page as well as users, so we will want to create routes for each. Let’s start off with articles. As an aside, many commands in Ember CLI have shorthands; g
here is short for generate
.
$ ember g route articles
$ atom app/templates/articles.hbs
In the template, insert the following.
{{! app/templates/articles.hbs }}
List of articles
When you navigate to http://localhost:4200/articles
, you will now see the “Waterfire” first-level heading above a “List of articles” second-level heading. Now that the template is functional, we’ll need to provide some data in the route handler.
$ atom app/routes/articles.js
As you’ll see in the code editor, Ember CLI’s generation tool has used available blueprints as a means to generate some initial code for us to proceed quickly. Below, we’re providing some dummy data as an array into the model hook (see second part for more on model hooks).
{{! app/routes/articles.js }}
import Ember from 'ember';
export default Ember.Route.extend({
model () {
return ['Article #1', 'Article #2', 'Article #3'];
}
});
If you have worked with JavaScript in the past but not the increasingly well-supported ES6, you may be flummoxed by some syntactic features in the foregoing example. First, the example uses ES6 modules, which grants distinct JavaScript files access to methods and functions defined within others. Second, it makes use of concise method names in place of traditional method definitions in JavaScript which would require an anonymous function as the value of an object property (model: function () { }
in lieu of model ()
).
Now that we have supplied a dummy model consisting of three filler articles within our articles route handler, we can now iterate over this array within our articles route template.
{{! app/templates/articles.hbs }}
List of articles
{{#each model as |article|}}
{{article}}
{{/each}}
The resulting HTML at
http://localhost:4200/articles
will consist of our familiar headings and, beneath, an unordered list of our dummy articles.A simple Ember component
Ember components are reusable and nestable. They are typically comprised of a Handlebars template, which describes a component’s presentation, and a JavaScript file, which articulates its behavior. In our current state, while we have written a template for articles, it would be quite tedious and less maintainable to use the same template repetitively for pages and users too.
That’s where a component comes in. Let’s generalize our articles template so it can be used to harness all content entities. If you are familiar with the Custom Elements specification proposed by the W3C, some aspects of Ember’s approach to components will seem old-hat.
$ ember g component entity-list
Within our template, copy the contents of the articles template and provide some more generic code, starting with the title, which will need to change based on how the component is used.
{{! app/templates/components/entity-list.hbs }}
{{title}}
{{#each entities as |entity|}}
{{entity}}
{{/each}}
Then, within the articles template, replace everything with a reference to our new “entity-list” component. Just like the Custom Elements specification dictates, in Ember, all component names must be hyphenated for the sake of forward compatibility.
{{! app/templates/articles.hbs }} {{entity-list title="List of articles" entities=model}}
The other routes can then use this component. We can generate new routes and edit the templates and route handlers accordingly. First, generate the “pages” and “users” routes:
$ ember g route pages $ ember g route users
Insert Handlebars code into both templates which reflects the use of the 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}}
Finally, update the route handlers with dummy data.
// app/routes/pages.js import Ember from 'ember'; export default Ember.Route.extend({ model() { return ['Page #1', 'Page #2', 'Page #3']; } });
// app/routes/users.js import Ember from 'ember'; export default Ember.Route.extend({ model() { return ['User #1', 'User #2', 'User #3']; } });
Now that we have these routes available, we can flesh out our root application template by providing supplemental navigation to improve the usability of our application.
{{! app/templates/application.hbs }}
{{#link-to 'index'}}Waterfire{{/link-to}}
{{#link-to 'articles'}}Articles{{/link-to}}
{{#link-to 'pages'}}Pages{{/link-to}}
{{#link-to 'users'}}Users{{/link-to}}
{{outlet}}
Conclusion
In the first part of this tutorial series, we’ve delved into Ember, JSON API, and the Ember ecosystem. We’ve also spelunked into the depths of the Ember software stack, erected a Drupal site as a data service, and circumnavigated some of the key elements which constitute a standard Ember application. In setting up Ember and generating routes, we’ve gleaned that Ember routes typically consist of a route template and route handler.
In the second part of this tutorial series, we’ll explore Ember models and the model hook, connect our Ember application to Drupal, fetch data from Drupal’s JSON API implementation, and access the properties available within JSON API resources. Finally, we’ll zoom out to 30,000 feet and examine more critically some questions about Drupal’s front-end outlook and what role Ember could play therein.
This is a two-part series adapted from the DrupalCon Dublin session “Decoupled Drupal and Ember”. The second part is available here.