Skip to content

Instantly share code, notes, and snippets.

@aloysius-lim
Last active December 11, 2023 06:51
Show Gist options
  • Save aloysius-lim/c793c1383fb17e3f4410 to your computer and use it in GitHub Desktop.
Save aloysius-lim/c793c1383fb17e3f4410 to your computer and use it in GitHub Desktop.
Ember.js CRUD with Validation (ember-easyForm, ember-validations and Bootstrap)
window.App = Ember.Application.create();
App.ApplicationAdapter = DS.FixtureAdapter.extend({
namespace: 'ember-crud'
});
Ember.EasyForm.Config.registerWrapper('bootstrap', {
formClass: '',
fieldErrorClass: 'has-error',
inputClass: 'form-group',
errorClass: 'help-block error',
hintClass: 'help-block',
labelClass: ''
});
App.Router.map(function() {
this.resource('products', function() {
this.route('new');
this.route('show', {path: '/:product_id'});
this.route('edit', {path: '/:product_id/edit'});
});
});
App.Product = DS.Model.extend(Ember.Validations.Mixin, {
name: DS.attr('string'),
author: DS.attr('string'),
description: DS.attr('string'),
price: DS.attr('number'),
// To identify html tag for a Product.
htmlID: function() {
return 'product' + this.get('id');
}.property('id'),
validations: {
name: {
presence: true
},
price: {
presence: true,
numericality: {
greaterThanOrEqualTo: 0
}
}
}
});
App.resetFixtures = function() {
App.Product.FIXTURES = [
{
id: 1,
name: 'Ember.js in Action',
author: 'Joachim Haagen Skeie',
description: 'Ember.js in Action is a crisp tutorial that introduces the Ember.js framework and shows you how to build production-quality web applications.',
price: 44.99
},
{
id: 2,
name: 'Building Web Applications with Ember.js',
author: 'Jesse Cravens & Thomas Brady',
description: 'This guide provides example-driven instructions on how to develop applications using Ember, one of the most popular JavaScript frameworks available.',
price: 29.99
},
{
id: 3,
name: 'The Ember.js Way',
author: 'Brian Cardarella & Alex Navasardyan',
description: "Inspired by Addison-Wesley's classic The Rails Way series, The Ember.js Way crystallizes all that's been learned about Ember.js development into a start-to-finish approach that works.",
price: 39.99
},
{
id: 4,
name: 'Instant Ember.JS Application Development: How-to',
author: 'Marc Bodmer',
description: 'A practical guide that provides you with clear step-by-step examples. The in-depth examples take into account the key concepts and give you a solid foundation to expand your knowledge and your skills.',
price: 20.69
}
];
};
App.resetFixtures();
App.ProductsRoute = Ember.Route.extend({
model: function() {
return this.store.find('product');
},
actions: {
// Redirect to new form.
new: function() {
this.transitionTo('products.new');
},
// Redirect to edit form.
edit: function(product) {
this.transitionTo('products.edit', product);
},
// Save and transition to /products/:product_id only if validation passes.
save: function(product) {
var _this = this;
product.validate().then(function() {
product.save();
_this.transitionTo('products.show', product);
});
},
// Roll back and transition to /products/:product_id.
cancel: function(product) {
product.rollback();
this.transitionTo('products.show', product);
},
// Delete specified product.
delete: function(product) {
product.destroyRecord();
this.transitionTo('products');
}
}
});
App.ProductsController = Ember.ArrayController.extend({
productsCount: function() {
return this.get('model.length');
}.property('@each')
});
App.ProductsIndexRoute = Ember.Route.extend({
model: function() {
return this.modelFor('products');
}
});
App.ProductsIndexController = Ember.ArrayController.extend({
needs: ['products'],
sortProperties: ['name']
});
App.ProductsEditRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('product', params.product_id);
},
// 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.edit');
if (model && model.get('isDirty') && !model.get('isSaving')) {
model.rollback();
}
}
});
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();
}
}
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ember.js CRUD Example</title>
<!-- Bootstrap -->
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.2.0/css/bootstrap.min.css">
<!-- QUnit -->
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/qunit/1.14.0/qunit.min.css">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<div id="qunit-tests"></div>
<script type="text/x-handlebars">
<div id="application" class="container-fluid">
<div class="row">
<div id="left-nav" class="col-md-3" style="background-color: #eeeeee">
<ul class="nav nav-pills nav-stacked sidebar">
{{#link-to "index" tagName="li"}}{{#link-to "index"}}Dashboard{{/link-to}}{{/link-to}}
{{#link-to "products" tagName="li"}}{{#link-to "products"}}Products{{/link-to}}{{/link-to}}
</ul>
</div>
<div id="main" class="col-md-9">
{{outlet}}
</div>
</div>
</div>
</script>
<script type="text/x-handlebars" data-template-name="index">
<div class="jumbotron">
<h1>Welcome!</h1>
<p>This app demonstrates CRUD with validation in Ember.js</p>
</div>
</script>
<script type="text/x-handlebars" data-template-name="products">
<h1>Products</h1>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="products/index">
<table id="products_table" class="table">
<thead>
<tr>
<th>Name</th>
<th>Author</th>
<th>Price</th>
<th><button type="button" class="btn btn-default new-button" {{action "new"}}><span class="glyphicon glyphicon-plus"></span></button></th>
</tr>
</thead>
<tbody>
{{#each product in model}}
<tr {{bind-attr id=product.htmlID}}>
<td class="name">{{#link-to "products.show" product}}{{product.name}}{{/link-to}}</td>
<td class="author">{{product.author}}</td>
<td class="price">{{product.price}}</td>
<td class="action-buttons"><button type="button" class="btn btn-default edit-button" {{action "edit" product}}><span class="glyphicon glyphicon-pencil"></span></button>
<button type="button" class="btn btn-default delete-button" {{action "delete" product}}><span class="glyphicon glyphicon-remove"></button></td>
</tr>
{{/each}}
</tbody>
</table>
<p id="products_count">Total: {{controllers.products.productsCount}} products.</p>
</script>
<script type="text/x-handlebars" data-template-name="products/show">
<h2><span class="name">{{name}}</span></h2>
<p>By <span class="author">{{author}}</span></p>
<p><strong>$<span class="price">{{price}}</span></strong></p>
<p><span class="description">{{description}}</span></p>
<p class="action-buttons"><button type="button" class="btn btn-default edit-button" {{action "edit" model}}><span class="glyphicon glyphicon-pencil"></span></button>
<button type="button" class="btn btn-default delete-button" {{action "delete" model}}><span class="glyphicon glyphicon-remove"></button></p>
<p>{{#link-to 'products' class="index-link"}}Back to products index{{/link-to}}</p>
</script>
<script type="text/x-handlebars" data-template-name="products/edit">
<h2>{{#if isNew}}New{{else}}Edit{{/if}} Product</h2>
{{#form-for controller id="form-product" wrapper="bootstrap"}}
{{#input name}}
{{label-field name text="Product"}}
{{input-field name class="form-control" autofocus="true"}}
{{#if view.showError}}
{{error-field name}}
{{/if}}
{{/input}}
{{#input author}}
{{label-field author text="Author"}}
{{input-field author class="form-control"}}
{{#if view.showError}}
{{error-field author}}
{{/if}}
{{/input}}
{{#input description}}
{{label-field description text="Description"}}
{{input-field description class="form-control"}}
{{#if view.showError}}
{{error-field description}}
{{/if}}
{{/input}}
{{#input price}}
{{label-field price text="Price"}}
{{input-field price class="form-control"}}
{{#if view.showError}}
{{error-field price}}
{{/if}}
{{/input}}
<button type="submit" class="btn btn-primary save-button" {{action "save" model}}>Save</button>
{{#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}}
{{/form-for}}
</script>
<!-- jQuery -->
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<!-- Handlebars -->
<script src="http://cdnjs.cloudflare.com/ajax/libs/handlebars.js/1.3.0/handlebars.min.js"></script>
<!-- Ember.js -->
<script src="http://cdnjs.cloudflare.com/ajax/libs/ember.js/1.5.1/ember.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/ember-data.js/1.0.0-beta.8/ember-data.min.js"></script>
<!-- ember-easyForm -->
<script src="http://builds.dockyard.com.s3.amazonaws.com/ember-easyForm/canary/shas/add6a40a68c8b081557a9011cf99ea255414e1b1/ember-easyForm.min.js"></script>
<!-- ember-validations -->
<script src="http://builds.dockyard.com.s3.amazonaws.com/ember-validations/canary/shas/2ff28c6ba4d227b0f863b327cdff23c381c37afb/ember-validations.min.js"></script>
<!-- Bootstrap -->
<script src="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.2.0/js/bootstrap.min.js"></script>
<!-- QUnit -->
<script src="http://cdnjs.cloudflare.com/ajax/libs/qunit/1.14.0/qunit.min.js"></script>
<!-- JavaScript files for our application and tests -->
<script src="app.js"></script>
<script src="tests.js"></script>
</body>
</html>
App.rootElement = '#qunit-tests';
App.setupForTesting();
App.injectTestHelpers();
module('Integration: Index', {
setup: function() {
App.reset();
}
});
test('app test', function() {
expect(1);
visit('/').then(function() {
equal(find('h1').text(), 'Welcome!');
});
});
module('Integration: Products Index', {
setup: function() {
App.reset();
App.resetFixtures();
}
});
test('products renders', function() {
expect(5);
visit('/products').then(function() {
equal(find('h1').text(), 'Products');
equal(find('table#products_table').length, 1);
equal(find('table#products_table thead tr th').length, 4);
var productCount = find('table#products_table tbody tr').length;
equal(productCount, App.Product.FIXTURES.length);
equal(find('p#products_count').text(), 'Total: ' + productCount + ' products.');
});
});
test('delete button works', function() {
expect(4);
var productID = 1;
visit('/products').then(function() {
var productCount = find('table#products_table tbody tr').length;
equal(productCount, App.Product.FIXTURES.length);
click('#product' + productID + ' .delete-button').then(function() {
var newproductCount = find('table#products_table tbody tr').length;
equal(App.Product.FIXTURES.length, productCount - 1);
equal(newproductCount, productCount - 1);
equal(find('#product' + productID).length, 0);
});
});
});
test('show link works', function() {
expect(3);
var productID = 3;
visit('/products').then(function() {
click('#product' + productID + ' td.name a').then(function() {
equal(currentRouteName(), 'products.show');
equal(currentPath(), 'products.show');
equal(currentURL(), '/products/' + productID);
});
});
});
test('edit button works', function() {
expect(3);
var productID = 2;
visit('/products').then(function() {
click('#product' + productID + ' .edit-button').then(function() {
equal(currentRouteName(), 'products.edit');
equal(currentPath(), 'products.edit');
equal(currentURL(), '/products/' + productID + '/edit');
});
});
});
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.
});
});
});
module('Integration: Products Show', {
setup: function() {
App.reset();
App.resetFixtures();
}
});
test('product renders', function() {
expect(7);
var productID = 1;
visit('/products/' + productID).then(function() {
equal(currentRouteName(), 'products.show');
equal(currentPath(), 'products.show');
equal(currentURL(), '/products/' + productID);
var product = App.Product.FIXTURES.findBy('id', productID.toString());
equal(find('h2').text(), product.name);
equal(find('.author').text(), product.author);
equal(find('.price').text(), product.price);
equal(find('.description').text(), product.description);
});
});
test('delete button works', function() {
expect(4);
var productID = 2;
visit('/products/' + productID).then(function() {
click('.delete-button').then(function() {
equal(currentRouteName(), 'products.index');
equal(currentPath(), 'products.index');
equal(currentURL(), '/products');
equal(find('#product' + productID).length, 0);
});
});
});
test('index link works', function() {
expect(3);
var productID = 4;
visit('/products/' + productID).then(function() {
click('.index-link').then(function() {
equal(currentRouteName(), 'products.index');
equal(currentPath(), 'products.index');
equal(currentURL(), '/products');
});
});
});
test('edit button works', function() {
expect(3);
var productID = 3;
visit('/products/' + productID).then(function() {
click('.edit-button').then(function() {
equal(currentRouteName(), 'products.edit');
equal(currentPath(), 'products.edit');
equal(currentURL(), '/products/' + productID + '/edit');
});
});
});
module('Integration: Products Edit', {
setup: function() {
App.reset();
App.resetFixtures();
}
});
test('product edit renders', function() {
expect(10);
var productID = 1;
visit('/products/' + productID + '/edit').then(function() {
equal(find('h1').text(), 'Products');
equal(find('h2').text(), 'Edit Product');
equal(find('form#form-product').length, 1);
var product = App.Product.FIXTURES.findBy('id', productID.toString());
equal(find('div.name input').val(), product.name);
equal(find('div.author input').val(), product.author);
equal(find('div.description input').val(), product.description);
equal(find('div.price input').val(), product.price);
equal(find('form#form-product button.save-button').length, 1);
equal(find('form#form-product button.cancel-button').length, 1);
equal(find('form#form-product button.delete-button').length, 1);
});
});
test('save button works', function() {
expect(7);
var productID = 2;
visit('/products/' + productID + '/edit').then(function() {
var name = find('div.name input').val();
var author = find('div.author input').val();
var description = find('div.description input').val();
var price = find('div.price input').val();
fillIn('div.name input', name + 'test');
fillIn('div.author input', author + 'test');
fillIn('div.description input', description + 'test');
fillIn('div.price input', '1' + price);
click('#form-product .save-button').then(function() {
equal(currentRouteName(), 'products.show');
equal(currentPath(), 'products.show');
equal(currentURL(), '/products/' + productID);
equal(find('.name').text(), name + 'test');
equal(find('.author').text(), author + 'test');
equal(find('.description').text(), description + 'test');
equal(find('.price').text(), '1' + price);
});
});
});
test('cancel button works', function() {
expect(7);
var productID = 3;
visit('/products/' + productID + '/edit').then(function() {
var name = find('div.name input').val();
var author = find('div.author input').val();
var description = find('div.description input').val();
var price = find('div.price input').val();
fillIn('div.name input', name + 'test');
fillIn('div.author input', author + 'test');
fillIn('div.description input', description + 'test');
fillIn('div.price input', '1' + price);
click('#form-product .cancel-button').then(function() {
equal(currentRouteName(), 'products.show');
equal(currentPath(), 'products.show');
equal(currentURL(), '/products/' + productID);
equal(find('.name').text(), name);
equal(find('.author').text(), author);
equal(find('.description').text(), description);
equal(find('.price').text(), price);
});
});
});
test('delete button works', function() {
expect(4);
var productID = 4;
visit('/products/' + productID + '/edit');
click('#form-product .delete-button').then(function() {
equal(currentRouteName(), 'products.index');
equal(currentPath(), 'products.index');
equal(currentURL(), '/products');
equal(find('#product' + productID).length, 0);
});
});
test('navigating away rolls back changes', function() {
expect(7);
var productID = 3;
visit('/products/' + productID + '/edit').then(function() {
var name = find('div.name input').val();
var author = find('div.author input').val();
var description = find('div.description input').val();
var price = find('div.price input').val();
fillIn('div.name input', name + 'test');
fillIn('div.author input', author + 'test');
fillIn('div.description input', description + 'test');
fillIn('div.price input', '1' + price);
visit('/products/' + productID).then(function() {
equal(currentRouteName(), 'products.show');
equal(currentPath(), 'products.show');
equal(currentURL(), '/products/' + productID);
equal(find('.name').text(), name);
equal(find('.author').text(), author);
equal(find('.description').text(), description);
equal(find('.price').text(), price);
});
});
});
module('Integration: Products Edit Validations', {
setup: function() {
App.reset();
App.resetFixtures();
}
});
test('validator catches empty name field', function() {
expect(3);
var productID = 1;
visit('/products/' + productID + '/edit').then(function() {
fillIn('div.name input', '');
fillIn('div.price input', '1.00');
click('#form-product .save-button').then(function() {
equal(currentRouteName(), 'products.edit');
equal(currentPath(), 'products.edit');
equal(currentURL(), '/products/' + productID + '/edit');
visit('/products'); // To cancel edit.
});
});
});
test('validator catches empty price field', function() {
expect(3);
var productID = 1;
visit('/products/' + productID + '/edit').then(function() {
fillIn('div.name input', 'Title');
fillIn('div.price input', '');
click('#form-product .save-button').then(function() {
equal(currentRouteName(), 'products.edit');
equal(currentPath(), 'products.edit');
equal(currentURL(), '/products/' + productID + '/edit');
visit('/products'); // To cancel edit.
});
});
});
test('validator catches non-numeric price field', function() {
expect(3);
var productID = 1;
visit('/products/' + productID + '/edit').then(function() {
fillIn('div.name input', 'Title');
fillIn('div.price input', 'a');
click('#form-product .save-button').then(function() {
equal(currentRouteName(), 'products.edit');
equal(currentPath(), 'products.edit');
equal(currentURL(), '/products/' + productID + '/edit');
visit('/products'); // To cancel edit.
});
});
});
test('validator catches negative price field', function() {
expect(3);
var productID = 1;
visit('/products/' + productID + '/edit').then(function() {
fillIn('div.name input', 'Title');
fillIn('div.price input', '-1');
click('#form-product .save-button').then(function() {
equal(currentRouteName(), 'products.edit');
equal(currentPath(), 'products.edit');
equal(currentURL(), '/products/' + productID + '/edit');
visit('/products'); // To cancel edit.
});
});
});
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.
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment