Ember Octane

Why and How?

Gokul Kathirvel

Front-end Developer
Zoho Corp

Why Ember?

Octane Edition

Editions...

  • Significant feature set

  • Polished Ecosystem

  • Respect SemVer

  • Signal to the vast community

  • A Happy point

Octane...

  • Our first Edition

  • Improve the way we write components

  • Moves along the JS community

  • A lot of Refreshing and FUN

Why & How ?

What's the deal, Now?

  • Native Classes

  • Decorators

  • Angle Brackets (@args)

  • Tracked Properties

  • Glimmer Components

Good old counter...

// components/counter-component.js

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

export default Component.extend({
  count: 0,
  isMin: equal('count', 0),

  isMax: computed(
    'count', 'maxCount', 
    function() {
      return this.count === this.maxCount;
    }
  ),

  actions: {
    inc() {
      this.set('count', this.count + 1);
    },
    dec() {
      this.set('count', this.count - 1);
    }
  }
});
{{!-- application.hbs --}}

{{counter-component
    maxCount=10
}}

{{!-- counter-component.hbs --}}


<h2> {{count}} </h2>
<h2> MAX: {{maxCount}} </h2>

<div>
  <button disabled={{isMin}} {{action "dec"}}> 
    - 
  </button>
  <button disabled={{isMax}} {{action "inc"}}>
    +
  </button>
</div>

Native Classes

// components/counter-component.js

import Component from '@ember/component';

export default Component.extend({
  count: 0,
  ...
});

Ember's Class Model

Native Classes

class Counter {
    count = 0;

    increment() {
        this.count++;
    }


    decrement() {
        this.count--;
    }
}
import Component from '@ember/component';

class counter extends Component {
    count = 0;

    increment() {
        this.count++;
    }


    decrement() {
        this.count--;
    }
}

Just JavaScript

Native Classes

  • Easy to teach

  • Vast Ecosystem

  • Shared Solutions

  • Extend using Decorators

  • "Real" Private Props 🙈

 

 

Decorators

  • Extends the functionality

Decorators


            class counter {    
            
                increment() {
                    ...
                }
            
            }

            import debounce from 'debounce';

            class counter {    
            
                @debounce
                increment() {
                    ...
                }
            
            }

Decorators

  • Extends the functionality

  • Limited built-ins

    • ​Computed and macros
    • Service and Controller injection
    • action

Decorators

// services/user.js

import Service from '@ember/service';
import { action } from '@ember/object';

export default Service.extend({
    
  @action
  showCurrentUser() {
    let { users, userId } = this;
    let currentUser = users.findBy('id', userId);
    ...
  }
});
{{!-- components/user-profile.hbs --}}

<button {{action this.userService.showCurrentUser}}> 
    Show User
</button>
{{!-- modifer --}}

<button {{action "inc"}}> 
  +
</button>


{{!-- helper --}}

<button onclick={{action "inc"}}> 
  +
</button>

on modifier

  • Magic 🔮


<button {{on "click" this.inc}}> 
  +
</button>

on modifier

  • Magic 🔮

  • Straight forward


<button {{on "click" this.inc}}> 
  +
</button>

<button {{on "mouseover" this.inc}}> 
  +
</button>

on modifier

  • Magic 🔮

  • Straight forward


<button {{on "click" this.inc}}> 
  +
</button>

on modifier

  • Magic 🔮

  • Straight forward

  • No Implicit "this"

export default Component.extend({
  ...

  inc() {
    this.set('count', this.count + 1);
  }
});

<button {{on "click" this.inc}}> 
  +
</button>

on modifier

  • Magic 🔮

  • Straight forward

  • No Implicit "this"

import { action } from '@ember/object';

export default Component.extend({
  ...

  @action inc() {
    this.set('count', this.count + 1);
  }
});

<button {{on "click" (fn this.inc 10)}}> 
  +
</button>

on modifier & fn helper

  • Magic 🔮

  • Straight forward

  • No Implicit "this"

  • Action arguments

How...

// components/counter-component.js

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

export default class Counter extends Component {
  count = 0;
  
  @equal('count', 0) isMin;

  @computed('count', 'maxCount')
  get isMax() {
    return this.count === this.maxCount;
  }

  @action inc() {
    this.set('count', this.count + 1);
  }
  
