Embering

on the shoulders of Giants


An introduction to Ember.js and surrounding technologies


@jeffreybiles

www.emberscreencasts.com

`import Ember from 'ember'`
`import BattleEvent from '../../models/battle-event'`

BattleModalController = Ember.Controller.extend
  needs: ['application', 'battle-queue']
  player: Ember.computed.alias('session.currentUser')
  battleQueue: Ember.computed.alias("controllers.battle-queue")
  enemyLost: false
  playerLost: false
  minigame: false
  selectMonster: false
  message: 'battle starting'
  messageShowing: false
  showingStats: Ember.computed 'minigame', 'selectMonster', ->
    !@get('minigame') and !@get('selectMonster')

  handleEnemyMonsterDefeat:->
    queue = @get('battleQueue')
    if @get('enemy.defeated')
      queue.addPriorityMessage("You have defeated #{@get('enemy.name')}.") unless @get("enemy.randomBattleArea")
      queue.addEvent(@eventify(this, 'exitBattle', {playerVictory: true}))
    else
      queue.addPriorityEvent(@eventify(@get('enemy'), 'switchToNextMonster'))
    expMessage = @get('playerMonster').gainExperience({defeated: @get('enemyMonster')})
    @handlePlayerMonsterGainingALevel() if @get('playerMonster.readyToLevel')
    queue.addPriorityMessage(expMessage)
    queue.addPriorityMessage("You defeated #{@get('enemyMonster.name')}.")
    

  handlePlayerMonsterGainingALevel: ->
    beforeStats = @get('playerMonster').statsHash()
    @get('playerMonster').levelUp()
    afterStats = @get('playerMonster').statsHash()
    monsterName = @get('playerMonster.name')
    @get('battleQueue').addPriorityMessage("<table class='level-up-table margin-auto'>
        <tr>
          <td>Health</td>
          <td><span class='icon-heart'></span></td>
          <td class='before-stats'>#{beforeStats['health']}</td>
          <td>→</td>
          <td class='after-stats'>#{afterStats['health']}</td>
        </tr>
        <tr>
          <td>Strength</td>
          <td>⚔</td>
          <td class='before-stats'>#{beforeStats['strength']}</td>
          <td>→</td>
          <td class='after-stats'>#{afterStats['strength']}</td>
        </tr>
        <tr>
          <td>Defense</td>
          <td>⛨</td>
          <td class='before-stats'>#{beforeStats['defense']}</td>
          <td>→</td>
          <td class='after-stats'>#{afterStats['defense']}</td>
        </tr>
      </table>")
    @get('battleQueue').addPriorityMessage("#{monsterName} gained a level!!!!  +1<span class='icon-muscle'></span><br>#{monsterName} is now level #{@get('playerMonster.level')}", 'soundfx/victory')

  handlePlayerMonsterDefeat: ->
    queue = @get('battleQueue')
    if @get('player.defeated')
      queue.addPriorityMessage("You have been defeated.")
      queue.addEvent(@eventify(this, 'exitBattle', {playerVictory: false}))
      @get("preloader").play('soundfx/loss')
      @transitionToRoute('levels.try-again')
    else
      switchToMonsterSelect = @eventify(this, 'toggleMonsterSelect')
      queue.addPriorityEvent(switchToMonsterSelect)
    queue.addPriorityMessage("#{@get('playerMonster.name')} knocked out.")
        
  battleOver: Ember.computed.or 'enemy.defeated', 'player.defeated'
  exitBattle: ({playerVictory}) ->
    # analyticsStats = {
    #   challengeId: @get('challengeLink.challenge.id')
    #   challengeName: @get('challengeLink.challenge.name')
    #   subchallengeId: @get('challengeLink.orderedItem.id')
    #   index: @get('challengeLink.currentIndex')
    #   currentMonsterId: @get("playerMonster.id")
    #   currentMonsterLevel: @get("playerMonster.level")
    #   currentMonsterHealth: @get("playerMonster.currentHp")
    #   enemyMonsterLevel: @get("enemyMonster.level")
    #   enemyMonsterTypeId: @get("enemyMonster.monsterType.id")
    #   enemyMonsterTypeName: @get("enemyMonster.monsterType.name")
    # }
    if playerVictory
      @get('analytics').track('gameplay', 'progress', 'progress in challenge')
      if @get('enemy.actingChallengeOrdering.next')
        @get('enemy.challenge').nextItem()
      else
        # $.extend(analyticsStats, {category: 'gameplay', action: 'complete', label: 'complete challenge'})
        @get('analytics').track('gameplay', 'complete', 'complete challenge')
        @get("preloader").play('soundfx/victory')
        @get('enemy.challenge.battleRecord').updateWithResults({playerVictory: true})
        @transitionToRoute('level', @get("territory"))
    else
      @get('analytics').track('gameplay', 'exit', 'defeat or run')

    @get('enemy.monsters').forEach (item) -> item.heal()
    @get('player.monsters').forEach (item) -> item.heal()
    @get("player.monsters").forEach (item) -> item.save()
    @get('battleQueue').clear() #this is to clear the 'handleEnemyMonsterDefeat' events that the healing made happen
    @queueEnded()

    @send('closeModal')

  territory: Ember.computed.alias('enemy.challenge.territory')
  resultText: null
  playerMonster: Ember.computed.alias('player.currentMonster.content')
  enemyMonster: Ember.computed.alias('enemy.currentMonster')
  playerOptions: Ember.computed.alias('player.userOption')

  attack: ({attacker, defender, skill, isPlayer, isSuccessful}) ->
    return new Ember.RSVP.Promise (resolve, reject) =>
      if attacker.get('unconscious') or defender.get('unconscious')
        reject()
      else
        attack = @store.createRecord('attack', {attacker: attacker, defender: defender, skill: skill, isSuccessful: isSuccessful})

        attack.commence().then =>
          elementName = attack.get('skill.element.name')     
          if isSuccessful
            @get('preloader').image(elementName, '.attack-animation', !isPlayer)

          @get('battleQueue').displayMessage("#{attack.get('attacker.name')} uses #{attack.get('skill.name')}")
          @get('preloader').play("soundfx/#{elementName.toLowerCase()}-attack")
          Ember.run.later(_this, ->
            attack.doDamage()
            multiplier = attack.get('elementalMultiplier')
            if multiplier > 1
              messageModifier = "very effective <span class='icon-up'></span>"
            else if multiplier < 1
              messageModifier = "weak <span class='icon-down'></span>"
            else
              messageModifier = "successful ⬌"
            message = "#{attacker.get('name')}'s #{elementName} attack is #{messageModifier} against #{defender.get('element.name')}"

            defendingMonster = if isPlayer then 'enemy-monster' else 'player-monster'
            $(".#{defendingMonster}.monster").effect('shake', distance: 10)

            @handleEnemyMonsterDefeat() if @get("enemyMonster.unconscious")
            @handlePlayerMonsterDefeat() if @get("playerMonster.unconscious")

            @get('preloader').play("soundfx/hit")
            attack.record(this) if isPlayer
            resolve("#{message}  <br>#{attack.get('defender.name')} lost #{attack.get('damage')} health<br>-#{attack.get('damage')} <span class='icon-heart'></span>")
          , 1700)

        , => #rejection means a miss
          @get('preloader').play("soundfx/miss")
          attack.record(this) if isPlayer
          resolve("Attack missed.")

  eventify: (context, functionName, options={}) ->
    promiseAction = =>
      new Ember.RSVP.Promise (resolve, reject) =>
        fn = context[functionName]
        resolve(fn.bind(context)(options))
    return BattleEvent.create(action: promiseAction, options: options)

  toggleMonsterSelect: ->
    @set('selectMonster', !@get('selectMonster'))
    "I choose you!"

  queueEnded: ->
    @set('messageShowing', false)

  actions:
    exitBattle: ->
      @exitBattle(playerVictory: false)
      @transitionToRoute('level', @get('territory'))

    beginMinigame: (skill) ->
      @set('deployedSkill', skill)
      @set('minigame', true)
    useSkill: (playerIsCorrect) ->
      @set('minigame', false)
      @set('messageShowing', true)
      queue = @get('battleQueue')
      playerAttack = @eventify this, 'attack', 
            attacker: @get('playerMonster') 
            defender: @get('enemyMonster') 
            skill: @get('deployedSkill')
            isPlayer: true
            isSuccessful: playerIsCorrect
      queue.addEvent(playerAttack)
      enemyAttack = @eventify this, 'attack', 
            attacker: @get('enemyMonster')
            defender: @get('playerMonster') 
            isPlayer: false
            isSuccessful: true
      queue.addEvent(enemyAttack)
      Ember.run.later =>
        queue.start()
    toggleMonsterSelect: ->
      if !@get('selectMonster') and (@get('messageShowing') or @get('minigame'))
        alert("Please wait until the end of your turn to change monsters.")
      else
        if !@get('playerOptions.tutorialChangeCurrentMonster')
          @set('playerOptions.tutorialChangeCurrentMonster', true)
          @get('playerOptions').save()
        @toggleMonsterSelect()

    volumeToggle: ->
      @get("preloader").toggleVolume()

  highlightCurrentSkill: ( ->
    $('.current-skill-button').toggleClass("highlighted")
    $('.shiny').toggleClass("highlighted")
    setTimeout((@highlightCurrentSkill).bind(this), 1500)
  ).on 'init'
`export default BattleModalController`

Too little time

for one technology

Just enough time

for many technologies

Philosophy

If I have seen further it is by

standing on the shoulders of giants.

--Isaac Newton

If I have made awesome web apps  it is by

Embering on the shoulders of Giants

--Not Isaac Newton

NAND gates

Microchips

Assembly Language

C

Javascript

jQuery

Ember

C language

Ember

javascript

jQuery

Angular

3d engines

openGL

CAD

3d game engine

Climbing the

tree of giants

  1. Select the right stack
  2. Climb
  3. Keep Climbing

until you reach the level of complexity directly below that which you wish to build

NAND gates

Microchips

Assembly Language

C

Javascript

jQuery

Ember

You might not

need jQuery

You might not

need Assembly Language

Reductio Ad Absurdum

The view from the 

top

C language

jQuery

Ember

javascript

jQuery

Angular

3d engines

3d game engine

openGL

CAD

Custom Web Framework

OR

ember-cli

npm install -g ember-cli
ember new giants-app
  1. Package manager (npm and bower)
  2. Directory structure and file naming conventions
  3. Build system (broccoli)
  4. jsHint on every run
  5. Livereload
  6. Module system (ES6 modules)
  7. ES6 transpilation
  8. Generators
  9. Test framework
  10. Addon system

Package Managers

File and directory structure

VS

Build System

module.exports = function(grunt) {

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      options: {
        separator: ';'
      },
      dist: {
        src: ['src/**/*.js'],
        dest: 'dist/<%= pkg.name %>.js'
      }
    },
    uglify: {
      options: {
        banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n'
      },
      dist: {
        files: {
          'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
        }
      }
    },
    qunit: {
      files: ['test/**/*.html']
    },
    jshint: {
      files: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
      options: {
        // options here to override JSHint defaults
        globals: {
          jQuery: true,
          console: true,
          module: true,
          document: true
        }
      }
    },
    watch: {
      files: ['<%= jshint.files %>'],
      tasks: ['jshint', 'qunit']
    }
  });

  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-contrib-qunit');
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-contrib-concat');

  grunt.registerTask('test', ['jshint', 'qunit']);

  grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']);

};
var EmberApp = require('ember-cli/lib/broccoli/ember-app');

