Best practice for instantiating Ember model on load (LocalStorage) - ember.js

I'm creating a basic store using ember with products and a shopping bag. I'm using LocalStorage as the adapter for the shopping bag so the user can come back to a cart with products they've added previously. At any one time, there should only be one shopping bag. Right now, I've set up a checker in the application route on activate to see if there's already a bag saved. If not, create one.
I also want to set-up the model correctly for it to be used in the controller and in templates. Here's my application route"
var ApplicationRoute = Ember.Route.extend({
activate: function() {
var store = this.store;
store.find('bag').then(function(bags) {
var existing_bag = bags.get('firstObject');
// If there isn't already a bag instantiated, make one and save it
if(typeof existing_bag === 'undefined') {
var new_bag = store.createRecord('bag');
new_bag.save();
}
});
},
model: function() {
return this.store.find('bag');
},
setupController: function(controller,model) {
controller.set('content', model);
}
});
export default ApplicationRoute;
Here is my bag model:
import DS from 'ember-data';
import Ember from "ember";
export default DS.Model.extend({
products: DS.hasMany('product', {async: true}),
bagProducts: DS.hasMany('bagProduct', {async: true}),
productCount: Ember.computed.alias('products.length')
});
In my controller, I'd like to check if the bag has products in it so I can display a product count:
import Ember from "ember";
export default Ember.ArrayController.extend({
bag: Ember.computed.alias("content"),
cartHasProducts: function() {
var bag = this.get('bag').objectAt('0');
return bag.get('productCount') > 0;
}.property('content.#each.productCount')
});
And my template:
<div id="bag" class="js-btn" {{action "showModal" 'bag-modal' model}}>
<i class="icon ion-bag"></i>
<p class="label">Your Bag</p>
{{#if controller.cartHasProducts}}
<div class="count">
<span>{{productCount}}</span>
</div>
{{/if}}
</div>
This works, but only after I use objectAt('0') to get the first instance of the bag. Shouldn't content just return the one instance of the bag? Is there a way to set up the model to just return the one current instance? What am I doing wrong?
I really really appreciate your help!

This could be a solution:
http://codepen.io/szines/pen/Aufmp?editors=101
(Should work with LocalStorage as well, above example uses FixtureAdapter.)
You can download both models in the store with RSVP, and in afterModel, you can check, bag already exist or not. If not, you can create one.
You can map your main model and your secondary model in setupController, so they gonna be exist in Controller and in views.
App.IndexRoute = Ember.Route.extend({
model: function() {
return Ember.RSVP.hash({
bagModel: this.store.find('bag'),
productList: this.store.find('product')
})
},
afterModel: function(model, transition) {
if (model.bagModel.get('length') < 1) {
model.bagModel = this.store.createRecord('bag');
}
},
setupController: function(controller, model) {
var productList = model.productList,
bagModel = model.bagModel;
model = model.bagModel;
this._super(controller, model);
controller.set('productList', productList);
}
});
Because you have only one bag as main model, better if you use ObjectController.
Controllers and models:
App.IndexController = Ember.ObjectController.extend({
productCount: Ember.computed.alias('model.products.length'),
actions: {
addProduct: function(product) {
this.get('model').get('products').pushObject(product);
}
}
});
App.Bag = DS.Model.extend({
products: DS.hasMany('product', {async: true}),
});
App.Product = DS.Model.extend({
name: DS.attr('string'),
});
App.Bag.FIXTURES = [];
App.Product.FIXTURES = [
{id: 1, name: 'First Product'},
{id: 2, name: 'Second Product'},
{id: 3, name: 'Third Product'}
]
and the index template:
Number of products: {{productCount}}
{{#each productList}}
<button {{action 'addProduct' this}}>Add {{name}}</button>
{{/each}}

Related

Sort model by id

I sideload products of a given category. The problem is that they are not sorted. I'd like to sort them by id and render the sorted products in a select.
How can I sort them?
app/category/model.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
products: DS.hasMany('product', { async: true })
});
route.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return {
category: this.store.find('category', 1)
};
}
});
template.hbs
{{view "select" prompt="All products"
content=model.category.products
optionLabelPath="content.name"
optionValuePath="content.name"
value=selectedProduct
class="form-control"}}
You can use a computed property and the Ember.SortableMixin to sort the products in your controller:
sortedProducts: Ember.computed('model.category.products', function() {
return Ember.ArrayProxy.createWithMixins(Ember.SortableMixin, {
sortProperties: ['id'],
sortAscending: true,
content: this.get('model.category.products')
});
})
And then simply use sortedProducts instead of model.category.products.
Source

