@page creating Creating Cookbook @parent getstarted 0
We're going to create a basic cookbook application that lets us create, and delete recipes. It will look like:
@image tutorials/getstarted/Cookbook.png
JavaScriptMVC uses [steal.generate generator scripts] to assist you in setting up your application's files and folders. They make everything you need to fall into the pit of success!
To create your application, open a console window and navigate to your public directory. Run:
> js jmvc\generate\app cookbook
This script creates an application folder and files. Here's what each file does:
cookbook/ // folder for your app
cookbook.css // css for your app
cookbook.html // a page for your app
cookbook.js // app file, loads other files
docs/ // documentation
fixtures/ // simulated ajax responses
funcunit.html // functional test page
models/ // model & data layers
qunit.html // unit test page
scripts/ // command line scripts
build.html // html for build script
build.js // build script
clean.js // code cleaning / linting
crawl.js // generate search content
docs.js // create documentation
test/
funcunit // functional tests
cookbook_test.js // functional test
funcunit.js // loads functional tests
qunit/ // unit tests
cookbook_test.js // unit test
qunit.js // loads unit tests
We'll use cookbook.html for our application. If you need to make another page for your app you can generate it:
@codestart text
js jmvc\generate\page cookbook index.html Generating ... index.html @codeend
Or you add the steal script to an existing page
page followed by ?cookbook
like:
<script type='text/javascript'
src='../path/to/steal/steal.js?cookbook'>
</script>
If you open cookbook/cookbook.html, you'll see a JavaScriptMVC welcome screen.
@image tutorials/getstarted/Welcome.png
Open cookbook/cookbook.html
and you will find:
<script type='text/javascript'
src='../steal/steal.js?cookbook'>
</script>
This line loads [steal] and tells steal to
load cookbook/cookbook.js
. cookbook/cookbook.js
is
your application file. Open it and you will find:
steal(
'./cookbook.css', // application CSS file
'./models/models.js', // steals all your models
'./fixtures/fixtures.js', // sets up fixtures for your models
function(){ // configure your application
})
The application file loads and configures your applications resources. Currently, it's loading the app's css file, models and fixtures (there are no fixtures or models yet).
Now it's time to make some widgets, models, and fixtures that allow us to create and delete recipes!
We'll use the scaffold generator to quickly create:
- A Recipe model for CRUDing recipes on the server
- A Fixture for simulating a recipe service
- A widget for creating recipes
- A widget for listing and deleting recipes
To scaffold recipes run the following in the command-line console:
> js jmvc\generate\scaffold Cookbook.Models.Recipe
Here's what each part does:
recipe.js
Creates a recipe [can.Model model] that is used to create, retrieve, updated, and delete recipes on the server.
recipe_test.js
Tests the recipe model.
fixtures.js
The generator added code to simulate the Recipe Model's Ajax requests (You might not have a Recipe service).
recipe/create
This folder contains the code, demo page, and tests for a widget that creates Recipes.
recipe/list
This folder contains the code, demo page, and tests for a widget that lists recipes.
(steal added)
The generator will also list files that say "(steal added)". For example:
@codestart text cookbook/models/models.js (steal added) @codeend
The "(steal added)" means the generator is
adding a steal call to load
a generated file for you. For example,
cookbook/models/models.js
now steals your
recipe.js
model like:
steal('./recipe.js')
After the generator runs, your application file (cookbook.js
)
looks like:
steal(
'./cookbook.css', // application CSS file
'./models/models.js', // steals all your models
'./fixtures/fixtures.js', // sets up fixtures for your models
'cookbook/recipe/create',
'cookbook/recipe/list',
function(){
// set your application up
new Cookbook.Recipe.List('#recipes');
new Cookbook.Recipe.Create('#create');
})
You'll notice that it now loads cookbook/create
and cookbook/list
and then tries to add these widgets to the
#recipes
and #create
elements.
However, #recipes
and #create
elements do not
exist. All we have to do now is add them. Open cookbook/cookbook.html
and add a #recipes
ul and a #create
form
so it looks like:
<!DOCTYPE HTML>
<html lang="en">
<head>
<title>cookbook</title>
</head>
<body>
<h1>Welcome to JavaScriptMVC!</h1>
<ul id='recipes'></ul>
<form id='create' action=''></form>
<script type='text/javascript'
src='../steal/steal.js?cookbook'>
</script>
</body>
</html>
That's it. You've created a simple Cookbook application. Open cookbook/cookbook.html in a browser.
@image tutorials/getstarted/Cookbook.png
NOTICE: If you are having problems and using Chrome from the filesystem, it's because Chrome has an [http://code.google.com/p/chromium/issues/detail?id=47416 insanely restrictive AJAX policies on the filesystem].
Essentially, Chrome does not allow AJAX requests to files outside the html page's folder. JavaScriptMVC organizes your files into separate folders.
To fix this, just run JavaScriptMVC from a web server. Or, you can use another browser. Or you can add `--allow-file-access-from-files` to Chrome's start script.
If you're annoyed like we are, [http://code.google.com/p/chromium/issues/detail?id=47416 star the issue] and let google know you'd like Chrome to work on the filesystem!
Continue to [testing Testing Cookbook] or continue to read how this code works.
The Cookbook application can be broken into 5 parts:
- The Recipe Model
- The Recipe Fixture
- The Recipe Create control
- The Recipe List control
- The Cookbook application that puts it all together
cookbook/models/recipe.js
looks like:
steal('can/model', function(){
/**
* @class Cookbook.Models.Recipe
* @parent index
* @inherits can.Model
* Wraps backend recipe services.
*/
can.Model('Cookbook.Models.Recipe',
/* @Static */
{
findAll: "GET /recipes",
findOne : "GET /recipes/{id}",
create : "POST /recipes",
update : "PUT /recipes/{id}",
destroy : "DELETE /recipes/{id}"
},
/* @Prototype */
{});
})
This loads [jQuery.Model can.Model] and uses it to create a
Cookbook.Models.Recipe
class. This class lets us
create, retrieve, update, and delete models programmatically like:
create
// create a recipe instance
var recipe = new Cookbook.Models.Recipe({
name: 'Hot Dog',
description: 'nuke dog, put in bun'
})
// call save to create on the server
recipe.save()
retrieve
// get recipes from the server
Cookbook.Models.Recipe.findAll({}).done(function(recipes){
// do something with recipes
})
update
// update the properties of a created recipe
recipe.attrs({
name: 'Bratwurst',
description: 'nuke bratwurst, put in bun'
});
// call save to send updates to the server
recipe.save()
delete
// call destroy
recipe.destroy()
Of course, we don't have a server to make requests to. This is where fixtures come in.
[can.fixture Fixtures] intercept Ajax requests and simulate the response. They are a great tool that enables you to start work on the front end without a ready server.
Open cookbook/fixtures/fixtures.js
and you will this:
steal("can/util/fixture", function(){
var store = can.fixture.make(5, function(i, recipe){
return {
name: "recipe "+i,
description: "Model " + i
}
});
can.fixture('GET /recipes', store.findAll);
can.fixture('GET /recipes/{id}', store.findOne);
can.fixture('POST /recipes', store.create);
can.fixture('PUT /recipes/{id}', store.update);
can.fixture('DELETE /recipes/{id}', store.destroy);
})
The scaffold generator added this to simulate a server with 5 recipes. Read more about how this works on [can.fixture.make make's documentation page].
Open cookbook/recipe/create/create.html
in your
browser. This page demos the Cookbook.Recipe.Create control and
lets you create recipes. It lets us work on Cookbook.Recipe.Create
independent of the rest of the application.
Open cookbook/recipe/create/create.js
to
see the Cookbook.Recipe.Create control's code:
steal('can', 'can/control/view', 'cookbook/models', './views/init.ejs', function() {
/**
* @class Cookbook.Recipe.Create
* @parent index
* @inherits jQuery.Controller
* Creates recipes
*/
can.Control('Cookbook.Recipe.Create',
/** @Prototype */
{
init : function(){
this.element.html(this.view());
},
submit : function(el, ev){
ev.preventDefault();
var el = this.element;
el.find('[type=submit]').val('Creating...')
new Cookbook.Models.Recipe(el.formParams()).save().done(function() {
el.find('[type=submit]').val('Create');
el[0].reset()
});
}
})
});
This code uses [steal] to load dependencies and then creates a
Cookbook.Recipe.Create
controller. This creates
a cookbook_recipe_create
jQuery helper function that
can be called on a form element like:
new Cookbook.Recipe.Create('form#create')
When the jQuery plugin is called, the controller's init
method is called and runs
this.element.html(this.view());
This code renders the template at cookbook/recipe/create/views/init.ejs
into the controller's [can.Control.prototype.element element].
When the jQuery plugin is called controller also binds event handlers on the controller's element. In this case, it listens for "submit" events on the element.
When a submit event happens, it updates the submit button's text, then creates a new recipe.
Open cookbook/recipe/create/create.html
in your
browser. This page demos the Cookbook.Recipe.List control. It loads
Recipes from the server, lets you delete recipes, and it also
listens for recipes being created and adds them to the list.
Open cookbook/recipe/list/list.js
to
see the Cookbook.Recipe.Create control's code:
steal( 'can/control/view', 'can/view/ejs', 'can/model/elements',
'cookbook/models', './views/init.ejs', function(){
/**
* @class Cookbook.Recipe.List
* @parent index
* @inherits can.Control
* Lists recipes and lets you destroy them.
*/
can.Control('Cookbook.Recipe.List',
/** @Static */
{
defaults : {}
},
/** @Prototype */
{
init : function(){
Cookbook.Models.Recipe.findAll().done(can.proxy(this.list, this));
},
list : function(list) {
this.list = list;
this.element.html(this.view('init', list));
},
'.destroy click': function( el ){
if(confirm("Are you sure you want to destroy?")){
el.closest('.recipe').model().destroy();
}
},
"{Cookbook.Models.Recipe} created" : function(Recipe, ev, recipe){
this.list.push(recipe);
}
});
});
When the List control is added to the page, init
is called:
Cookbook.Models.Recipe.findAll().done(can.proxy(this.list, this));
This does the following:
Makes a findAll request to the Cookbook.Models.Recipe
model and when it returns executes the list
method
of the control. There we store the returned list and render cookbook/recipe/list/views/init.ejs
with the list data
using this.view('init', list)
.
init.ejs
looks like:
<% this.each(function(current) { %>
<li <%= current %>>
<h3>
<%= current.attr('name') %>
<a href='javascript://' class='destroy'>X</a>
</h3>
<p><%= current.attr('description') %></p>
</li>
<% }) %>
This iterates through the recipes retrieved from the server. For each
recipe, it creates an LI element and renders name
and description
using
live binding.
Notice that the view 'adds' each recipe instance to its LI element with:
<%= current %>
This adds the model to jQuery.data and sets a 'recipe' className on the LI element. We'll make use of this in a moment.
Destroying Recipes
Each recipe has a destroy link. When it is clicked on the list's
'.destroy click'
method is called:
if(confirm("Are you sure you want to destroy?")){
el.closest('.recipe').model().destroy();
}
This method checks if you want to destroy the method. If you do, it finds the parent 'recipe' element and gets back the model instance (that's in jQuery.data). It then calls [can.Model.prototype.destroy model's destroy] method.
When a model is destroyed, all occurrences will be removed from any list. Due to live binding the displayed list will update automatically.
Creating Recipes
When a recipe is created, a "created" event is triggered. The List control listens for this with:
"{Cookbook.Models.Recipe} created" : function(Recipe, ev, recipe){
this.list.push(recipe);
}
So, when a recipe is created, we just add it to the list we are currently displaying. Again, thanks to live binding the list will just update itself without having anything else to do.
The cookbook application loads both of these widgets and adds them to the page. When Cookbook.Recipe.Create creates a Recipe, it creates a 'created' event which Cookbook.Recipe.List listens for and adds that newly created recipe to its list of recipes.
Continue to [testing Testing Cookbook].