Converting an Ember Component to a Glimmer Component, Part 1

Learn the basics needed to convert an Ember native class component to a Glimmer component.

Summary

In this video we convert an Ember component using native class syntax into a Glimmer component. This will involve:

  • How to actually use Glimmer to make your component
  • Removing the wrapper tag
  • Replacing the didInsertElement lifecycle hook
  • Updating the component argument syntax:
    • using @ in template
    • using this.args in JavaScript files
    • no implicit this

How to use Glimmer to make your component

To use Glimmer to render components, import Component from @glimmer/component rather than @ember/component:

import Component from '@glimmer/component'

Removing the wrapper tag

Glimmer components do not have wrapper elements, so we no longer need to define any tagNames in our JavaScript file. Instead, we can put any HTML tag we want directly in our template.

Next we need to update the didInsertElement hook. This lifecycle hook is not supported in Glimmer components. To replace logic that happens in didInsertElement, we can use the did-insert modifier from the ember-render-modifiers addon.

Updating the component argument syntax

An important thing to understand about Glimmer components is that passed-in arguments are referred to differently than internal properties.

Properties that are passed into your component are now immutable. You can't change them and you can't overwrite them. For this reason, the syntax you use to refer to passed-in arguments in your components is different in both your JavaScript and template files.

In a JavaScript file, you use the args object to reference a passed-in argument. In a template file, you use the @ sign (instead of this) to reference a passed-in argument. For more information about this, check out the video or see the official migration cheat sheet.

If you do not prefix your external props with this syntax, your component will not read the correct values and it will break.

No implicit this

Finally, updating components to use Glimmer changes the relationship the component has with this. In a Glimmer world, this is reserved for properties defined directly on the component object. This idea is called no-implicit-this and can be enforced with a linter rule of the same name with ember-template-lint. While not technically required in current versions of Ember, it is considered good practice to follow and implicit-this will be deprecated in Ember 4. If you need to convert your whole app away from implicit-this there is a codemod that will do most of the work for you.

Here's the diff of all these changes applied to our files:

// app/components/comments.js
import {tagName} from '@ember-decorators/component';
import {action, computed} from '@ember/object';
import {inject as service} from '@ember/service';
import {gt, readOnly} from '@ember/object/computed';
+ import Component from '@glimmer/component';
- import Component from '@ember/component';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';

- @tagName('section')
export default class Comments extends Component {
  @service
  store;

  @tracked isShowMoreExpanded = false;
  @tracked comments = [];
  @tracked allowNewComments = false;

  @readOnly('comments.length')
  commentCount;

  @gt('commentCount', 5)
  isTooManyComments;

  @computed('isTooManyComments', 'isShowMoreExpanded')
  get shouldShowExpandCommentOption() {
    if (this.isTooManyComments && !this.isShowMoreExpanded) return true;
    return false;
  }

  @computed('isTooManyComments', 'isShowMoreExpanded')
  get slicedComments() {
    if (this.isTooManyComments && !this.isShowMoreExpanded) {
      return this.comments.slice(0, 5);
    } else {
      return this.comments;
    }
  }

  @action
  expandComments() {
    this.isShowMoreExpanded = true;
  }

-  didInsertElement() {
-    this.fetchComments.perform(this.post);
-  }

  @task(function* (post) {
    const comments = yield this.store.query('comment', {
      post_id: post.id,
      include: 'author',
    });
    this.comments = comments;
  })
  fetchComments;
}
// app/components/comments.hbs
- {{#if fetchComments.isRunning}}
-   <span data-test-comments-loading>
-     <div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
-   </span>
- {{else}}
-   <div data-test-post-comments>
-     {{#if allowNewComments}}
+ <section {{did-insert (perform this.fetchComments @post)}}>
+   {{#if this.fetchComments.isRunning}}
+     <span data-test-comments-loading>
+       <div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
+     </span>
+   {{else}}
     {{#if this.allowNewComments}}
      <NewComment @currentUser={{@currentUser}} />
    {{/if}}
-    {{#each slicedComments as |comment|}}
+    {{#each this.slicedComments as |comment|}}
      <Comment @comment={{comment}}/>
    {{/each}}
-     {{#if shouldShowExpandCommentOption}}
+     {{#if this.shouldShowExpandCommentOption}}
      <button
        class="expand-comments-button border-gray-500 text-gray-500 hover:border-teal-500 hover:text-teal-500"
        {{action "expandComments"}}
      >
        Show more comments
      </button>
    {{/if}}
-   </div>
- {{/if}}
+   {{/if}}
+ </section>

Questions?

Send us a tweet:

Or ask us in Inside EmberMap, our private Slack workspace for subscribers.