Client MVC != Server MVC



Client MVC != Server MVC

0 0


ember_ndc_london

My presentation for NDC London

On Github jagthedrummer / ember_ndc_london

Client MVC != Server MVC

Client MVC != Server MVC

Ember for Server Side MVC Developers

A bit about me

Jeremy Green - Consultant

Organizer of OkcRuby.org

@jagthedrummer jeremy@octolabs.com http://www.octolabs.com/

Things I Enjoy : Dopamine, Serotonin

Other Interests : Drumming, Photography, and Brewing

These items are in chronological order. I've been drumming much more than half my life. Coding, just about half. * next fragment * Thanks to Jesse and Vance of okcjs for organizing Thunder Plains. * next fragment * I'm historically bad at Twitter, but trying to get better. Contacting me through OctoLabs is more reliable.
http://watch.canary.io

A talk in two parts

Compare/Contrast Ember <=> Rails Step through building a small app

Set the stage

Ember is a tool, and like all tools you should understand how and when to use it. Like knowing when to use a hammer instead of a saw, it's important to know when to use Ember instead of something else. To do that it's helpful to think about what kind of problem Ember solves. Is it "build a website"? "build a website plus some JS?" "build an application?" "build a well structured application?" So, a useful question to get started thinking about where Ember fits in is:

Set the stage

Where does the faceof your web app live?

Where is it generated?

"Traditional" web apps

Generate a giant string of HTML on the server Deliver a fully formed 'document' to the browser Apply a sprinkle of JavaScript Replace bits of markup via AJAX

Single page apps

Deliver empty application assets to the browser Pull/push raw data via JSON Assemble the 'document' in the browser

Challenges of SPAs

Challenges of SPAs

  • Rebuilding the a tag
  • Data retrieval & display
  • User interaction
  • Data binding
  • Manipulating the URL
  • Setting up initial state

Ember to the rescue!

Ember to the rescue!

Ember strives to solve all of these problems

in an opinionated way

MVC-ish

NOT server side MVC

Many of the same termsbut sometimes they mean different things

MVC+

  • Model
  • View
  • Controller
Router Route Template

where Ember == Rails

where Ember == Rails

Router == config/routes.rb

Connect URLs to code paths

  # Rails config/routes.rb
  Rails.application.routes.draw do
    get 'about' => 'static_pages#about', :as => :about
  end
  
  // Ember Router
  App.Router.map(function(){
    this.route('about');
  });
  

where Ember == Rails

Model == Model

Object wrapper for business data

  # Rails db/schema.rb
  create_table "patterns" do |t|
    t.integer   "p1"
    t.integer   "p2"
    t.integer   "p3"
    # ...
  end
  
  class Pattern < ActiveRecord::Base
    def name
      "#{p1}-#{p2}-#{p3}"
    end
  end
  

where Ember == Rails

Model == Model

Object wrapper for business data

  App.Pattern = DS.Model.extend({
    p1 : DS.attr('number'),
    p2 : DS.attr('number'),
    p3 : DS.attr('number'),

    name : function(){
      return this.get('p1') + '-' +
             this.get('p2') + '-' +
             this.get('p3');
    }.property('p1','p2','p3')
  });
  

where Ember != Rails

where Ember != Rails

Rails : view == template

Ember : view != template

Template == Template

Markup for UI

Ember Views =~ JS Sprinkles

Interaction & DOM Logic

Ember Components are VERY similar

Components & Views

Interaction & DOM logic

This is the place for jQuery DOM selectors andintegration with other JS libs.

Components vs. Views

Component

  • Isolated (No application context)
  • Only knows about whatever you pass in to it

View

  • Not-Isolated (Has appication context)
  • Knows about the controller for the current template

Use Components if possible

where Ember != Rails

What is a Route?

  # Rails config/routes.rb
  get 'about' # <-- This line is commonly called a route.
  
  // Ember Router
  this.route('about'); // <-- This line is commonly called ???
                       // A line in the router?
                       // It is NOT a Route.
  

where Ember != Rails

Rails : route IN the router   

Ember : Route AFTER the router

where Ember != Rails