var app = new EmberApp();

//custom imports
app.import("bower_components/jquery.cookie/jquery.cookie.js");
app.import("bower_components/moment/moment.js");
app.import('bower_components/bootstrap/dist/js/bootstrap.js');
app.import('bower_components/pickadate/lib/legacy.js');
app.import('bower_components/pickadate/lib/picker.js');
app.import('bower_components/pickadate/lib/picker.date.js');
app.import('bower_components/pickadate/lib/picker.time.js');

module.exports = app.toTree();

LiveReload

Module System

(ES6 modules)

ES6 Transpilation

+

Generators

ember generate component giant-tree
//app/components/giant-tree.js
import Ember from 'ember';

export default Ember.Component.extend({
});
//app/templates/components/giant-tree.hbs
{{yield}}
//tests/unit/components/giant-tree-test.js
import {
  moduleForComponent,
  test
} from 'ember-qunit';

moduleForComponent('giant-tree', {
  // Specify the other units that are required for this test
  // needs: ['component:foo', 'helper:bar']
});

test('it renders', function(assert) {
  assert.expect(2);

  // Creates the component instance
  var component = this.subject();
  assert.equal(component._state, 'preRender');

  // Renders the component to the page
  this.render();
  assert.equal(component._state, 'inDOM');
});

Testing