Filter values in the controller

I want to filter products depending of the selected category which can be selected with a drop-down menu. products belongTo category
What do I have to change in the controller to filter products depending of the chosen value of the drop-down menu?
How can I add an empty field in the drop-down menu and display all products when it is chosen?
Here is the current Ember CLI code:
app/routes/index.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return {
categories: this.store.find('category'),
products: this.store.find('product')
};
}
});
app/controllers/index.js
import Ember from 'ember';
export default Ember.Controller.extend({
selectedCategory: null,
categories: function(){
var model = this.get('model.categories');
return model;
},
products: function(){
var model = this.get('model.products');
return model;
}.property('selectedCategory')
});
app/templates/index.hbs
<p>
{{view "select"
content=model.categories
optionValuePath="content.id"
optionLabelPath="content.name"
value=selectedCategory
}}
</p>
{{#each product in products}}
<li>{{product.name}}</li>
{{/each}}
app/models/product.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
category: DS.belongsTo('category', { async: true }),
});
app/models/category.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
products: DS.hasMany('product', { async: true })
});
What do I have to change in the controller to filter products
depending of the chosen value of the drop-down menu?
You could create a computed property that filters the products like:
filteredProducts: function() {
var selectedCategory = this.get('selectedCategory');
var products = this.get('products');
return products.filter(function(product) {
return product.get('category.name') === selectedCategory;
});
}.property('selectedCategory')
How can I add an empty field in the drop-down menu and display all
products when it is chosen?
Just add prompt value in the Ember select view:
{{view "select" prompt="All products"
content=categories
optionLabelPath="content.name"
optionValuePath="content.name"
value=selectedCategory}}
And then when you observe the selectedCategory, if the user select's the prompt, the value of the selection will be null.
Thus you can update the filteredProducts computed property to take that into account as well:
filteredProducts: function() {
var selectedCategory = this.get('selectedCategory');
var products = this.get('products');
if(selectedCategory) { // return filtered products
return products.filter(function(product) {
return product.get('category.name') === selectedCategory;
});
}
return products; // return all products
}.property('selectedCategory')

Computed property on a model in a different controller

