2.2. Creating Cookbook  page     

We're going to create a basic cookbook application that lets us create, read, update, and delete (CRUD) recipes. It will look like:

JavaScriptMVC uses 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!

Generating an Application

To create your application, open a console window and navigate to your public directory. Run:

> js jquery\generate\app cookbook

This script creates an application folder and files. Here's what each file does:

cookbook/                // app for your folder
  cookbook.css           // css for your app
  cookbook.html          // a page for your app
  cookbook.js            // app file, loads other files
  controllers/           // plugins & widgets
  docs/                  // documentation
  fixtures/              // simulated ajax responses
  funcunit.html          // functional test page
  models/                // model & data layers
  qunit.html             // unit test page
  resources/             // 3rd party scripts
  scripts/               // command line scripts
    build.html           // html for build script
    build.js             // build script
    clean.js             // code cleaning / linting
    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
  views/                 // client side templates

Read Folder and File Organization for more information.

We'll use cookbook.html for our application. If you need to make another page for your app you can generate it:

> js jquery\generate\page cookbook index.html
Generating ... index.html

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.

Scaffolding Recipes

The scaffold generator creates all the code you need for simple Create-Read-Update-Delete (CRUD) functionality.
For our cookbook app, we want to make recipes. To scaffold recipes run the following in a console:

> js jquery\generate\scaffold Cookbook.Models.Recipe

Here's what each part does:

recipe_controller.js
Cookbook.Controllers.Recipe, like all Controllers, respond to events such as click and manipulate the DOM.
edit.ejs,init.ejs,list.ejs,show.ejs
Views are JavaScript templates for easily creating HTML.
recipe_controller_test.js
Tests the CRUD functionality of the user interface.
recipe.js
Cookbook.Models.Recipe model performs AJAX requests by manipulating services.
recipes.get
Fixtures simulate AJAX responses. This fixture responds to GET '/recipes'.
recipe_test.js
A unit test that tests Recipe model.

Including Scripts

After generating the scaffolding files, you must steal them in your application file. Open cookbook/cookbook.js and modify the code to steal your recipe controller and model as follows:

steal.plugins(    
    'jquery/controller',            
    'jquery/controller/subscribe',  
    'jquery/view/ejs',  
    'jquery/controller/view',           
    'jquery/model',                 
    'jquery/dom/fixture',           
    'jquery/dom/form_params')       
    .css('cookbook')    

    .resources()                    
    .models('recipe')                       
    .controllers('recipe')                  
    .views();
P.S. By default the app file loads the most common MVC components and a few other useful plugins.

To add your unit and functional tests, include them in your qunit.js and funcunit.js files.

cookbook/test/qunit/qunit.js

steal
  .plugins("funcunit/qunit", "cookbook")
  .then("cookbook_test","recipe_test")
P.S. qunit.js describes what scripts are loaded into qunit.html

cookbook/test/funcunit/funcunit.js

steal
 .plugins("funcunit")
 .then("cookbook_test","recipe_controller_test")
P.S. funcunit.js describes what scripts are loaded into funcunit.html

Run Cookbook

That's it. You've created a simple Cookbook application. Open cookbook/cookbook.html in a browser.

NOTICE: If you are having problems and using Chrome from the filesystem, it's because Chrome has an 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, star the issue and let google know you'd like Chrome to work on the filesystem!

Continue to Testing Cookbook or continue to read how this code works.

How it Works

The Cookbook application's functionality can be broken into 4 parts:

  • Loading scripts.
  • Get and show recipes and recipe form.
  • Create a recipe.
  • Delete a recipe.
  • Edit a recipe.

Lets see how this gets mapped to files in our Cookbook app.

Loading Scripts

In cookbook.html, you'll find a script tag like:

<script type='text/javascript' 
        src='../steal/steal.js?cookbook'>   
</script>

This does 2 things:

  • Loads the steal script.
  • Tells steal to load the cookbook app (at cookbook/cookbook.js) in development mode.

When cookbook/cookbook.js runs, it loads a bunch of plugins, then loads the generated controller and model.

Get and Show Recipes and Recipe Form.

When recipe_controller.js is loaded, it creates Cookbook.Controllers.RecipeController.

RecipeController extends controller and describes what events control recipe functionality.

Because RecipeController is a "document" controller (onDocument: true), it automatically listens on the document element for events described by it's prototype methods. The load method listens for the window onload event and calls RecipeController's load function.

The load function looks for a '#recipe' element in the page and creates one if not present. Then uses the Recipe model to retrieve a list of recipes and callback the list function.

In Recipe.findAll ....