What happens after URL mapping?

  # app/controllers/pages_controller.rb
  class PagesController < ApplicationController
    def about
      @authors = Author.all
    end
  end
  
  App.AboutRoute = Ember.Route.extend({
    model : function(){
      return this.store.find('author');
    }
  });
  

where Ember != Rails

So Ember Route == Rails Controller?

Not exactly

Route =~ GET Controller actions

Setup data for user interaction (index, new, edit, show)

where Ember != Rails

So, what about otherRails controller actions?

Controller =~ [POST|PUT|DELETE] Controller actions

Handle user interactions and input (create, update, delete)

where Ember != Rails

Ember Controllers

  App.PatternsNewController = Ember.ObjectController.extend({
    actions : {
      savePattern : function(){
        this.get('model').save();
        this.transitionToRoute('pattern',this.get('model'));
      }
    }
  });
  
  

Part 1 Wrap Up

Part 1 Wrap Up

Ember is opinionated

Don't try to fight it

Just learn aboutthose opinions

Handy comparisons

Ember   =~   Rails Router   =~   config/routes.rb Model   =~   Model Template   =~   Template Views   =~   JS Sprinkles Route   =~   GET Controller actions Controller   =~   [POST|PUT|DELETE] Controller actions

When to use Ember?

When NOT to use Ember?

Part II : PatternLab Demo

Part II : PatternLab Demo

http://emberjs.jsbin.com/falat/8/edit?output

The Plan

The Plan

State a Goal Rails way (sometimes) Ember way GOTO 1

Getting Started

Getting Started

Starting With A Very Simple App

Rails

  # config/routes.rb
  Rails.application.routes.draw do
    root :to => 'static_pages#home'
  end
  
  <!-- app/views/static_pages/home.html.erb -->
  <h1>This app is useless!</h1>
  
  # app/controllers/static_pages_controller.rb
  class StaticPagesController < ApplicationController
  end
  

Ember

  App = Ember.Application.create();
  
  <script type="text/x-handlebars">
    <h2>Welcome to the Pattern Lab!</h2>
    <p>Your home for procedurally generated patterns.</p>
    <p>This is a small demo app built with Ember and Ember Data.</p>
  </script>
  
http://emberjs.jsbin.com/guyed/1/edit

Ember "magic" at work.

Only write the parts you need.

Ember will 'auto generate' any parts of the stack that you don't write

If you wanted to be explicit

  App = Ember.Application.create();

  App.Router.map(function(){
    this.route('index',{ path:'' })
  });
  
  App.IndexRoute = Ember.Route.extend({});

  App.IndexController = Ember.ObjectController.extend({});

  App.IndexView = Ember.View.extend({});

  <script type="text/x-handlebars" id="index">
    <h1>This app is verbose AND useless!</h1>
  </script>
  
http://emberjs.jsbin.com/boyim/3/edit

Don't write it if you don't need it!

Application layout

Rails

  <!--app/views/layouts/application.html.erb -->
  <html>
    <head>...</head>
    <body>

      <h1>This is the layout</h1>
      <hr>
      <%= yield %>

    </body>
  </html>
  
  <!-- app/views/static_pages/home.html.erb -->
  <h1>This app is useless!</h1>
  

