Skip to content

Instantly share code, notes, and snippets.

@jmar910
Last active April 26, 2017 13:40
Show Gist options
  • Save jmar910/aae135cf9fb8adcc6fda2d31363e45ba to your computer and use it in GitHub Desktop.
Save jmar910/aae135cf9fb8adcc6fda2d31363e45ba to your computer and use it in GitHub Desktop.
A component focused overview of Ember

autoscale: true slidenumbers: true

Ember for all your client-side needs

James Martinez


original fit


Why Ember?

  • Opinionated architecture
  • "All-in-one" solution 🎉
    • Templating
    • Routing
    • Composable Components
    • Data/Model layer
    • Build tooling/CLI
    • Testing framework
  • Addon ecosystem
  • Community

right


ember-cli

  • Project creation

    • ember new project-name
  • Local development

    • ember server
  • Builds

    • ember build
  • Generators

    • ember g component my-component
  • Testing

    • ember test

original fit


original fit


Major Framework Classes

  • POEOs (Ember Object)
    • Routes
    • Components
    • Controllers (Routable Components)
    • Models (Ember-data)
    • Services

POEOs, The Object Model, & Computed Properties

import Ember from 'ember';
const { computed } = Ember;

let jedi = Ember.Object.create({
  firstName: 'Luke',
  lastName: 'Skywalker',
  fullName: computed('firstName', 'lastName', function() {
    return `${this.get('firstName')} ${this.get('lastName')}`;
  })
});

jedi.get('fullName');
// => "Luke Skywalker"

jedi.set('firstName', 'Anakin');

jedi.get('fullName');
// => "Anakin Skywalker"

Handlebars -- It's HTML

  • Pure HTML is perfectly valid Handlebars
<div class="header">
  <h1>Hello World!</h1>
</div>

Handlebars -- Context

<div class="header">
  {{someTextProperty}}
</div>

