Ember Octane: the latest edition from Ember.js

EmberData Request Service Cheat Sheet

This guide is a cheat sheet for using EmberData's Request Service. It doesn't cover everything, but it should get you started! PRs welcome at the GitHub repository.

For in-depth information about the upgrade paths and differences compared to older EmberData patterns, see the RFC for the EmberData Request Service.

Generating Files

§

Use an option to generate a component's JavaScript

§

In classic Ember, ember generate component created three files: the template, a JavaScript file, and a test. In Octane, ember generate component skips creating the JavaScript file. If you want the backing JavaScript class as well, include the -gc option.

Older patterns

ember generate component my-component

Request Service

# -gc stands for glimmer component
ember generate component my-component -gc

# -cc stands for classic component
ember generate component my-component -cc

# See the full set of options with this:
ember generate component --help

Component templates and JavaScript are in the same directory

§

The location of component templates has changed in Octane. This is known as "template co-location."

Older patterns

app/
  components/
    my-component.js
  templates/
    components/
      my-component.hbs

Request Service

app/
  components/
    my-component.hbs
    my-component.js

Component Templates

§

Angle brackets component invocation

§

Angle brackets are a way to invoke components in a template file. There's no change in behavior. Learn more about features and using codemods to update your existing components.

Older patterns

{{my-component}}

Request Service

<MyComponent />

Inline vs block components

§

Angle brackets components can be used as either inline or block components. There's no change in behavior. {{yield}} looks and works the same. Learn more about features and using codemods to update your existing components.

Older patterns

{{!-- inline --}}
{{my-component}}

