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
- Select the right stack
- Climb
- 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
- Package manager (npm and bower)
- Directory structure and file naming conventions
- Build system (broccoli)
- jsHint on every run
- Livereload
- Module system (ES6 modules)
- ES6 transpilation
- Generators
- Test framework
- 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
- Package manager (npm and bower)
- Directory structure and file naming conventions
- Build system (broccoli)
- jsHint on every run
- Livereload
- Module system (ES6 modules)
- ES6 transpilation
- Generators
- Test framework
- Addon system
ember.js
- Separation of Concerns
- Object Model (Classes, Inheritance, Mixins)
- Router (no more broken back button)
- Observers
- Computed Properties (functional reactive programming)
- Isolated Components
- Container
- Templating system
- 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
- Separation of Concerns
- Object Model (Classes, Inheritance, Mixins)
- Router (no more broken back button)
- Observers
- Computed Properties (functional reactive programming)
- Isolated Components
- Container
- Templating system (with data-binding)
- Run Loop (debouncing DOM changes)
- 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