Ember

  <script type="text/x-handlebars">
    <div class="header">
      <h1 id='title'>
        {{#link-to 'index'}}PatternLab{{/link-to}}
      </h1>
    </div>

    {{outlet}}
  </script>
 

  <script type="text/x-handlebars" id="index">
    <h2>Welcome to the Pattern Lab!</h2>
    <p>Your home for procedurally generated patterns.</p>
    <p>This is a small demo app built with Ember and Ember Data.</p>
  </script>
  
http://emberjs.jsbin.com/rumit/1/edit

Adding a page

Rails

  # config/routes.rb
  Rails.application.routes.draw do
    # ...
    get 'about' => 'static_pages#about', :as => :about
  end
  
  <!-- app/views/static_pages/about.html.erb -->
  <h1>This app has not gotten any more useful...</h1>
  
  <%= link_to 'About', about_path %>
  

Ember

  App.Router.map(function(){
    this.route('about');
  });
  
  <script type="text/x-handlebars" id="about">
    <h2>About</h2>
    <p>PatternLab is a small demo appliction of Ember.</p>
  </script>
  
  <script type="text/x-handlebars">
    <!-- ... -->
    {{#link-to 'about'}}About{{/link-to}}
    <!-- ... -->
  </script>
  
http://emberjs.jsbin.com/falat/3/edit

Rendering Partials

Rails

    <!-- in app/views/layouts/application.html.erb -->
    <%= render 'layouts/footer' %>
  
    <!-- app/views/layouts/_footer.html.erb -->
    <hr>&copy; OctoLabs
  

Ember

  <script type="text/x-handlebars">
    <!-- ... -->
    {{partial 'footer'}}
  </script>
  
  <script type="text/x-handlebars" id="_footer">
    <hr/>&copy; OctoLabs
  </script>
  
http://emberjs.jsbin.com/jevexo/1/edit

Displaying Data

Rails

  # app/controllers/static_pages_controller.rb
  class StaticPagesController < ApplicationController
    def about
      @about_data = {
        :version => "0.0.1"
      }
    end
  end
  
  <!-- app/views/static_pages/about.html.erb -->
  <h1>About</h1>
  <p>Version : <%= @about_data[:version] %></p>
  

Ember

  App.IndexRoute = Ember.Route.extend({
    model : function(){
      return {
        version : "0.0.1"
      };
    }
  });
  
  <script type="text/x-handlebars" id="index">
    <!-- ... -->
    <p>Version : {{version}}</p>
  </script>
  
http://emberjs.jsbin.com/wiyoh/1/edit

Data Binding

  <script type="text/x-handlebars" id="index">
    <!-- ... -->
    <p>Version : {{version}}</p>
    {{input value=version}}
  </script>
  
http://emberjs.jsbin.com/leyog/1/edit

Models

Object wrapper for data

Rails

  # in db/schema.rb
  create_table "patterns" do |t|
    t.integer   "p1"
    t.integer   "p2"
    t.integer   "p3"
    t.datetime "created_at",              :null => false
    t.datetime "updated_at",              :null => false
  end
  
  class Pattern < ActiveRecord::Base
    def name
      "#{p1}-#{p2}-#{p3}"
    end
  end
  

Rails

  resources :patterns
  
  class PatternsController < ApplicationController
    def index
      @patterns = Pattern.all
    end
  end
  
  <h2>Latest Patterns</h2>
  <ul>
    <% @patterns.each do |pattern| %>
      <li><%= pattern.name %></li>
    <% end %>
  </ul>
  

Ember

  App.Pattern = DS.Model.extend({
    p1 : DS.attr('number'),
    p2 : DS.attr('number'),
    p3 : DS.attr('number'),

    name : function(){
      return this.get('p1') + '-' +
             this.get('p2') + '-' +
             this.get('p3');
    }.property('p1','p2','p3')
  });
  
  App.Router.map(function() {
    // ...
    this.resource('patterns');
  });
  

Ember

  App.PatternsRoute = Ember.Route.extend({
    model : function(){
      return [this.store.createRecord('pattern',
        { p1 : 1, p2 : 2, :p3 : 5}
      )];
    }
  });
  
  <script type="text/x-handlebars" id="patterns">
    <h2>Latest Patterns</h2>
    <ul>
      {{#each}}
        <li>{{name}}</li>
      {{/each}}
    </ul>
  </script>
  
http://emberjs.jsbin.com/letok/1/edit

Fixture Adapter

  App.ApplicationAdapter = DS.FixtureAdapter;
  
  App.Pattern.reopenClass({
    FIXTURES: [
      { id: 1, p1 : 1, p2 : 2, p3 : 5},
      { id: 2, p1 : 42, p2 : 142, p3 : 242},
      { id: 3, p1 : 100, p2 : 200, p3 : 300}
    ]
  });
  
  App.PatternsRoute = Ember.Route.extend({
    model : function(){
      return this.store.find('pattern');
    }
  });
  
http://emberjs.jsbin.com/vibeza/2/edit

Dynamic routes & Nested routes and templates

Ember

  App.Router.map(function() {
    this.resource('patterns',function(){
      this.resource('pattern', { path: '/:pattern_id' });
    });
  });
  
  <script type="text/x-handlebars" id="pattern">
    <h3>{{name}}</h3>
  </script>
  
  <script type="text/x-handlebars" id="patterns">
    <h2>Latest Patterns</h2>
    <ul>
      {{#each}}
        <li>
          {{#link-to 'pattern' this}}{{name}}{{/link-to}}
        </li>
      {{/each}}
    </ul>
    {{outlet}}
  </script>
  
http://emberjs.jsbin.com/gekuw/1/edit

Rendering a pattern

Components

Pattern Display Component

  {{pattern-display pattern=model width="256" height="256"}}
  
  <script type="text/x-handlebars" id="components/pattern-display">
    <canvas {{bind-attr id=canvasId
                        width=width
                        height=height
    }}></canvas>
  </script>
  
Chromanin.js
  App.PatternDisplayComponent = Ember.Component.extend({
    drawPattern : function(){
      var ch = new Chromanin(256,this.get('canvasId')),
          p1 = this.get('pattern.p1'),
          p2 = this.get('pattern.p2'),
          p3 = this.get('pattern.p3');
      ch.initlayers(256,256);
      ch.checkerBoardLayer(0,16,16,p1,p2,p3,0,0,128);
      ch.sineDistort(0,0,0.100000001490116,p1,p2,p3);
      ch.addLayers(0,1,4,1,1);
      ch.addLayers(4,2,4,1,1);
      ch.addLayers(4,3,4,1,1);
      ch.writeCanvas();
    }
    // ...
  });
  
  App.PatternDisplayComponent = Ember.Component.extend({
    // ...
    didInsertElement : function(){
      this.drawPattern();
    },

    automaticUpdate : function(){
      Ember.run.next(this,function(){
        this.drawPattern();
      });
    }.observes('pattern','pattern.p1','pattern.p2','pattern.p3'),

    canvasId : function(){
      return "canvas-" + Math.floor((Math.random() * 1000000) + 1);;
    }.property('pattern.id')
  });
  
http://emberjs.jsbin.com/hatev/1/edit

Create a new pattern

  App.Router.map(function() {
    this.resource('patterns',function(){
      this.resource('pattern', { path: '/:pattern_id' });
      this.route('new');
    });
  });
  
  {{#link-to 'patterns.new'}}New Pattern{{/link-to}}
  
  <script type="text/x-handlebars" id="patterns/new">
    <h2>Create a new Pattern</h2>
    {{input value=p1 type='number'}}
    {{input value=p2 type='number'}}
    {{input value=p3 type='number'}}
    <button {{action randomize}}>Randomize</button>
    <button {{action savePattern}}>Save</button>
    <br/><br/>
    {{pattern-display pattern=model width="256" height="256"}}
  </script>
  
  App.PatternsNewRoute = Ember.Route.extend({
    model : function(){
      return this.store.createRecord('pattern');
    }
  });
  
  App.PatternsNewController = Ember.ObjectController.extend({
    actions : {
      savePattern : function(){
        this.transitionToRoute('pattern',this.get('model'));
      },
      randomize : function(){
        this.setProperties({
          p1 : Math.floor((Math.random() * 1000) + 1),
          p2 : Math.floor((Math.random() * 1000) + 1),
          p3 : Math.floor((Math.random() * 1000) + 1)
        });
      }
    }
  });
  
  
http://emberjs.jsbin.com/falat/8/edit

REST Adapter

  App.ApplicationAdapter = DS.RESTAdapter.extend({
    host : 'http://pattern-lab-api.herokuapp.com'
  });
  
  savePattern : function(){
    var _this = this;
    this.get('model').save().then(function(pattern){
      _this.transitionToRoute('pattern',pattern);
    });
  }
  
http://emberjs.jsbin.com/kimoyo/2/edit

Thanks For Watching!

@jagthedrummer

jeremy@octolabs.com

http://www.octolabs.com/ndclondon2014