{{!-- block --}}
{{#my-component as |text|}}
  some markup or {{text}}
{{/my-component}}

Request Service

{{!-- inline --}}
<MyComponent />

{{!-- block --}}
<MyComponent as |text|>
  some markup or {{text}}
</MyComponent>

Nesting components in your file structure

§

You can nest components in your file structure, and use them in a template with angle brackets invocation. There's no change in behavior.

Older patterns

{{ui-files/my-component}}

Request Service

<UiFiles::MyComponent />

Using named argument

§

If a property was received from a parent component, refer to it with an @. There's no change in behavior. (Visit the Ember Guides to learn more.)

Older patterns

{{answer}} is the answer.

Request Service

{{@answer}} is the answer.

Using own properties

§

If a property is defined in the JavaScript file for this component's template, use this when you refer to it. There's no change in behavior. Learn more about features and using codemods to update your existing components.

Older patterns

{{answer}} is the answer.

Request Service

{{this.answer}} is the answer.

Passing named arguments

§

When you pass an argument to a component, use an @ symbol on the left hand side. There's no change in behavior. Learn more about features and using codemods to update your existing components.

Older patterns

{{my-component answer=answer}}

Request Service

<MyComponent @answer={{@answer}} />

Passing arguments defined on "this" component

§

If a property is coming from a template's own JavaScript file, remember to put a this before it and wrap it in curly braces. Learn more about features and using codemods to update your existing components.

Older patterns

{{my-component answer=answer}}

Request Service

<MyComponent @answer={{this.answer}} />

All components are tagless

§

In Octane, components don't have a default wrapper anymore, so you don't need tagName! Just put the tag right in the template.

Older patterns

{{!-- parent-component.hbs --}}
{{child-component tagName="ul"}}

{{!-- child-component.hbs --}}
<li>item</li>
<li>item</li>
<!-- rendered output -->
<ul id="ember209" class="ember-view">
  <li>item</li>
  <li>item</li>
</ul>

Request Service

{{!-- parent-component.hbs --}}
<ul>
  <ChildComponent />
</ul>

{{!-- child-component.hbs --}}
<li>item</li>
<li>item</li>
<!-- rendered output -->
<ul>
  <li>item</li>
  <li>item</li>
</ul>

Make your own elementId

§

Since components are tagless, there's no elementId, but you can generate your own. This is especially helpful for creating accessible forms.

Older patterns

import Component from '@ember/component';

export default Component.extend({
  didInsertElement() {
    console.log(this.elementId);
  }
});
<div class="form-group">
  <label for="textInput-{{elementId}}">{{inputLabelText}}</label>
  <input 
    id="textInput-{{elementId}}"
    name={{inputName}} 
    type="text" 
  />
</div>

Request Service

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

export default class InputTextComponent extends Component {
  inputId = 'textInput-' + guidFor(this); 
}
<div class="form-group">
  <label for={{this.inputId}}>{{@inputLabelText}}</label>
  <input 
    id={{this.inputId}} 
    name={{@inputName}} 
    type="text" 
  />
</div>

Component Properties

§

Component JavaScript Syntax

§

An Octane component imports from the @glimmer package namespace. It also uses native class syntax. Glimmer components have different API methods than classic components. If you aren't familiar with native classes, first check out the documentation on MDN. Then, learn more about Glimmer components from the Ember.js Guides and the API Reference.

Older patterns

import Component from '@ember/component';

export default Component.extend({
  init() {
    this._super(...arguments);
  }
});

Request Service

import Component from '@glimmer/component';

export default class MyComponent extends Component {
  constructor() {
    super(...arguments);
  }
}

Declaring a property

§

Properties follow native JavaScript class syntax. No commas!

Older patterns

import Component from '@ember/component';

export default Component.extend({
  min: 0,
  max: 100
});

Request Service

import Component from '@glimmer/component';

export default class MyComponent extends Component {
  min = 0;
  max = 100;
}

Data Down, Actions Up

§

Octane components enforce "Data Down, Actions Up." When data is passed down to a component, the only way to change that data is to call an action that was passed in too. In another words, there is no two-way binding for component arguments.

Older patterns

// parent-component.js
import Component from '@ember/component';

export default Component.extend({
  count: 0
});
{{!-- parent-component.hbs --}}
{{child-component count=count}}
Count: {{this.count}}
// child-component.js
import Component from '@ember/component';

export default Component.extend({
  actions: {
    plusOne() {
      this.set('count', this.get('count') + 1);
    }
  }
});
{{!-- child-component.hbs --}}
<button type="button" {{action "plusOne"}}>
  Click Me
</button>

Request Service

// parent-component.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class ParentComponent extends Component {
  @tracked count = 0;

  @action plusOne() {
    this.count++;
  }
}
{{!-- parent-component.hbs --}}
<ChildComponent @plusOne={{this.plusOne}} />
Count: {{this.count}}
{{!-- child-component.hbs --}}
<button type="button" {{on "click" @plusOne}}>
  Click Me
</button>

Component Arguments

§

In Octane, arguments are set on the args property, this.args. They are not set on the class instance, this. As a result, you can distinguish internal class values from external arguments. (Visit the Ember Guides to learn more about arguments.)

Older patterns

import Component from '@ember/component';
import { computed } from '@ember/object';

export default Component.extend({
  prefix: 'ISO',

  displayName: computed('prefix,isoStandardNumber', function() {
    /*
      There are two issues with this code:

      1. `prefix` may or may not be overriden from outside
      2. We can't tell if `isoStandardNumber` is a property or an argument
    */
    return `${this.prefix} ${this.isoStandardNumber}`;
  }),

  /* Rest of the code omitted */
});

Request Service

import Component from '@glimmer/component';

export default class MyComponent extends Component {
  prefix = 'ISO';

  get displayName() {
    return `${this.prefix} ${this.args.isoStandardNumber}`;
  }

  /* Rest of the code omitted */
}

No more get and set on components

§

Octane components do not use this.get or this.set. Access and modify properties directly, the same as you would in a regular JavaScript class.

Older patterns

import Component from '@ember/component';

export default Component.extend({
  count: 0,

  actions: {
    minusOne() {
      this.set('count', this.get('count') - 1);
    }
  }
});

Request Service

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class MyComponent extends Component {
  @tracked count = 0;

  @action minusOne() {
    this.count = this.count - 1;
  }
}

Use @tracked and getters instead of computed properties

§

Label the properties that should be tracked. Getters will be updated automatically.

Older patterns

import Component from '@ember/component';
import { computed } from '@ember/object';

export default Component.extend({
  index: 0,

  nextIndex: computed('index', function() {
    return this.index + 1;
  })
});

Request Service

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

export default class MyComponent extends Component {
  @tracked index = 0;

  get nextIndex() {
    return this.index + 1;
  }
}

@computed decorator

§

In Octane, it is recommended to rewrite a computed property as tracked property. In case that isn't possible (maybe you need caching), the @computed decorator is available.

Older patterns

import Component from '@ember/component';
import { computed } from '@ember/object';

export default Component.extend({
  index: 0,

  nextIndex: computed('index', function() {
    return this.index + 1;
  })
});

Request Service

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

export default class MyComponent extends Component {
  index = 0;

  @computed('index')
  get nextIndex() {
    return this.index + 1;
  }
}

Actions

§

Use @action, {{on}}, and {{fn}} instead of {{action}}

§

In the JavaScript class, use the @action decorator to mark the function that you want to call from the template. In the template, use the {{on}} modifier to decide when to call the function. If you need to pass an argument to the function, use {{fn}} helper too. (Visit the Ember Guides to learn more.)

Older patterns

import Component from '@ember/component';

export default Component.extend({
  actions: {
    sayHello() {
      console.log('Hello, world!');
    },

    saySomethingElse(message) {
      console.log(message)
    }
  }
});
<button
  type="button"
  onclick={{action "sayHello"}}
>
  Say hello!
</button>

<button
  type="button"
  onclick={{action "saySomethingElse" "Cheese!"}}
>
  Say cheese!
</button>

Request Service

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

export default class MyComponent extends Component {
  @action sayHello() {
    console.log('Hello, world!');
  }

  @action saySomethingElse(message) {
    console.log(message);
  }
}
<button
  type="button"
  {{on "click" this.sayHello}}
>
  Say hello!
</button>

<button
  type="button"
  {{on "click" (fn this.saySomethingElse "Cheese!")}}
>
  Say cheese!
</button>

Setting the default value of an argument

§

Because an argument is read-only, you cannot set its default value in the consuming class. Instead, you can create a getter to wrap the argument and provide the default value.

Older patterns

{{!-- parent-component.hbs --}}
{{child-component answer=answer}}
// child-component.js
import Component from '@ember/component';

export default Component.extend({
  answer: 42
});

Request Service

{{!-- parent-component.hbs --}}
<ChildComponent @answer={{this.answer}} />
// child-component.js
import Component from '@glimmer/component';

export default class ChildComponent extends Component {
  get answer() {
    return this.args.answer ?? 42;
  }
}

Mixins

§

You cannot use mixins on anything that uses native class syntax (e.g. components that import from @glimmer/component). The migration strategy depends on your use case. If your app depends on an addon that uses mixins, it may be best to continue using classic components until the addon is Octane-ready.

Older patterns

import Component from '@ember/component';
import SomeMixin from 'some-addon';

export default Component.extend(SomeMixin, {
  // ...
});

Request Service

See Do you need Ember Object? for alternatives to mixins, which include utility functions, services, delegates, and class decorators.

Component Lifecycle

§

Use constructor instead of init

§

constructor comes from native JavaScript class. You can use this hook to set up the component class, similarly to what you might have done in init. (Visit the Ember Guides to learn more about constructor and super.)

Older patterns

import Component from '@ember/component';

export default Component.extend({
  init() {
    this._super(...arguments);
    this.set('answer', 42);
  }
});

Request Service

import Component from '@glimmer/component';

export default class MyComponent extends Component {
  constructor(owner, args) {
    super(owner, args);
    this.answer = 42;
  }
}

Use willDestroy instead of willDestroyElement

§

willDestroy is the only other lifecycle hook (besides constructor) that Glimmer components have. You can use this hook to tear down the component class, similarly to what you might have done in willDestroyElement. (Visit the Ember Guides to learn more about lifecycle of Glimmer components.)

Older patterns

import Component from '@ember/component';

export default Component.extend({
  willDestroyElement() {
    // Remove event listener
  }
});

Request Service

import Component from '@glimmer/component';

export default class MyComponent extends Component {
  willDestroy() {
    // Remove event listener
  }
}

Element Modifiers

§

If you install @ember/render-modifiers, you get {{did-insert}} and {{did-update}} modifiers. You may use them to replace a classic component's lifecycle hooks didInsertElement, didRender, and didUpdate.

Older patterns

Have Ember apps at version 2.18 or higher? You can use these modifiers to start converting classic components to Glimmer ones.

Request Service

ember install @ember/render-modifiers

Use {{did-insert}} instead of didInsertElement

§

If you used the didInsertElement hook, consider making an action. You can call the action with the {{did-insert}} modifier. Use the @action decorator to bind the correct context (this). (Visit the Ember Guides to learn more about lifecycle of Glimmer components.)

Older patterns

<canvas id="my-canvas"></canvas>
import Component from '@ember/component';

export default Component.extend({
  didInsertElement() {
    // Find the canvas element by id. Then, create a graph.
  }
});

Request Service

<canvas {{did-insert this.createGraph}}></canvas>
import Component from '@glimmer/component';
import { action } from '@ember/object';

export default class MyComponent extends Component {
  @action createGraph(element) {
    // You have the canvas element. Now make the graph!
  }
}

Routes

§

Accessing a route's model

§

In a route template, the model comes from an outside context. Use @model to access the result from the route's model hook.

Older patterns

First name: {{model.firstName}}

Request Service

First name: {{@model.firstName}}