  @action dec() {
    this.set('count', this.count - 1);
  }

});
// components/counter-component.js

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

export default Component.extend({
  count: 0,
  isMin: equal('count', 0),

  isMax: computed(
    'count', 'maxCount', 
    function() {
      return this.count === this.maxCount;
    }
  ),

  actions: {
    inc() {
      this.set('count', this.count + 1);
    },
    dec() {
      this.set('count', this.count - 1);
    },
  }
});

< />  Components

<CounterComponent
    @maxCount={{10}} 
    class="text-red" 
/>
{{counter-component 
    maxCount=10 
    class="text-red" 
}}

< />  Components

  • HTML Syntax 💪

{{!-- application.hbs --}}

<CounterComponent
    @maxCount={{10}} 
    class="text-red" 
/>

< />  Components

  • HTML Syntax 💪

  • Named args (@args)

{{!-- application.hbs --}}

<CounterComponent
    @maxCount={{10}} 
    class="text-red" 
/>
{{!-- counter-component.hbs --}}

<div> {{@maxCount}} </div>

< />  Components

  • HTML Syntax 💪

  • Named args (@args)

  • Required "this"


{{!-- counter-component.hbs --}}

<div> {{this.count}} </div>
<div> {{@maxCount}} </div>
// components/counter-component.js

import Component from '@ember/component';

export default class Counter extends Component {
  count = 0;
  ...
});

< />  Components

  • HTML Syntax 💪

  • Named args (@args)

  • Required "this"

  • Single Word Naming

{{!-- application.hbs --}}

<Counter
    @maxCount={{10}} 
    class="text-red" 
/>
{{!-- application.hbs --}}

<Input /> | <input />

< />  Components

  • HTML Syntax 💪

  • Named args (@args)

  • Required "this"

  • Single Word Naming

  • Default attr. Bindings

{{!-- application.hbs --}}

<Counter
    @maxCount={{10}} 
    title="Ember Counter"
    role="counter"
    ...
/>
// components/counter.js
export default Component.extend({
  attributeBindings: [
    'title', 'role',
    ...
  ]
});
{{!-- counter-component.hbs --}}

<div ...attributes> 
    {{this.count}} 
    ...
</div>

< />  Components

{{firstName}}

{{fullName}}

{{greet-user}}

{{capitalize-user}}



{{@firstName}}

{{this.fullName}}

<GreetUser />

{{capitalize-user}}

Very Explicit Templates 💪

How...

{{!-- application.hbs --}}

<Counter 
    @maxCount={{10}}
/>

{{!-- counter.hbs --}}

<h2> {{this.count}} </h2>
<h2> MAX: {{@maxCount}} </h2>

<div>
  <button 
    disabled={{this.isMin}} 
    {{on "click" this.dec}}> 
    - 
  </button>
  <button 
    disabled={{this.isMax}} 
    {{on "click" this.inc}}>
    +
  </button>
</div>

{{!-- counter-component.hbs --}}

<h2> {{count}} </h2>
<h2> MAX: {{maxCount}} </h2>

<div>
  <button 
    disabled={{isMin}} 
    {{on "click" this.dec}}> 
    - 
  </button>
  <button 
    disabled={{isMax}} 
    {{on "click" this.inc}}>
    +
  </button>
</div>
{{!-- application.hbs --}}

{{counter-component 
    maxCount=10
}}

@tracked properties

  • Explicit Declaration

  • Faster updates



// components/counter-component.js

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

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

  ...
});

@tracked properties

  • Explicit Declaration

  • Faster updates

  • Native Setters


// components/counter-component.js

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

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

  @action inc() {
    this.count = this.count + 1;
    // or
    // this.count++;
  }
});

@tracked properties

  • Explicit Declaration

  • Faster updates

  • Native Setters

  • Auto tracking


// components/counter-component.js

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

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

  @computed('count', 'maxCount')
  get isMax() {
    return this.count === this.maxCount;
  }
});

How...

// components/counter.js

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

export default class Counter extends Component {
  @tracked count = 0;
  
  @equal('count', 0) isMin;

  get isMax() {
    return this.count === this.maxCount;
  }

  @action inc() {
    this.count++;
  }
  
  @action dec() {
    this.count--;
  }

});
// components/counter.js

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

export default class Counter extends Component {
  count = 0;
  
  @equal('count', 0) isMin;