An Ajax request is made to /recipe, but because the fixtures plugin is included, the ajax request is directed to //cookbook/fixtures/recipes.json.get. After the content is retrived from the fixture, new instances of Recipe are created with the wrapMany function and passed to the success callback.

P.S. Fixtures are awesome and help you develop while the slow-poke backend teams catch up. Once the service is ready you simply have to remove the fixtures plugin from your application file.

The success function is RecipeController's list method.
List replaces the "#recipe" element's html with the content rendered by the template in cookbook/views/recipe/init.ejs with the recipe's data.

cookbook/views/recipe/init.ejs draws out the outline of the recipe table and the recipe form. It uses the partial template 'views/recipe/list' to draw out the individual recipes.

Multiple partial templates are used because other functionality will resuse them.

Create a Recipe.

RecipeController listens for "form submit". It's important to note that document controllers only respond to events in an element that has an id that matches the name of the controller. In this case, RecipeController only responds to "form submit" events in "#recipe" element.

When the event happens, the formParams plugin is used to turn the name and description fields into an object like:

{
  name: "The entered name",
  description : "The entered description" 
}

These attributes are passed to create a new recipe. When save is called, Recipe model's create function is called with the recipe's attributes. In Recipe.create a post request is sent to "/recipes", but intercepted by the fixtures plugin. Instead, fixtures call back success with a JSON object that looks like:

{
  "id": 100,
  "name": "The entered name",
  "description" : "The entered description" 
}

Success is the new recipe instance's created function which updates the attributes of the recipe and publishes an OpenAjax "recipe.created" message.

"recipe.created subscribe" messages are listened for in RecipeController. Here, RecipeController uses the list template to insert the new recipe's html into the page.

Destroy a Recipe.

When a recipe's html "tr" element created, it is labeled with the recipe instance like this:

<tr <%= recipes[i]%> >

This code adds the following to the recipe element:

  • the "recipe" class name
  • a unique identifier to the class name: cookbookmodelsrecipe_5
  • the recipe instance to jQuery.data

Inside the tr, the destroy link look like this:

<a class="destroy">destroy</a>

Recipe controller listens for clicks on destroy in the
'.destroy click' action. if the person wants to destroy that recipe, it uses closest to find the first parent with className= 'recipe' and then gets back the model instance. With that instance, it calls destroy.

jQuery.Model.destroy calls Recipe.destroy with the id of the object to be destroyed. If successful, jQuery.Model.destroyed publishes a "recipe.destroyed" OpenAjax event. RecipeController listens for this event, then removes the element from the page.

PRO TIP: Use OpenAjax events instead of callback functions. This will help you a lot if you have a representation of the same instance in multiple places on the page. For example, if you have 2 todo lists with a shared todo. If that todo is deleted in one place, it will be removed in the other.

Edit Recipe

Edit starts out similar to destroy - RecipeController listens for ".edit click" and gets the recipe instance from model(). Then RecipeController replaces the tr's html with the rendered content of the edit template.

The edit template adds an Update and cancel. RecipeController listens for ".update click" and ".cancel click".

When ".update click" happens, the model instance is updated with the values in the input elements. This results in a call to Recipe.update which tries to send a put request to 'recipe/:id', but instead uses fixtures.

When the request complates, a "recipe.updated" message is published. RecipeController listens for these events, and uses the show template to render the updated content.

When ".cancel click" occurs, the tr's content is replaced using the show template.

Adding isTasty

I hate mushrooms. I'd like to know if a recipe is tasty (it doesn't have mushrooms) and list it in the Recipe's table. Here's how to do that:

Add isTasty to Cookbook.Models.Recipe

Add an isTasty method to the prototype object of Recipe model (at the end of recipe.js):

/* @Prototype */
{
  isTasty : function(){
    return !/mushroom/.test(this.name+" "+this.description)
  }
})

Adding an "is tasty" column

In cookbook/views/recipe/init.ejs add a th like this:

<% for(var attr in Cookbook.Models.Recipe.attributes){%>
    <% if(attr == 'id') continue;%>
    <th><%= attr%> </th>    
<%}%>
<th>Tasty?</th>
<th>Options</th>

In cookbook/views/recipe/show.ejs add a td like this:

<%for(var attribute in this.Class.attributes){%>
    <%if(attribute == 'id') continue;%>
    <td class='<%= attribute%>'>
            <%=this[attribute]%>
    </td>
<%}%>
<td><%= this.isTasty() %></td>
<td>
    <a href='javascript: void(0)' class='edit'>edit</a>
    <a href='javascript: void(0)' class='destroy'>destroy</a>
</td>

Reload your page. You should see the Tasty column. Add a recipe with mushrooms and Tasty? should be false.

Continue to Testing Cookbook.

© Jupiter IT - JavaScriptMVC Training and Support