"Incrementalism"

Will we get left behind?

No

But we'll get to that soon

A Better Ember

Shorter Distance From Mind to Reality

<h1>The Brown Bear</h1>
<h2>About Brown Bears</h2>
<h3>Species</h3>
<h3>Features</h3>

<h2>Habitat</h2>
<h3>Countries with Large Brown Bear Populations</h3>
<h3>Countries with Small Brown Bear Populations</h3>
<h2>Media</h2>
<ul class="navigation">
  <li>
    <img
      src="https://s3.amazonaws.com/codecademy-content/courses/web-101/unit-9/htmlcss1-img_logo-shiptoit.png"
      height="20px;"
    />
  </li>
  <li class="active">Action List</li>
  <li>Profiles</li>
  <li>Settings</li>
</ul>

<div class="search">Search the table</div>

<table>
  <thead>
    <tr><th>Company Name</th><th>Number of Items to Ship</th><th>Next Action</th></tr>
  </thead>
  <tbody>
    <tr><th>Adam's Greenworks</th><td>14</td><td>Package Items</td></tr>
    <tr><th>Davie's Burgers</th><td>2</td><td>Send Invoice</td></tr>
    <tr><th>Baker's Bike Shop</th><td>3</td><td>Send Invoice</td></tr>
    <tr><th>Miss Sally's Southern</th><td>4</td><td>Ship</td></tr>
    <tr><th>Summit Resort Rentals</th><td>4</td><td>Ship</td></tr>
    <tr><th>Strike Fitness</th><td>1</td><td>Enter Order</td></tr>
  </tbody>
</table>

It's Just HTML

export default Component.extend({
  tagName: 'p',
  attributeBindings: ['title'],

  title: null,
});
<em>
  {{yield}}
</em>
<p title={{this.title}}>
  <em>{{yield}}</em>
</p>

It's Just HTML

Tracked Properties

Problem

It's Not What You Think

Ember Data 😱

  • I have an events table
  • I have a speaker table
  • Events have many speakers
  • I couldn't figure out how to wire up the relationship
  • Maybe I can try doing it manually in the component

The Tables

// Event
{
  "name": "EmberConf",
  "speakers": ["aecd123", "bcad267"]
}

// Speaker
{
  "id": "aecd123",
  "name": "Robert Jackson"
}

What We're Trying to Do