  @computed('count', 'maxCount')
  get isMax() {
    return this.count === this.maxCount;
  }

  @action inc() {
    this.set('count', this.count + 1);
  }
  
  @action dec() {
    this.set('count', this.count - 1);
  }

});

Glimmer Components 🌟 

  • Simpler Component Model

Glimmer Components 🌟 

    

    interface GlimmerComponent<T = object> {
      args: T;
    
      isDestroying: boolean;
      isDestroyed: boolean;
    
      constructor(owner: Opaque, args: T): void;
      willDestroy(): void;
    }

Glimmer Components 🌟 

  • No Wrapper Element

// components/counter.js

import Component from '@ember/component';

export default Component.extends({
  classNames: [
    'bg-teal-100', 'text-center'
  ],
  classNameBindings: ['isMax:text-red-100'],
  ...
})

Glimmer Components 🌟 

  • No Wrapper Element

// components/counter.js

import Component from '@ember/component';

export default Component.extends({
  classNames: [
    'bg-teal-100', 'text-center'
  ],
  classNameBindings: ['isMax:text-red-100'],
  ...
})
{{!-- counter.hbs --}}

<div class="bg-teal-100 text-center 
    {{if isMin 'text-grey-100'}} 
    {{if isMin 'text-red-100'}}"
> 
    ...
</div>

Glimmer Components 🌟 

  • No Wrapper Element

  • Explicit Args (this.args)

// components/counter.js

import Component from '@glimmer/component';

export default class Counter extends Component {
  get isMax() {
    return this.count === this.args.maxCount;
  }

  ...

});

Glimmer Components 🌟 

  • No Wrapper Element

  • Explicit Args (this.args)

  • OneWay data binding

// components/counter.js

import Component from '@glimmer/component';

export default class Counter extends Component {

  updateMaxCount(maxCount) {
    this.args.maxCount = maxCount;
  }

});

How...

// components/counter.js

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

export default class Counter extends Component {
  @tracked count = 0;
  
  @equal('count', 0) isMin;

  get isMax() {
    return this.count === this.args.maxCount;
  }

  @action inc() {
    this.count++;
  }
  
  @action dec() {
    this.count--;
  }

});
// components/counter.js

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

export default class Counter extends Component {
  @tracked count = 0;
  
  @equal('count', 0) isMin;

  get isMax() {
    return this.count === this.maxCount;
  }

  @action inc() {
    this.count++;
  }
  
  @action dec() {
    this.count--;
  }

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

export default Component.extend({
  count: 0,
  
  isMin: equal('count', 0),

  isMax: computed('count', 'maxCount', function() {
      return this.count === this.maxCount;
    }
  ),

  actions: {
    inc() {
      this.set('count', this.count + 1);
    },
    dec() {
      this.set('count', this.count - 1);
    },
  }
});
{{counter maxCount=10 }}
<h2> {{count}} </h2>
<h2> MAX: {{maxCount}} </h2>

<div>
  <button disabled={{isMin}} {{action "dec"}}> 
    - 
  </button>
  <button disabled={{isMax}} {{action "inc"}}>
    +
  </button>
</div>
<Counter @maxCount={{10}}/>
<h2> {{this.count}} </h2>
<h2> MAX: {{@maxCount}} </h2>

<div>
  <button disabled={{this.isMin}} {{on "click" this.dec}}> 
    - 
  </button>
  <button disabled={{this.isMax}} {{on "click" this.inc}}>
    +
  </button>
</div>
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { equal } from '@ember/object/computed';
import { tracked } from '@glimmer/tracking';

export default class Counter extends Component {
  @tracked count = 0;
  
  @equal('count', 0) isMin;

  get isMax() {
    return this.count === this.args.maxCount;
  }

  @action inc() {
    this.count++;
  }
  
  @action dec() {
    this.count--;
  }
});

Let's have

Fun with Octane

Gotta catch 'em all

Gotta catch 'em all

Create an Octane App

 yarn add global ember-cli

 

ember new -b @ember/octane-app-blueprint octane-pokedex

What's Next

  • Embroider Builds

  • Code splitting / Bundle Sizes

  • Template imports

  • and more exciting things...

I Referred...

Thanks for your time

Any Questions?

Octane: Why and How?

By Gokul Kathirvel

Octane: Why and How?

Gentle intro to Octane Edition on Ember.

  • 1,684