Rewriting Apps in Ember Octane
Tags: ember, javascript, beginners, showdev
Published: 2019-12-24 10:12:41

*Originally published on [crunchingnumbers.live](https://crunchingnumbers.live/2019/12/23/rewriting-apps-in-ember-octane/)*

Last Friday, Ember 3.15 was [dubbed the Octane edition](https://blog.emberjs.com/2019/12/20/octane-is-here.html). To see how easy (and fun) writing an Octane app is, I spent the weekend rewriting my apps [Ember Animated (v3.8)](https://ember-animated-octane.herokuapp.com/) and [Lights Out (v2.18)](https://lights-out-octane.herokuapp.com/). Let me share what I learned.

If you have tutorials and demo apps, I encourage you to rewrite them in Octane. You can publish both versions to help everyone understand how the programming model in Ember has evolved over time.


---


# 1. Ember Animated (3.8 → 3.15)

Between the two, the Ember Animated represents a production app. It features a few complex routes, several components, mocked APIs, and a comprehensive test suite. The problems and joys that I encountered while rewriting, you will likely also.

I didn't use ember-cli-update and codemods because I had somewhat atypical code from trying out Ember Data Storefront and Ember Animated. I figured, by writing a new app from scratch, I would learn Octane faster.

- App: [3.15](https://ember-animated-octane.herokuapp.com/), [3.8](https://ember-animated.herokuapp.com/)
- Repo: [3.15](https://github.com/ijlee2/ember-animated-tutorial-octane), [3.8](https://github.com/ijlee2/ember-animated-tutorial)
- Blog post: [Animation and Predictable Data Loading in Ember](https://crunchingnumbers.live/2019/04/02/animation-and-predictable-data-loading-in-ember/)


## a. Baby Steps

I found an incremental approach to be helpful. **Introduce routes one at a time** and see what components need to be migrated over. Once a component is in Octane, write or port over rendering tests. After all components are done, write application tests for the route. Move to the next route and the next set of components. Rinse and repeat.

Often, you will find yourself recasting `{{action}}` modifiers as a mix of `@action` decorator, `{{on}}` modifier, and `{{fn}}` helper.

```handlebars
File: /app/templates/authors/details.hbs

<!-- Before -->
<button type="button" {{action "deleteAuthor" model}}>
    Delete
</button>

<!-- After -->
<button type="button" {{on "click" (fn this.deleteAuthor @model)}}>
    Delete
</button>
```

Though verbose, the new syntax helps you be clear with your *intent*. With practice, the syntax will become second nature. [Visit the Ember Guides to learn more](https://guides.emberjs.com/release/upgrading/current-edition/action-on-and-fn/).

On a related note, take caution when converting actions called on a form submission. (I omitted Ember Concurrency in the following example to make the point clear.)

```handlebars
File: /app/templates/search.hbs

<!-- Before -->
<form {{action "searchStudents" on="submit"}}>
    ...
</form>

<!-- After -->
<form {{on "submit" this.searchStudents}}>
    ...
</form>
```

The `{{action}}` modifier calls `event.preventDefault();` and prevents page reload for you. In Octane, *you* express the intent to prevent the default behavior. You can find the `event` object as the last argument to your function.

```javascript
File: /app/controllers/search.js

// Before
actions: {
    searchStudents() {
        const skillIds = this.selectedSkills.mapBy('id').join(',');

        ...
    }
}

// After
@action searchStudents(event) {
    event.preventDefault();

    const skillIds = this.selectedSkills.mapBy('id').join(',');

    ...
}
```


## b. Test Suites

You can **be confident with a rewrite** if you have existing tests. Because my 3.8 tests already followed the new testing paradigm, my 3.15 tests needed a minor update: Replace `server`​ with `this.server` for Ember CLI Mirage. Note that, in component, helper, and modifier tests, `hbs` is [now a named import](https://github.com/ember-cli/ember-cli-htmlbars#tagged-template-usage--migrating-from-htmlbars-inline-precompile).

```javascript
File: /tests/integration/components/loading/component-test.js

// Before
import hbs from 'htmlbars-inline-precompile';

// After
import { hbs } from 'ember-cli-htmlbars';
```

If you don't have existing tests, I encourage you to take time to write them for your future self. [Learn more about testing in Ember](https://crunchingnumbers.live/2019/10/11/write-tests-like-a-mathematician-part-3/).


## c. Where Do Foos Come from?

As soon as you rewrite routes and components, you will **love how you explicitly call things in a template**. No more confusion over if `{{foo}}` is a component, a helper, a passed argument, or a local property. (You now write `<Foo>`, `{{foo}}`, `@foo`​, and `this.foo`, respectively. Ember will throw a helpful error for forgotten mistakes.)

```handlebars
File: /app/templates/students.hbs

<div>
    {{#if this.showHeader}}
        <h1>{{t "layout.students.header.students"}}</h1>
    {{/if}}

    <StudentsGrid
        @students={{@model}}
    />
</div>
```

Although you are seeing this code for the first time, you can tell that `<StudentsGrid>` is a component, `{{t}}` is a (translation) helper, `@model` is a passed argument, and `this.showHeader` is a local property. You know which file to look next to learn more.


## d. Template-Only Components

Glimmer components don't create a "wrapping-div," so you can say goodbye to `tagName`, `attributeBindings`, `classNames`, and `classNameBindings`. More often than not, these hindered me from reasoning the HTML code fast. After you remove these properties, **smile when you see how many components don't need a backing class**. 6 out of my 10 components became template-only.

One caveat with Glimmer components: `elementId`, which was useful for binding a label to an input for accessibility, no longer exists. Instead, use `guidFor(this)` in the backing class to create the ID.

```javascript
File: /app/components/skill-pill/component.js

import { guidFor } from '@ember/object/internals';
import Component from '@glimmer/component';

export default class SkillPillComponent extends Component {
    inputId = `input-${guidFor(this)}`;
}
```


## e. Modifiers

When I dove into the rewrite, I wasn't sure about converting the modifier that I had used to demonstrate Web Animations API. I had used the ember-oo-modifiers addon, but I now wanted to use the official [ember-modifier](https://github.com/ember-modifier/ember-modifier).

To my pleasant surprise, the code remained virtually the same. The new modifier even seemed to have fixed the animation bug that I had seen before. You got to **give cheers to Ember contributors** for following a well-defined API.

```javascript
File: /app/modifiers/fade-up.js

import Modifier from 'ember-modifier';

export default class FadeUpModifier extends Modifier {
    didReceiveArguments() {
        const { duration, delay } = this.args.named;

        this.element.animate(
            [
                { opacity: 0, transform: 'translateY(60px)' },
                { opacity: 1, transform: 'translateY(0px)' }
            ],
            {
                duration: duration || 2000,
                delay: delay || 0,
                easing: 'cubic-bezier(0.075, 0.82, 0.165, 1)',
                fill: 'backwards'
            }
        );
    }
}
```


## f. Avoid Shortcuts

In 3.8, I had created the search results-route as a child of the search. After all, a user would first search, then see the results. Nesting had seemed to convey that user flow accurately.

```javascript
File: /app/router.js

Router.map(function() {
    this.route('search', function() {
        this.route('results');
    });
});
```

Nesting normally implies that, when the user is on the results page, they will also see the search page. In reality, the app only shows the results page (which is what I had wanted) because I had used `renderTemplate` to bypass the parent.

I don't recommend this practice since `renderTemplate` is on [the path to deprecation](https://github.com/emberjs/rfcs/pull/418). The alternate solution is just as easy and doesn't load records that are never used in search results:

```javascript
File: /app/router.js

Router.map(function() {
    this.route('search');
    this.route('search-results', { path: '/search/results' });
});
```

While you rewrite, I encourage you to **return to the happy path that is paved with good practices**. Your future updates will be easier. You can also review [deprecations](https://deprecations.emberjs.com/) regularly and exchange ideas for solutions with people on [Discord](https://discord.gg/emberjs).


# 2. Lights Out (2.18 → 3.15)

Between the two, I found rewriting this app to be more interesting and rewarding. It is a simple app in terms of components: There is only 1 component. However, because I had written it while I was still new to Ember and D3, the app was riddled with hard-to-reason control flow. Oh, mixins, CPs, and observers...

By rewriting the app from scratch, I got to understand how to design a D3 component, perhaps with composability in mind.

- App: [3.15](https://lights-out-octane.herokuapp.com/), [2.18](https://d3-lights-out.herokuapp.com/)
- Repo: [3.15](https://github.com/ijlee2/lights-out-octane), [2.18](https://github.com/ijlee2/lights-out)
- Blog post: [Lights Out](https://crunchingnumbers.live/2018/04/07/lights-out/)


## a. Tracked Properties + Getters 💞

[Tracked properties are magic](https://www.pzuraq.com/coming-soon-in-ember-octane-part-3-tracked-properties/). You **no longer worry about whether a component should update** along with when, where, why, and how. It just works.™ The code is cleaner, too, because you don't specify the dependency list.

The following snippet shows how to define a D3 scale. Should the object `numButtons` or `boardSize` change, the scale will be recomputed and anything that depends on the scale also.

```javascript
File: /app/components/lights-out/component.js

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { scaleLinear } from 'd3-scale';

export default class LightsOutComponent extends Component {
    @tracked numButtons = { x: 5, y: 5 };

    get boardSize() { ... }

    get scaleX() {
        return scaleLinear()
            .domain([0, this.numButtons.x])
            .range([0, this.boardSize.x]);
    }

    get scaleY() {
        return scaleLinear()
            .domain([0, this.numButtons.y])
            .range([0, this.boardSize.y]);
    }
}
```

A small print: Updating a complex data structure may require extra work. (It always did, to be fair.) To update `buttons`, a double array of objects, I made a deep copy and used `set`:

```javascript
File: /app/components/lights-out/component.js

import { set } from '@ember/object';
import { copy } from 'ember-copy';

export default class LightsOutComponent extends Component {
    @tracked buttons;

    toggleLights(i, j) {
        let buttons = copy(this.buttons, true);

        // Center
        this.toggleLight(buttons[i][j]);

        // Top, Bottom, Left, Right
        ...

        this.buttons = buttons;
    }

    toggleLight(button) {
        set(button, 'isLightOn', !button.isLightOn);
    }
}
```


## b. Modifiers to the Rescue 💯

During the rewrite, I was worried that I would mess up the control flow again. In 2.18, I had introduced a mixin and turned a blind eye to Ember's then-13 lifecycle hooks. I had also relied on computed properties and observers to force the flow my way.

Since Glimmer components have [2 lifecycle hooks by default](https://www.pzuraq.com/coming-soon-in-ember-octane-part-5-glimmer-components/), I had much less to work with. The D3 component also needed to react to a window resize. I wasn't sure where I would now create and destroy the event listeners.

These problems went away as soon as I discovered more modifiers. [ember-render-modifiers](https://github.com/emberjs/ember-render-modifiers) provides the `{{did-insert}}` modifier, and [ember-did-resize-modifier](https://github.com/gmurphey/ember-did-resize-modifier) the `{{did-resize}}` modifier.

Thanks to these two, I was able to **write a declarative, observer-free code**:

```handlebars
File: /app/components/lights-out/template.hbs

<div class="lights-out"
    {{did-insert this.setContainerSize}}
    {{did-insert this.drawGame}}
    {{did-insert this.startGame}}
    {{did-resize this.setContainerSize debounce=25}}
    {{did-resize this.drawGame debounce=25}}
>
    ...
</div>
```

I can look at this template and be confident that, when the div element is added to the DOM, Ember will set the game container size, draw the game, then start it. (The order of appearance is respected.) Similarly, when the div element changes size, Ember will set the game container size and draw the game again.

It's interesting that, by having less, I could do more.


# 3. Conclusion

Octane, the first edition of Ember, has really shifted for the better how developers will approach writing, architecting, and testing their apps. I saw a glimpse when I rewrote two apps over a weekend. I'm looking forward to learn more about Octane at work, [Meetups](https://emberjs.com/community/meetups), and [conferences](https://emberconf.com/) in 2020!

Since the beginning, a core value within Ember has been *accelerating* (boosting, etc.—add as many octane-related puns as possible) your journey to productivity. Thanks to seemingly small things like named arguments and improved [Ember Inspector](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi), as well as big, ambitious things like modifiers and tracked properties, writing apps in Ember is easy and fun.

Again, if you have written tutorials and demo apps before, I encourage you to rewrite them in Octane and share what you learned. If you have never tried out Ember before, you can start out with the official, revamped [Super Rentals tutorial](https://guides.emberjs.com/release/tutorial/part-1/). Feel free to ask for help on [Discord](https://discord.gg/emberjs) at any time!


# Resources

If you want to learn more about Octane, I encourage you to visit these links:

- [Octane Is Here](https://blog.emberjs.com/2019/12/20/octane-is-here.html)
- [Ember.js Octane vs Classic Cheat Sheet](https://ember-learn.github.io/ember-octane-vs-classic-cheat-sheet/)
- [Bringing Clarity to Templates Through Ember Octane](https://simplabs.com/blog/2019/12/20/clarity-in-templates/)
- [The Most Common Ember.js Octane Mistakes and How to Avoid Them](https://medium.com/ember-ish/the-most-common-ember-js-octane-mistakes-and-how-to-avoid-them-c6420e1b0423)
- [Ember Atlas: Recommended Migration Order](https://www.notion.so/Atlas-Recommended-Migration-Order-a22f948a7cce4e01896d674f727bee74)
- [Ember Octane - Great for Beginners (video)](https://www.youtube.com/watch?v=iTPFsXcTAaY)
- [Ember Octane Livestream: Build a Drum Machine (video)](https://www.youtube.com/watch?v=5znpEiwHpL4)