{{#each model as |event|}}
  <Event @event={{event}} />
{{/each}}
<h1>{{@event.title}}</h1>

<h2>Speakers</h2>

<ul>
{{#each @event.speakers as |speaker|}}
  <li>{{speaker.name}}</li>
{{/each}}
</ul>

<h2>Description</h2>

<div>{{@event.description}}</div>
components/event.hbs
templates/events.hbs

an array of ids

Pseudocode

// Event:
//   { "name": "EmberConf", "speakers": ["aecd123", "bcad267"] }
//
// Speaker:
//   { "id": "aecd123", "name": "Robert Jackson" }

function speakers(store, event) {
  let objects =
    event.speakers.map(id => store.find('speaker', id))

  return objects;
}

Translated into a Component

export default class EventComponent {
  get speakers() {
    return event.speakers.map(id =>
      store.find('speaker', id))
  }
}

???

???

Where do these come from?

Translated into a Component

export default class EventComponent {
  get speakers() {
    return this.args.event.speakers.map(id =>
      store.find('speaker', id))
  }
}

Octane arguments are explicit

@event is this.args.event

Translated into a Component

export default class EventComponent {
  @service store;

  get speakers() {
    return this.args.event.speakers.map(id =>
      this.store.find('speaker', id))
  }
}

Octane services are syntactically pleasant

store is aΒ @service

accessed on this

Updating the Template

{{#each model as |event|}}
  <Event @event={{event}} />
{{/each}}
<h1>{{@event.title}}</h1>

<h2>Speakers</h2>

<ul>
{{#each this.speakers as |speaker|}}
  <li>{{speaker.name}}</li>
{{/each}}
</ul>

<h2>Description</h2>

<div>{{@event.description}}</div>
components/event.hbs
templates/events.hbs

access the getter

πŸ™Œ

One More Problem

export default class EventComponent {
  @service store;

  get speakers() {
    return this.args.event.speakers.map(id =>
      this.store.find('speaker', id))
  }
}

What happens if event.speakers changes?

πŸ€”

One More Problem

export default class EventComponent {
  @service store;

  get speakers() {
    return this.args.event.speakers.map(id =>
      this.store.find('speaker', id))
  }
}

Before Octane, it was really tricky to get this right

😟

Enter Autotrack

It changes everything

It Just Worksβ„’

export default class EventComponent {
  @service store;

  get speakers() {
    return this.args.event.speakers.map(id =>
      this.store.find('speaker', id))
  }
}

You can just use getters

😎

It Just Worksβ„’

export default class EventComponent {
  @service store;

  get speakers() {
    return speakers(this.args.event, this.store);
  }
}

function speakers(event, store) {
  return this.args.event.speakers.map(id =>
    this.store.find('speaker', id));
}

You can break out functions

😎

In Your Own Code

A Brief History

Problem Description

  • I have an events table
  • I want to write a component that takes a list of events and a pattern and filters the events by the pattern
  • When the underlying events update, I want the filter to update as well

What We're Trying to Do

<FilterableEvents @list={{model}} />
<Input @value={{this.filterPattern}} />

<ul>
{{#each this.filteredEvents as |events|}}
  <li>{{event.name}}</li>
{{/each}}
</ul>
components/filterable-events.hbs
templates/events.hbs

local state

derived state

Ember.Component.extend({
  eventPattern: '',

  filteredEvents: filter(
    'events', ['eventPattern'], 
    function(event, pattern) {
      return e.indexOf(pattern) !== -1;
    }
  )
})

Implementation, Circa 1.12

Ember's two-way binding approach in 1.x required a custom API for interacting with mutable state

Ember.Component.extend({
  eventPattern: '',

  filteredEvents: computed('events', 'eventPattern', function() {
    return this.get('events').filter(e => 
      e.indexOf(this.get('eventPattern')) !== -1
    )
  })
})

Implementation, After Glimmer 2

After Glimmer 2, it became highly efficient to write a regular computed property to filter an array

UNDERRATED!

Ember.Component.extend({
  eventPattern: '',

  filteredEvents: computed('events', 'eventPattern', function() {
    return this.events.filter(e => 
      e.indexOf(this.eventPattern) !== -1
    )
  })
})

Implementation, After RFC 281

RFC 281 removed the need to use .get to access properties in Ember

Ember.Component.extend({
  eventPattern: '',

  filteredEvents: computed('events', 'eventPattern', function() {
    return this.events.filter(e => 
      e.indexOf(this.eventPattern) !== -1
    )
  })
})
Ember.Component.extend({
  eventPattern: '',

  filteredEvents: filter('events', ['eventPattern'],
    function(event, pattern) {
      return e.indexOf(pattern) !== -1;
    }
  )
})

BEFORE

AFTER

DECENT PROGRESS, BUT... πŸ€”

We can do better!

Octane! Octane! Octane!

Ember.Component.extend({
  eventPattern: '',

  filteredEvents: computed('events', 'eventPattern', function() {
    return this.events.filter(e => 
      e.indexOf(this.eventPattern) !== -1
    )
  })
})
class extends Component {
  @tracked eventPattern = '';

  get filteredEvents() {
    return this.args.events.filter(event =>
      event.indexOf(this.eventPattern) !== -1);
  }
}

@tracked mutable state

just use a getter

explicit this.args

Ember.Component.extend({
  eventPattern: '',

  filteredEvents: computed('events', 'eventPattern', function() {
    return this.events.filter(e => 
      e.indexOf(this.eventPattern) !== -1
    )
  })
})
class extends Component {
  @tracked eventPattern = '';

  get filteredEvents() {
    return this.args.events.filter(event =>
      event.indexOf(this.eventPattern) !== -1);
  }
}

BEFORE

AFTER

NOW WE'RE COOKING!Β  πŸ‘

Glimmer Components

When?

Glimmer Component Features

feature benefits when?
<AngleBrackets /> nicer syntax, pass attributes Now!
@glimmer/component no tagName, className, etc. DSL
clean base class
Now!
modifiers cleaner, more composable DOM API Now!
native class syntax use native class syntax anywhere Now!
{{on}} and {{fn}} cleaner built-in API for event handling 3.11
@tracked one place to mark mutable state, everything else is Just JS 3.13

Almost All Incremental

Don't Stress

Being "On Octane"

Stop Using These Optional Features

feature transition
application-template-wrapper add a <div> if you need one
jquery-integration use @ember/jquery if you need to
template-only-glimmer-component add a component class if you rely on one
default-async-observers opt in to specific sync observers if really necessary

And then you're ready!

Stuff You Can Do Now

activity where
start using <AngleBrackets /> anywhere, to invoke any component πŸ₯‚
Start using {{on}} anywhere you were using {{action}} before
create new modifiers anywhere (ember-{functional,oo}-modifiers)
use native class syntax anywhere, even subclassing from Ember classes
subclass @glimmer/component new code, after learning; accept addon instability
use @tracked only if you're willing to use Canary and put up with some bugs and instability

Once You're on Octane and It's Shipped

activity how?
migrate to <AngleBrackets /> codemod + testing
Migrate to {{on}} anywhere you were using {{action}} before
Migrate to @glimmer/component manually, and carefully
Migrate to native class syntax codemod, will leave you with @computed, etc.
Migrate to @tracked manually, once you've gotten used to tracked properties
use all the new features together in new code with a smile on your face

Octane is a Bundle of Features

Octane is Polish

Octane is Almost Here

Go have some fun

And let us know how it goes

Glimmer Components

When?

Glimmer Components

What?

Upgrading to Octane

How?

Upgrading to Octane

What?

Octane for Seattle Ember

By Yehuda Katz

Octane for Seattle Ember

  • 65
Loading comments...

More from Yehuda Katz