{{#if userLoggedIn}}
  <!-- Show logout link -->
{{else}}
  <!-- Show login link -->
{{/if}}

Handlebars -- Iteration

<!-- `foos` is an array of objects -->
<ul>
  {{#each foos as |foo|}}
    <li>{{foo.someProperty}}</li>
  {{/each}}
</ul>

Routing -- The Router

  • Ember is a "Routing first" framework
  • URLs are meant to represent state
// in app/router.js
Router.map(function() {
  this.route('index', { path: '/' }); // This comes implicitly

  this.route('jedis'); // => /jedis
});

Routing -- Route

// app/routes/jedis.js
import Ember from 'ember';
const { Route } = Ember;
export default Route.extend({
  model() {
    return [
      "Luke Skywalker",
      "Obi-Wan Kenobi",
      "Yoda",
      "Mace Windu"
    ];
  }
});

Routing -- Templates

<!-- app/templates/jedis.hbs -->
<ul class="jedi-index">
  {{#each model as |jedi|}}
    <li>{{jedi}}</li>
  {{/each}}
</ul>

Routing -- Dynamic Segments & Nesting

Router.map(function() {
  this.route('index', { path: '/' }); // This comes implicitly

  this.route('jedis', function() {
    this.route('jedi', { path: '/jedis/:jedi_id' })
  });
});

Routing -- Dynamic Segments & Nesting

<!-- app/templates/jedis.hbs -->
<ul class="jedi-index">
  {{#each model as |jedi|}}
    <li>{{jedi}}</li>
  {{/each}}
</ul>

Routing -- Dynamic Segments & Nesting

<!-- app/templates/jedis.hbs -->

<!-- Master -->
<ul class="jedi-index">
  {{#each model as |jedi|}}
    <li>{{jedi}}</li>
  {{/each}}
</ul>

<!-- Detail -->
<div class="jedi-detail">
  {{outlet}}
</div>

Controllers, Briefly...

  • One controller backs one route
  • "Routeable component" -- The top-level component for this route
// app/controllers/jedis.js
import Ember from 'ember';
const { Controller, computed } = Ember;

export default Controller.extend({
  // Serves the template @ app/templates/jedis.hbs
  // Has access to whatever is returned in route's model() hook via `model` prop
  jedisModel: computed.alias('model')
});

Components

  • The core of Ember 🍎
  • Composable 🎵
  • Two files
    • Component definition (.js)
    • Component template (.hbs)

Component Definition

// app/components/my-component.js
import Ember from 'ember';
const { Component } = Ember;

export default Ember.Component.extend({
  tagName: 'div',
  classNames: ['class-name-one', 'class-name-two'],
  classNameBindings: ['isShowingFoo:true-class:false-class']

  foo: 'hello world!',

  isShowingFoo: false
})

Component Template

<!-- app/templates/components/my-component.hbs -->

{{#if isShowingFoo}}
  <div>
    {{foo}}
  </div>
{{/if}}

Component Usage

<!-- app/templates/index.hbs -->

<div class="index-wrapper">
  {{my-component isShowingFoo=true}}
</div>

Component Usage -- Blocks

<!-- app/templates/index.hbs -->

<div class="index-wrapper">
  {{#my-component isShowingFoo=true}}
    Some extra content
  {{/my-component}}
</div>

Component Usage -- Blocks

<!-- app/templates/components/my-component.hbs -->

{{#if isShowingFoo}}
  <div>
    {{foo}}
  </div>
{{/if}}

<div class="some-extra-content">
  {{yield}}
</div>

Component Actions

// app/components/lightsaber-picker.js
import Ember from 'ember';
const { Component } = Ember;

export default Component.extend({
  lightsaberColor: 'green'

  isShowingLightsaber: false,

  actions: {
    toggleShowingLightsaber() {
      // Ember sugar that toggles boolean values
      this.toggleProperty('isShowingLightsaber');
    },

    setLightsaberColor(color) {
      this.set('lightsaberColor', color);
    }
  }
});

Component Actions

<!-- app/templates/components/jedi-component.hbs -->

<div class="set-lightsaber-color">
  <div class="green" {{action 'setLightsaberColor' 'green'}}></div>
  <div class="red"   {{action 'setLightsaberColor' 'red'}}></div>
  <div class="blue"  {{action 'setLightsaberColor' 'blue'}}></div>
</div>

<button {{action 'toggleShowingLightsaber'}}>
  Toggle Lightsaber
</button>

{{#if isShowingLightsaber}}
  <div class="lightsaber {{lightsaberColor}}"></div>
{{/if}}

Component Events

// app/components/han-solo.js
import Ember from 'ember';
const { Component } = Ember;

export default Component.extend({
  classNames: ['han-solo']
  classNameBindings: ['isFrozenInCarbonite:frozen:unfrozen'],

  isFrozenInCarbonite: false,

  click() {
    this.toggleProperty('isFrozenInCarbonite');
  }
});

Composing Components -- Parent

// app/components/x-wing.js
import Ember from 'ember';
const { Component } = Ember;

export default Component.extend({
  shipHealth: 100,
  firingTorpedos: false,

  actions: {
    fireProtonTorpedos() {
      this.set('firingTorpedos', true);
    }
  }
});

Composing Components -- Parent

<!-- app/templates/components/x-wing.hbs -->
<div class="pilot">
  {{luke-skywalker shipHealth=shipHealth
                   fire=(action 'fireProtonTorpedos')
  }}
</div>

Composing Components -- Child

// app/components/luke-skywalker.js
import Ember from 'ember';
const { Component } = Ember;

export default Component.extend({
  usingTheForce: false,

  // shipHealth passed in by parent
  shouldAbortMission: computed('shipHealth', function() {
    return this.get('shipHealth') < 50;
  }),

  actions: {
    useTheForce() {
      this.set('usingTheForce', true);
      // Closure action -- passed in by parent
      this.get('fire')(); // or this.attrs.fire()
    }
  }
});

Composing Components -- Child

<!-- app/templates/components/luke-skywalker.hbs -->

<button {{action 'useTheForce'}}>Use the Force</button>

Component Lifecycle hooks

  • didUpdateAttrs: invoked when a component's attributes have changed but before the component is rendered.
  • willUpdate: invoked before a component will rerender, whether the update was triggered by new attributes or by rerender.
  • didUpdate: invoked after a component has been rerendered.
  • didReceiveAttrs: invoked when a component gets attributes, either initially or due to an update.
  • willRender: invoked before a component will render, either initially or due to an update, and regardless of how the rerender was triggered.
  • didRender: invoked after a component has been rendered, either initially or due to an update
  • didInsertElement
  • willDestroyElement

ember-data

  • Model/Data Library
  • Backend agnostic (json-api compliant works out of the box)
  • store interface
  • Plus
    • Relationship Support for Models
    • Adapters - Talks Endpoints
    • Serializers - Talks Payloads

ember-data -- Models

// app/models/jedi.js
import DS from 'ember-data';
const { Model, attr, hasMany, belongsTo } = DS;

export default Model.extend({
  firstName: attr('string'),
  lastName:  attr('string'),
  hasBothHands: attr('boolean'),
  shotsDeflected: attr('number'),

  midichlorians: hasMany('midichlorians'),
  lightsaber: belongsTo('lightsaber')
});

ember-data -- Fetching Models

// app/routes/jedis.js
import Ember from 'ember';
const { Route } = Ember;
export default Route.extend({
  model() {
    // Store => Adapter => "/jedis" => payload => Serializer => Model
    return this.store.findAll('jedi');
  }
});

ember-data -- Working w/ Models

<!-- app/templates/jedis.hbs -->
<ul class="jedi-index">
  {{#each model as |jedi|}}
    <li>{{jedi.firstName}} {{jedi.lastName}}</li>
  {{/each}}
</ul>

<div class="jedi-detail">
  {{outlet}}
</div>

ember-data -- Working w/ Models

// In a route, controller, or component action
let jedi = this.store.createRecord('jedi', {
  firstName: 'Luke',
  lastName: 'Skywalker',
  hasBothHands: true,
  shotsDeflected: 99999999
});

jedi.save(); // => POST /jedis
jedi.get('isDirty'); // => false
jedi.set('hasBothHands', false);
jedi.get('isDirty'); // => true
jedi.save(); // => PATCH/PUT /jedis/:id
jedi.destroyRecord(); // => DELETE /jedis

Testing

  • QUnit or Mocha
  • Acceptance, Integration, & Unit
  • Ember helpers

Testing -- Types

  • Acceptance
    • Feature workflow and user interaction from start to finish.
  • Unit
    • Common to test the methods, computed properties, and observers of Components, Models, Controllers, Services, and Utility Objects.
  • Integration
    • Sit between Unit tests and Acceptance tests
    • Since components are never truly isolated in an Ember application, these work well for testing a components behavior

Interesting tidbits -- Routes

  • Route hooks are promise-aware
    • beforeModel
    • model
    • afterModel
  • Returned promise resolving === Loading state
  • Error states
  • Actions hash just like components!

Interesting tidbits -- Services

  • Extremely powerful construct for sharing state across application
  • Example uses of services include
    • Logging
    • User/session authentication
    • Geolocation
    • Third-party APIs
    • Web Sockets
    • Server-sent events or notifications
    • Server-backed API calls that may not fit Ember Data

Interesting tidbits -- Addons

  • Drop in libraries tailored for ember apps
  • Robust ecosystem
  • ember install addon <addon-name>
  • Popular addons include
    • ember-cli-mirage
    • emberx-select
    • ember-cli-simple-auth

Interesting tidbits -- Grievances

  • Async loading outside of routes
  • Two way bindings
  • Testing story & Async testing
  • Doesn't like inconsistent APIs
  • Learning Curve?

Interesting tidbits -- Other

  • Contextual components
  • Mixins
  • Initializers
  • Ember run loop (Backburner.js)
  • Ember inspector

GlimmerJS

  • Standalone Library
  • Extracted from Ember's component layer
  • Typescript
  • "Ember-flavored React"
  • Only 34kb 😉

Resources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment