Now we're going to build on that scaffolding to create a simple web app, adding modules as necessary as we go.
So we need a spec for our web app; lets go with something simple from Codingdojo's Kata Catalogue. The Roman Numeral converter is a nice simple one.
First we'll lay out a quick and dirty UI.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div> | |
<form id="converter"> | |
<input type="text" id="field1"/> | |
<button id="convert">Convert to Roman Numeral</button> | |
</form> | |
<span id="output"></span> | |
</div> |
Let's start with qwery - a selector module - to get at our target elements. Install it, and add to app.js
npm install qwery
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var qwery = require('qwery'); |
The first problem is that under mocha, qwery will not even load without errors. Obviously it will work in the browser, but we need to fake being a browser so that qwery will load.
ReferenceError: document is not defined
Fortunately there's another module that can help us out here - jsdom. Install it and add it to the appTest.js to fake the global document object.
npm install jsdom
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var jsdom = require('jsdom').jsdom; | |
document = jsdom( '<html/>' ); |
Now that we actually want to start using qwery we find that it is not structured in such a way as to make it easy to break our dependency on it. The same is true of many other browser modules. So what do we do? We store a local variable that references qwery and use this to break the dependency in our app code, using sinon to provide our stub interface.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var App = { | |
selector = qwery, | |
init: function( callback ) { | |
... | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
beforeEach(function(done){ | |
app.selector = sinon.stub(); | |
done(); | |
}); |
So now we have selector code and a fake selector. If we want to make it do something useful then we need a DOM manipulator. Enter bonzo, a lightweight DOM manipulation utility. Qwery and bonzo are both included as part of a set of utilities called ender. The names of the modules mostly come from characters in Orson Scott Card's book "Ender's Game", which is quite a good read.
npm install bonzo
Bonzo needs a little bit more faking of browser environment, and similar dependency breaking code.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var bonzo = require('bonzo'); | |
var App = { | |
... | |
dom: bonzo, | |
... | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
window = jsdom.createWindow(); | |
navigator = { userAgent: '' } | |
... | |
// in the beforeEach function call | |
app.dom = sinon.stub(); | |
... |
Now we need to be very careful about what our fake selector and DOM tool return, we need to make sure that it is correct. So lets look at it in a browser - www.javascriptoo.com offers a useful service that you can use to check the API. You can edit the html in the left hand window and the results will be shown on the right.
So our fake selector output is just going to be passed to the DOM tool, but the DOM tool's output needs to include stub functions that we will call.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
it( 'fetchInput fetches input value using selector+dom', function( done ) { | |
app.selector.returns( [{}] ); | |
app.dom.returns( { val: function(){ return '7'; } } ); | |
app.fetchInput( function(value) { | |
assert.equal( value, '7' ); | |
done(); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var App = { | |
... | |
fetchInput: function( callback /*value*/ ){ | |
callback( this.dom( this.selector('#field1') ).val() ); | |
}, | |
... | |
} |
Green test, yay!
But now we can't find qwery or bonzo to include it in the build process...
Tracing dependencies for: app
Error: ENOENT, no such file or directory '.../src/qwery.js'
Symbolic links to the minimized node module versions in src should do the trick.
cd src
ln -s ../node_modules/qwery/qwery.min.js qwery.js
ln -s ../node_modules/bonzo/bonzo.min.js bonzo.js
cd ..
Run the build process again, and now we have it in the browser - lets make a couple of tweaks to prove it works there.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div> | |
<form id="converter"> | |
<input type="text" id="field1" value="12"/> | |
<button id="convert">Convert to Roman Numeral</button> | |
</form> | |
<span id="output"></span> | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
... | |
App.init(function(){ | |
App.fetchInput(function(value){ | |
console.log( "Field contains " + value ); | |
}); | |
}); | |
... |
So now that works - and you get a console message when you load the page in a browser.
Field contains 12
To finish off with the DOM manipulation we need to do two more things; set up a function that inserts the output into the DOM, and cause the init method to attach a click handler to the button that does the work, then we can crack on with the logic of the problem itself.
Writing the output is more of the same.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
it( 'writeOutput saves value using selector+dom', function( done ) { | |
app.selector.returns( [{}] ); | |
var textFunc = sinon.spy(); | |
app.dom.returns( { text: textFunc } ); | |
app.writeOutput( 'XVI', function(response) { | |
sinon.assert.calledWith( textFunc, 'XVI' ); | |
done(); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var App = { | |
... | |
writeOutput: function( value, callback ){ | |
callback( this.dom( this.selector('#output') ).text( value ) ); | |
} | |
... | |
} |
The click handling requires a new module, bean, which is just a simple event api. Install it and create a symlink for the front end.
npm install bean
cd src
ln -s ../node_modules/bean/bean.min.js bean.js
cd ..
We can replace the skeleton test for init that we made last time now. Note especially here the use of the bind call to ensure that we don't end up with this referring to the DOM element. Also bean only operates on single elements, not on sets of them.
So all that remains is to write the logic of the Roman Numeral conversion, and link up to it in the action function.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
... | |
// in beforeEach function | |
app.event = { | |
on: sinon.stub() | |
}; | |
app.action = { | |
bind: function(){ return 'ACTION'; } | |
} | |
... | |
it( 'init sets up action function as event handler for button click', function(done) { | |
app.selector.returns( [{}] ); | |
app.init( function() { | |
sinon.assert.calledWith( app.event.on, {}, 'click', 'ACTION' ); | |
done(); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var bean = require('bean'); | |
var App = { | |
... | |
event: bean, | |
action: function(event) { | |
console.log( "Action applied" ); | |
event.stop() | |
}, | |
init: function( callback ) { | |
this.event.on( this.selector('#convert')[0], 'click', this.action.bind(this) ); | |
callback(); | |
} | |
... | |
} |
Next time we'll run through the Roman Numeral code kata to finish off our little project, which will result in a working (if simple) web app.
First post in the series: here
Final post in the series: here
No comments:
Post a Comment