How To: CRUD with Validation in Ember.js Using ember-easyForm, ember-validations, and Boostrap

Step 7. Create New Product

The last step is to create the interface to create new products. This is an easy step, as we have done most of the work in Step 6. We will re-use as much as we can from before.

First, let us re-use the products/edit template for the New Product Form. We will need two minor changes. We need to change the title of the form from “Edit Product” to “New Product” if we are creating a new product. To do that, we will define a new property isNew later. For now, let us edit the template so that the correct title is displayed depending on the isNew property.

Edit index.html:

<!-- Change -->
<!-- <h2>Edit Product</h2> -->
<!-- to -->
<h2>{{#if isNew}}New{{else}}Edit{{/if}} Product</h2>

We also need to define a different set of buttons for the New Product form. We do not need a “Delete” button, and we want the “Cancel” button to destroy our new product record; otherwise, we will get stray records lingering in the data store.

Edit index.html:

<!-- Change -->
<!-- <button type="button" class="btn btn-default cancel-button" {{action "cancel" model}}>Cancel</button> -->
<!-- <button type="button" class="btn btn-danger delete-button" {{action "delete" model}}>Delete</button> -->
<!-- to -->
{{#if isNew}}
<button type="button" class="btn btn-default cancel-button" {{action "delete" model}}>Cancel</button>
{{else}}
<button type="button" class="btn btn-default cancel-button" {{action "cancel" model}}>Cancel</button>
<button type="button" class="btn btn-danger delete-button" {{action "delete" model}}>Delete</button>
{{/if}}

Next, let us define ProductsNewRoute. The model for this route will be a new, empty Product record. Every time a user enters this route, a new record is created. The record is then either saved into the data store if the user clicks the “Save” button, or destroyed if he cancels or navigates away. We will use the renderTemplate hook to render the products/edit template using ProductsNewController (automatically generated by Ember.js). Finally, we will implement the deactivate hook to destroy the new product record if the user navigates away from the form.

Append to app.js:

App.ProductsNewRoute = Ember.Route.extend({
  model: function() {
    return this.store.createRecord('product');
  },

  isNew: true,

  renderTemplate: function(controller, model) {
    this.render('products.edit', {
      controller: controller
    });
  },

  // Roll back if the user transitions away by clicking a link, clicking the
  // browser's back button, or otherwise.
  deactivate: function() {
    var model = this.modelFor('products.new');
    if (model && model.get('isNew') && !model.get('isSaving')) {
      model.destroyRecord();
    }
  }
});

We also need to add the new action to ProductsRoute so that the “New” button on /products works.

Add to the actions dictionary of ProductsRoute in app.js:

// Redirect to new form.
new: function() {
  this.transitionTo('products.new');
}

Finally, let us define the tests for the New Product form. They are very similar to the tests for Edit Product.

Add to tests.js under the “Integration: Products Index” module:

test('new button works', function() {
  expect(3);

  visit('/products').then(function() {
    click('.new-button').then(function() {
      equal(currentRouteName(), 'products.new');
      equal(currentPath(), 'products.new');
      equal(currentURL(), '/products/new');
      visit('/products'); // To cancel new.
    });
  });
});

Append to tests.js:

module('Integration: Products New', {
  setup: function() {
    App.reset();
    App.resetFixtures();
  }
});

test('product new renders', function() {
  expect(9);

  visit('/products/new').then(function() {
    equal(find('h1').text(), 'Products');
    equal(find('h2').text(), 'New Product');
    equal(find('form#form-product').length, 1);
    equal(find('div.name input').val(), '');
    equal(find('div.author input').val(), '');
    equal(find('div.description input').val(), '');
    equal(find('div.price input').val(), '');
    equal(find('form#form-product button.save-button').length, 1);
    equal(find('form#form-product button.cancel-button').length, 1);
    visit('/products'); // To cancel new
  });
});

test('save button works', function() {
  expect(6);

  visit('/products/new').then(function() {
    fillIn('div.name input', 'NameTest');
    fillIn('div.author input', 'AuthorTest');
    fillIn('div.description input', 'DescriptionTest');
    fillIn('div.price input', '999');
    click('#form-product .save-button').then(function() {
      equal(currentRouteName(), 'products.show');
      equal(currentPath(), 'products.show');
      equal(find('.name').text(), 'NameTest');
      equal(find('.author').text(), 'AuthorTest');
      equal(find('.description').text(), 'DescriptionTest');
      equal(find('.price').text(), '999');
    });
  });
});

test('cancel button works', function() {
  expect(4);

  visit('products').then(function() {
    var productCount = find('table#products_table tbody tr').length;
    visit('/products/new').then(function() {
      fillIn('div.name input', 'NameTest');
      fillIn('div.author input', 'AuthorTest');
      fillIn('div.description input', 'DescriptionTest');
      fillIn('div.price input', '999');
      click('#form-product .cancel-button').then(function() {
        equal(currentRouteName(), 'products.index');
        equal(currentPath(), 'products.index');
        equal(currentURL(), '/products');
        equal(find('table#products_table tbody tr').length, productCount);
      });
    });
  });
});

test('navigating away rolls back changes', function() {
  expect(4);

  visit('products').then(function() {
    var productCount = find('table#products_table tbody tr').length;
    visit('/products/new').then(function() {
      fillIn('div.name input', 'NameTest');
      fillIn('div.author input', 'AuthorTest');
      fillIn('div.description input', 'DescriptionTest');
      fillIn('div.price input', '999');
      visit('products').then(function() {
        equal(currentRouteName(), 'products.index');
        equal(currentPath(), 'products.index');
        equal(currentURL(), 'products');
        equal(find('table#products_table tbody tr').length, productCount);
      });
    });
  });
});



module('Integration: Products New Validations', {
  setup: function() {
    App.reset();
    App.resetFixtures();
  }
});

test('validator catches empty name field', function() {
  expect(3);

  visit('/products/new').then(function() {
    fillIn('div.name input', '');
    fillIn('div.price input', '1.00');
    click('#form-product .save-button').then(function() {
      equal(currentRouteName(), 'products.new');
      equal(currentPath(), 'products.new');
      equal(currentURL(), '/products/new');
      visit('/products'); // To cancel new.
    });
  });
});

test('validator catches empty price field', function() {
  expect(3);

  visit('/products/new').then(function() {
    fillIn('div.name input', 'Title');
    fillIn('div.price input', '');
    click('#form-product .save-button').then(function() {
      equal(currentRouteName(), 'products.new');
      equal(currentPath(), 'products.new');
      equal(currentURL(), '/products/new');
      visit('/products'); // To cancel new.
    });
  });
});

test('validator catches non-numeric price field', function() {
  expect(3);

  visit('/products/new').then(function() {
    fillIn('div.name input', 'Title');
    fillIn('div.price input', 'a');
    click('#form-product .save-button').then(function() {
      equal(currentRouteName(), 'products.new');
      equal(currentPath(), 'products.new');
      equal(currentURL(), '/products/new');
      visit('/products'); // To cancel new.
    });
  });
});

test('validator catches negative price field', function() {
  expect(3);

  visit('/products/new').then(function() {
    fillIn('div.name input', 'Title');
    fillIn('div.price input', '-1');
    click('#form-product .save-button').then(function() {
      equal(currentRouteName(), 'products.new');
      equal(currentPath(), 'products.new');
      equal(currentURL(), '/products/new');
      visit('/products'); // To cancel new.
    });
  });
});

And that is it! We now have a fully functioning CRUD interface with data validation built on Ember.js.

You can get the code for this step by running:

git checkout step-7
Advertisements

5 thoughts on “How To: CRUD with Validation in Ember.js Using ember-easyForm, ember-validations, and Boostrap

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s