I'm building the mandatory TODO app to learn ember.
I have tasks and tags in a belongsTo/hasMany relationship (each tag hasMany tasks). When showing tasks, I want to show a computed property on each available tag.
Models:
App.Tag = DS.Model.extend({
tasks: DS.hasMany('task', {async: true}),
..
});
App.Task = DS.Model.extend({
tag: DS.belongsTo('tag', {async: true}),
..
});
Route:
App.TasksRoute = Ember.Route.extend({
model: function(params) {
return Ember.RSVP.hash({
tasks: this.store.find('task'),
tags: this.store.find('tag')
});
},
setupController: function(controller, model) {
this.controllerFor('tasks').set('content', model.tasks);
this.controllerFor('tags').set('content', model.tags);
}
});
Tags controller:
App.TagsController = Ember.ArrayController.extend({
needs: ["tag"]
})
Tag controller:
App.TagController = Ember.ObjectController.extend({
taskCount: function() {
// FOLLOWING DOES NOT WORK
return this.get('tasks.length')
}.property('tasks')
});
Tag partial:
<ul>
{{#each tag in model}}
<li>
{{tag.name}} ({{controllers.tag.taskCount}} tasks)
</li>
{{/each}}
</ul>
The computed property 'taskCount' does not work. There is something wrong with 'this'.
Is this canonical way of doing it? And if so, what is wrong? Thanks
EDIT: FIXED
I had missed out
App.ApplicationSerializer = DS.ActiveModelSerializer.extend();
And I've used render to get the controller decoration:
{{render 'tag' tag}}
which calls the controller before rendering

Ember.js Masonry rendering doesn't wait for Data Models to be generated

I am trying to build a masonry view of the top selling Items in a hypothetical eCommerce Site but Masonry is being rendered before the Data Models can be generated over RESTAdapter. Here are is my Ember.js code:
App.Userprofile = DS.Model.extend({
loggedIn: DS.attr('boolean'),
name: DS.attr('string'),
totalItems: DS.attr('number'),
});
App.ApplicationRoute = Ember.Route.extend({
setupController: function(controller) {
this.store.find('userprofile', 'bat#man.com').then (function(userprofile) {
controller.set ('model', userprofile);
});
}
});
App.ApplicationAdapter = DS.DjangoRESTAdapter.extend({
host: HOST,
namespace: 'api'
});
App.ApplicationView = Ember.View.extend({
elementId: '',
classNames: ['container','fullwidth'],
templateName: 'application'
});
App.Cloud = DS.Model.extend({
item: DS.attr('string'),
numberItems: DS.attr('number'),
rank: DS.attr('number')
});
App.CloudAdapter = DS.DjangoRESTAdapter.extend({
host: HOST,
namespace: 'api',
});
App.CloudController = Ember.ObjectController.extend({
needs: ['application'],
cloudSize: function() { // Determines the size of the div
var cloudsize = Math.round (this.get('model.numberItems') * 5 / this.get('controllers.application.totalItems')) + 1;
var divName = "score" + cloudsize.toString();
return divName;
}.property('model.numberItems', 'controllers.application.totalitems')
});
App.ItemcloudRoute = Ember.Route.extend({
setupController: function(controller) {
this.store.findAll('cloud').then (function(itemcloud) {
controller.set ('model', itemcloud);
});
}
});
App.ItemcloudController = Ember.ArrayController.extend({
needs: ['cloud', 'application'],
sortProperties: ['rank'],
});
App.ItemcloudView = Ember.View.extend({
elementId: 'magicgrid',
classNames: ['cloudcontainer'],
templateName: 'itemcloud',
didInsertElement: (function() {
this._super();
Ember.run.scheduleOnce('afterRender', this, this.applyMasonry);
}).observes('controller.itemcloud'),
applyMasonry: function() {
setTimeout( function() { // settimeout to ensure masonry is called after data models are generate
console.log ("applyMasonry being called");
$('#magicgrid').masonry({
itemSelector: '.company',
isAnimated: true
});
}, 2000);
}
});
Here is the portion of the template file where itemcloud is generated.
<script type="text/x-handlebars" data-template-name='itemcloud'>
{{#each controller.model itemController="cloud"}}
<div {{bind-attr class=":company cloudSize"}}>
<div class="companylogo">
<img src="images/logos/color-logos/logo-01.jpg" />
</div>
<div class="count">{{numberItems}}</div>
</div>
{{/each}}
<div class="clearfix"></div>
</script>
Now, I am struggling to find a way to hold the Masonry rendering until after the data is fetched due to the asynchronous nature of the data fetching and the template rendering. My research says that using a View for the CloudController Objects would be useful, but am trying to figure out if there is something I am missing in my current design. Also, if someone can provide pointers to use Views correctly here for the CloudController Objects
Let me know if I need to provide any more clarifications. Thanks!
if you doing it in the setupController Ember assumes the model is already ready and continues rendering the page despite the response not coming back from the server.
The easiest way to do it is to return your model/promise in the model hook. Ember will wait on rendering the page until the model has been resolved.
App.ItemcloudRoute = Ember.Route.extend({
model: function(){
this.store.find('cloud');
}
});
The code above will do the same thing your code was doing, except Ember will wait for the find to resolve before creating and setting the model on the controller.
As per kingpin2k comments updating the answer to reflect the working code:
App.ApplicationRoute = Ember.Route.extend({
model: function() {
return this.store.find ('userprofile', 'bat#man.com');
},
setupController: function(controller, model) {
controller.set ('model', model);
}
});

Load additional data into an EmberData model that is still in the store

I have a list of products that will be loaded under the /products route, from there you can navigate to a single product under the /products/:product_id. This is my models and the route:
var Product = DS.Model.extend({
page_title: DS.attr('string'),
image: DS.attr('string')
});
var ProductComment = DS.Model.extend({
contents: DS.attr('string')
});
var ProductRoute = Ember.Route.extend({
model: function(params) {
return App.Product.find(params.product_id)
},
setupController: function(controller, model) {
controller.set('content', model);
}
});
On the product page I want to load the products and additionally the comments for a product. As I use an external Api I cant load the id of the comments into the product model. So now I want to load the comments in to the ProductsController. I tried like described in this SO but it doesn't work. I'm using EmberDatas RESTAdapter.
I came up with solution. In the modelAfter hook of the products route, check if the comments are loaded into the model using this.get('product_comments').content.length. If not, load the data using App.ProductComment.find({product_id: this.id}) and store them into the model.
App.ProductRoute = Ember.Route.extend({
afterModel: function(model) {
model.ensureComments();
}
});
Product = DS.Model.extend({
page_title: DS.attr('string'),
image: DS.attr('string'),
product_comments: DS.hasMany('App.ProductComment'),
ensureComments: function() {
var productcomments = this.get('product_comments');
if (!productcomments.content.length) {
App.ProductComment.find({product_id: this.id}).then(function(result) {
productcomments.pushObjects(result)
});
}
}
});