ember test          //run once
ember test --server //run on filechange
ember g acceptance-test standing-on-giant
//tests/acceptance/standing-on-giant-test.js
import Ember from 'ember';
import {
  module,
  test
} from 'qunit';
import startApp from 'enterprise/tests/helpers/start-app';

var application;

module('Acceptance: StandingOnGiant', {
  beforeEach: function() {
    application = startApp();
  },

  afterEach: function() {
    Ember.run(application, 'destroy');
  }
});

test('visiting /standing-on-giant', function(assert) {
  visit('/standing-on-giant');

  andThen(function() {
    assert.equal(currentURL(), '/standing-on-giant');
  });
});

Addons

ember install ember-cli-mocha

ember-cli

npm install -g ember-cli
ember new giants-app
  1. Package manager (npm and bower)
  2. Directory structure and file naming conventions
  3. Build system (broccoli)
  4. jsHint on every run
  5. Livereload
  6. Module system (ES6 modules)
  7. ES6 transpilation
  8. Generators
  9. Test framework
  10. Addon system

ember.js

  1. Separation of Concerns
  2. Object Model (Classes, Inheritance, Mixins)
  3. Router (no more broken back button)
  4. Observers
  5. Computed Properties (functional reactive programming)
  6. Isolated Components
  7. Container
  8. Templating system
  9. Hundreds of tiny defaults decisions made for your convenience

