I have a simple app on fiddle http://jsfiddle.net/kitsunde/qzj2n/2/
<script type="text/x-handlebars">
{{outlet}}
</script>
<script type="text/x-handlebars" id="profile">
Profile Page
{{ email }}
</script>
Where I'm trying to display a profile page.
window.App = Ember.Application.create();
App.Router.map(function () {
this.resource('profile', {path: '/'});
});
App.User = DS.Model.extend({
email: DS.attr('string')
});
App.User.FIXTURES = [
{
id: 1,
email: 'herpyderp#gmail.com'
}
];
App.ProfileRoute = Ember.Route.extend({
model: function(){
return App.User.find().get('firstObject');
}
});
But I'm getting an exception:
Error while loading route: TypeError: undefined is not a function
What am I missing?
There are a few things missing. You can find the fixed fiddle here:
http://jsfiddle.net/47cHy/
window.App = Ember.Application.create();
App.ApplicationAdapter = DS.FixtureAdapter;
App.Router.map(function () {
this.resource('profile', { path: '/' });
});
App.User = DS.Model.extend({
email: DS.attr('string')
});
App.User.FIXTURES = [
{
id: 1,
email: 'herpyderp#gmail.com'
}
];
App.ProfileRoute = Ember.Route.extend({
model: function() {
return this.store.find('user').then(function(users) {
return users.get('firstObject');
});
}
});
Your template had the id index and not the name of the route profile
You have to tell Ember specifically to use the fixture adapter.
You accessed the model directly via the global object. You should let Ember do the work via the internal resolver and use this.store.find.
.find() returns a promise. You should get the first object in the then handler and return it there.
Related
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}}
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);
}
});
I have a fiddle (http://jsfiddle.net/kitsunde/3FKg4/) with a simple edit-save application:
<script type="text/x-handlebars">
{{outlet}}
</script>
<script type="text/x-handlebars" id="profile/edit">
Edit.
<form {{action 'save' on="submit"}}>
<div>
<input type="email" {{bind-attr value="email"}}>
</div>
<button>Save</button>
</form>
</script>
<script type="text/x-handlebars" id="profile/index">
{{#link-to 'profile.edit'}}Edit{{/link-to}}
{{email}}
</script>
And my application:
window.App = Ember.Application.create();
App.ApplicationAdapter = DS.FixtureAdapter.extend();
App.Router.map(function () {
this.resource('profile', {path: '/'}, function(){
this.route('edit');
});
});
App.ProfileRoute = Ember.Route.extend({
model: function() {
return this.store.find('user').then(function(users){
return users.get('firstObject');
});
}
});
App.ProfileEditRoute = App.ProfileRoute;
App.ProfileEditController = Ember.ObjectController.extend({
actions: {
save: function(){
var profile = this.get('model');
profile.setProperties({email: this.get('email')});
profile.save();
this.transitionTo('profile');
}
}
});
App.User = DS.Model.extend({
email: DS.attr('string')
});
App.User.FIXTURES = [
{
id: 1,
email: 'herpyderp#gmail.com'
}
];
When I hit save and it goes back to profile/index it doesn't have an updated model and when I go back to profile/edit the edit isn't there. I realize I could use {{input value=email}} which does seem to remember the model changes, but that seems to persist the changes to the model as I type which isn't what I want.
What am I missing?
The save method returns a promise, you could transition when the promise is resolved as:
var route = this;
profile.save().then(function() {
route.transitionTo('profile');
}, function() {
// TODO: implement error logic
});
In that case, your method will be updated when the application goes back to the index state.
I fixed it with 2 changes. First since I was grabbing this.get('email') I was getting the models email address and not the one from input field, so it was actually never updating the data.
<script type="text/x-handlebars">
{{outlet}}
</script>
<script type="text/x-handlebars" id="profile/edit">
Edit.
<form {{action 'save' on="submit"}}>
<div>
{{input value=email}}
</div>
<button>Save</button>
</form>
{{#link-to 'profile'}}Back{{/link-to}}
</script>
<script type="text/x-handlebars" id="profile/index">
{{#link-to 'profile.edit'}}Edit{{/link-to}}
{{email}}
</script>
Second to deal with only updating the model on save I used ember-data transaction handling to rollback the commit when navigating away from the current route, unless it had been saved. I also moved my logic into the router.
window.App = Ember.Application.create();
App.ApplicationAdapter = DS.FixtureAdapter.extend();
App.Router.map(function () {
this.resource('profile', {path: '/'}, function(){
this.route('edit');
});
});
App.ProfileRoute = Ember.Route.extend({
model: function() {
return this.store.find('user').then(function(users){
return users.get('firstObject');
});
}
});
App.ProfileEditRoute = App.ProfileRoute.extend({
deactivate: function(){
var model = this.modelFor('profile');
if(model.get('isDirty') && !model.get('isSaving')){
model.rollback();
}
},
actions: {
save: function(){
this.modelFor('profile').save();
this.transitionTo('profile');
}
}
});
App.User = DS.Model.extend({
email: DS.attr('string')
});
App.User.FIXTURES = [
{
id: 1,
email: 'herpyderp#gmail.com'
}
];
Updated fiddle: http://jsfiddle.net/kitsunde/3FKg4/2/
App.Router.map(function() {
this.resource('products', function() {
this.resource('product', { path: ':product_id' }, function() {
this.route('general');
});
})
});
App.ProductsRoute = Ember.Route.extend({
model: function() {
return this.store.find('product');
}
});
App.ProductRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('product', params.product_id);
}
});
Templates:
<script type="text/x-handlebars" data-template-name="product">
Showing {{ name }}
<p>{{ outlet }}</p>
</script>
<script type="text/x-handlebars" data-template-name="product/general">
General template for {{ name }}
</script>
In the /products/3 view the name shows up as it should, but not in the /products/3/general view. Anybody know why?
I have tried to copy the App.ProductRoute and rename it to App.ProductGeneralRoute to find the correct model, but then the params does not exist.
In Ember, nested routes don't have access to their parent routes model. There are two ways to access parent models in a child route.
App.ProductGeneralRoute = Ember.Route.extend({
model: function() {
return this.modelFor('product');
}
});
This sets the model on the ProductGeneral route by getting the model for the ProductRoute. Or you can use needs:
App.ProductGeneralController = Ember.ObjectController.extend({
needs: ['product']
});
In the latter example, you will have access to controllers.product, which will allow you to call controllers.product.model in the template.
See this article for more info on needs.
Have an upcoming weekend project and using it to evaluate Ember.js and I cannot figure out why I cannot display nested objects in my template. This does not work:
{{#each emails}}
{{email_address}}
{{/each}}
When I try just {{emails}} I get a hint that something is right:
Models:
App.Contact = DS.Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string'),
company: DS.attr('string'),
emails: DS.hasMany('App.Email')
});
App.Email = DS.Model.extend({
contact: DS.belongsTo('App.Contact'),
emailAddress: DS.attr('string'),
});
Route:
App.Router.map(function() {
this.resource('contacts', function() {
this.resource('contact', {path: ':contact_id'});
});
});
App.ContactsRoute = Ember.Route.extend({
init: function() {},
model: function() {
return App.Contact.find();
}
});
App.ContactRoute = Ember.Route.extend({
model: function(params) {
return App.Contact.find(params.contact_id);
}
});
I have no idea what to try next. I'm using active_model_serializer in Rails. I've tried embedding, side-loading to no avail. I'm sure it's something simple that I'm missing.Thanks in advance!
When using the each helper it's recomendable to be more specific about the items you are looping over to avoid such problems.
Try the following:
{{#each email in model.emails}}
{{email.emailAddress}}
{{/each}}
This should also work:
{{#each emails}}
{{this.emailAddress}}
{{/each}}
And also, your model property is called emailAddress and not email_address.
Hope it helps.