Separation of Concerns

VS

Object System

Objects     vs  Prototypes

Objects     vs  Prototypes

  • 26,477 results
  • Many up-to-date, written by top authors
  • Deep literature on how to write object-oriented software
  • 787 results
  • 3rd listing is from 1999
  • 4th listing is a shoe
  • 1st and 2nd listing are titled "You don't know JS"

No One Understands Prototypes

Ember.Object

with Classes, Inheritance, and Mixins

Routes

Observers

Computed Properties

ember.js

  1. Separation of Concerns
  2. Object Model (Classes, Inheritance, Mixins)
  3. Router (no more broken back button)
  4. Observers
  5. Computed Properties (functional reactive programming)
  6. Isolated Components
  7. Container
  8. Templating system (with data-binding)
  9. Run Loop (debouncing DOM changes)
  10. And more...

Moving

Outside

the Framework

ember-data

//models/giant.js
export default DS.Model.extend({
  name: DS.attr('string'),
  standingOnShouldersOf: DS.hasMany('giant'),  
})
//old jQuery way
var giant = {
  name: 'Hodor',
  standingOnShouldersOf: []
}
jQuery.ajax({
  url: '/api/giants',
  type: 'post',
  data: {giant: giant},
  dataType: 'json'
})
//Ember Data way
var giant = this.store.createRecord('giant', {
    name: 'Hodor'
});
giant.save();

animations

//in command line
ember install liquid-fire

//app/transitions.js
export default function(){
  this.transition(
    this.fromRoute('giants.index'),
    this.toRoute('giants.show'),
    this.use('toLeft'),
    this.reverse('toRight')
  );

  ...
};

fastboot

ember install ember-cli-fastboot

What if there are no available giants?

BUILD ONE

This is a lot

of stuff you

don't

have to know

a bunch of stuff that Ember takes care of for you

a few new APIs that Ember introduces

Great Power

 

What will you build?

Great Vision

 

Please Ember Responsibly

 

@emberscreencast

www.emberscreencasts.com

jeffrey@emberscreencasts.com

www.emberscreencasts.com

Embering on the shoulders of Giants

By Jeffrey Biles

Embering on the shoulders of Giants

  • 1,902