I have a pretty basic setup where I'm trying to format a date in my Controller. The problem is I can't access it in the formattedStart function below, whereas I CAN access it in the summaryRowAction handler. This is baffling me, because console.logging this in both places gives the same result. But for some reason inside of formattedStart, this.get('model.startDate') is undefined.
App.SummaryRowController = Ember.ObjectController.extend({
formattedStart: function(){
console.log(this.get('model.startDate');)
return this.get('model.startDate');
}.property(),
actions: {
summaryRowAction: function(){
console.log(this.get('model.startDate'));
}
}
});
Here is my model and my template (in Jade) for reference:
App.PricingSummary = DS.Model.extend({
startDate: DS.attr(),
endDate: DS.attr(),
days: DS.hasMany('day', {async: true}),
property: DS.belongsTo('property', {async: true})
});
script(type="text/x-handlebars", data-template-name="summaryRow")
.summaries__summary("{{action 'summaryRowAction'}}")
.summaries__summary--item{{formattedStart}} — {{endDate}}
It's because the first (and only) time that the property is evaluated, model is actually null. You need to specify startDate as a dependency in the property so Ember knows to re-evaluate when the data changes. Also, you don't need model.* in an object controller, the properties are automatically delegated to content/model
So:
formattedStart: function(){
console.log(this.get('startDate');)
return this.get('startDate');
}.property('startDate'),
Related
I have 3 different fixture models, as shown below.
var Room = DS.Model.extend({
title: DS.attr('string'),
categories: DS.hasMany('Category', { async: true }),
isSelected: DS.attr('boolean')
});
var Category = DS.Model.extend({
title: DS.attr('string'),
room: DS.belongsTo('Room', {async: true }),
materials: DS.hasMany('Material', { async: true }),
isSelected: DS.attr('boolean')
});
var Material = DS.Model.extend({
title: DS.attr('string'),
category: DS.belongsTo('Category', {async: true} ),
isSelected: DS.attr('boolean')
});
I find when I try to view the contents inside the Materials model it is blank. In my controller I expose the materials by doing this:
currentMaterials: function() {
var room = this.filterBy('isSelected', true).get('firstObject');
var categories = room.get('categories');
var selectedCategory = categories.get('firstObject');
var material = selectedCategory.get('materials');
return material;
}.property('#each.isSelected')
However when I try to access currentMaterials the value is null. I am ONLY able to access its values if I first access the Rooms/Categories using a {{#each} loop. Oddly once I do the {{#each}} I am then able to access the values in currentMaterials.
Does anyone understand why?
It's due to fact of promises existance. Your categories relationship is async, which means that it's not present initially and ember-data should fetch it if needed. However, it takes time to fetch data, therefore ember-data returns a promise from this: var categories = room.get('categories'). After that promise, you first get firstObject from it, which does not exist for a promise (is null), and than you get materials relationship from that null. It simply is null.
However, ember templates are smart and if you put an each on them, they know that these relationships are needed and makes ember-data fetch these data.
What you can do? If you need this data to perform page-specific job, you should make sure that you have access to it before showing the page to the user - therefore in the model hook. You can use Ember.RSVP to make multiple fetch calls and set them up in the controller:
model: function() {
data =
room: store.find("room")
categories: store.find("category)
materials: store.find("material")
return Ember.RSVP.hash(data)
}
However, take notice that it will fetch all the materials, etc. If you need only the ones connected to your model, you should consider speeding up your data fetching using side loading. If you are using fixtures, it won't work.
Last that I can think of is making computed property a method that would fetch the data, but set them on other variable. You can use some kind of flag to inform the app when the data is ready:
currentMaterials: function() {
var room = this.filterBy('isSelected', true).get('firstObject');
room.get('categories').then(function(categories) {
return categories.get('firstObject').get('materials');
}).then(function(materials) {
// here you have your materials
// you can pass _this to that method and set these materials
// on some kind of controller property (e.g. materialsChosen)
// and use a flag like setting 'is Fetching' on the start of this
// computed property and setting down right here
});
}.property('#each.isSelected')
I have a Property model and a Pricing Summary model, which relate to each other and are shown below:
App.Property = DS.Model.extend({
totalRoomCount: DS.attr(),
name: DS.attr(),
address: DS.attr(),
city: DS.attr(),
state: DS.attr(),
zip: DS.attr(),
pricingSummaries: DS.hasMany('pricingSummary', {async: true})
});
App.PricingSummary = DS.Model.extend({
startDate: DS.attr(),
endDate: DS.attr(),
days: DS.hasMany('day', {async: true}),
property: DS.belongsTo('property', {async: true})
});
Inside of my Property route I set the model to a Property, and then in the template, I want to output a list of the PricingSummary's that are related to that Property, as follows:
{{#each pricingSummary in pricingSummaries}}
{{render 'summaryRow' pricingSummary}}
{{/each}}
This works, and I'm able to output the attributes of each particular PricingSummary inside of the summaryRow template, like its startDate and endDate, for example. But what I REALLY want to do here is modify/format the startDate and output this formatted version. Basically I think I want a controller at this point, but I don't know how to tie a controller to the specific Pricing Summary model being output.
How do I do this? And furthermore, you can see that a PricingSummary also has a relationship to my Day model, so I'm going to want to do this again, another level deep.
Please help!
There are several ways to accomplish this, and all of them are relatively simple.
In relation to actually decorating a model, the easiest method would be to create a computed property on the model itself. Some people don't like this because they believe the models should be skinny and decorators should be in controllers/components, but it's all up to your preference. You could accomplish it this way:
App.YourModel = DS.Model.extend({
date: attr('date'),
formattedDate: function() {
var date = this.get('date');
return date ? this.get('date').toDateString() : null ; // Use your own format :-)
}.property('date')
});
Alternatively, I like to use a getter/setter pattern so you can use two-way bindings and it will marshal the value to a date on set, or to a string on get. In the following example, I'm using moment.js to parse/format:
App.YourModel = DS.Model.extend({
date: attr('date'),
dateMarshal: function(key, value) {
if (arguments.length > 1) {
var parsed = moment(value);
this.set('date', parsed.isValid() ? parsed.toDate() : null);
}
return this.get('date') ? moment(this.get('date')).format('MM/DD/YYYY') : null ;
}.property('date'),
});
Another option would be to provide an itemController property to the {{#each}} helper, but that's effectively the same as using render without having to use a custom view.
If you're using more properties and perhaps some actions on the pricing summary row (to delete it, for instance), my preference would be to use a component:
{{#each pricingSummary in pricingSummaries}}
{{pricing-summary-item content=pricingSummary}}
{{/each}}
And your component:
App.PricingSummaryItem = Ember.Component.extend({
content: null,
dateFormatted: function() {
var formattedDate = this.get('content.date');
// Format your date
return formattedDate;
}.property('content.date')
actions: {
'delete': function() {
this.get('content').deleteRecord();
},
markRead: function() {
this.set('content.isRead', true);
this.get('content').save();
}
}
});
Finally, to address JUST the date issue and not decoration, I would make a bound helper. Again, this example uses moment.js (and I'm using ember-cli as well, so pardon the ES6 syntax):
import Ember from 'ember';
export function formatDate(input, options) {
input = moment(input);
if (options.hashContexts.fromNow) {
return input.fromNow();
} else {
return input.format(options.hash.format || 'LL');
}
}
export default Ember.Handlebars.makeBoundHelper(formatDate);
Then you just use {{format-date yourDateProperty}} in your template.
Even though i have set the RESPAdapter to take care of everything, it doesn't reach out to my server to get the data. My code is:
var App = Ember.Application.create({
LOG_TRANSITIONS: true
});
App.ApplicationAdapter = DS.RESTAdapter.extend({
host: '../api/'
});
App.Me = DS.Model.extend({
email: DS.attr('string'),
firstname: DS.attr('string'),
lastname: DS.attr('string')
});
App.WelcomemessageController = Ember.Controller.extend({
firstname:"I get rendered as i should",
model: function() {
return this.store.find('me');
}
});
And yeah, the property "firstname" gets rendered out just fine. And when inspecting with Chrome Devtools, no requests are being made.
In your case you just want to use a computed property, and not the model function. you could call it model, but it'd be slightly confusing since generally a controller decorates a model, and in this case it'd just be a property called model on the controller (in order to decorate a model, the controller needs to be an ObjectController or ArrayController)
App.WelcomemessageController = Ember.Controller.extend({
firstname:"I get rendered as i should",
user: function() {
return this.store.find('me');
}.property()
});
I'm trying to set up a hasMany relationship between two models and a hasOne (belongsTo in the current version of Ember Data) between the hasMany and hasOne.
I'm working with Ember Data and have a made a RESTful API that works according to Ember's conventions. All the classes can be queried individually.
Bookmark = hasMany -> Termbinding
Termbinding = belongsTo -> Term
Term = belongsTo -> Termbinding
So the goal is to fetch a Bookmark and get the Terms that are attached to it through the Termbinding. I would already be pretty happy to get the Bookmark to Termbinding relation working. I went through all questions posted on here, sadly enough that didn't work.
Router.js
var Router = Ember.Router.extend();
Router.map(function() {
this.resource('bookmarks', { path:'bookmarks'});
this.resource('bookmark', { path:'bookmarks/:bookmark_id' });
this.resource('termbindings', { path:'termbindings' });
this.resource('termbinding', { path:'termbindings/:termbinding_id' });
});
export default Router;
Bookmark.js
var Bookmark = DS.Model.extend({
url: DS.attr('string'),
description: DS.attr('string'),
visits: DS.attr('number'),
termbinding: DS.hasMany('termbinding')
});
export default Bookmark;
Termbinding.js
var Termbinding = DS.Model.extend({
bookmarkId: DS.attr('number'),
termId: DS.attr('number'),
termOrder: DS.attr('number'),
bookmarks: DS.belongsTo('bookmark')
});
export default Termbinding;
I hope someone can help me because this is preventing me from using Ember for my bookmark application. Thanks in advance.
It might be wise to explicitly specify your inverses, i.e.
var Termbinding = DS.Model.extend({
bookmarkId: DS.attr('number'),
termId: DS.attr('number'),
termOrder: DS.attr('number'),
bookmarks: DS.belongsTo('bookmark', { inverse: 'termbinding' })
});
export default Termbinding;
var Bookmark = DS.Model.extend({
url: DS.attr('string'),
description: DS.attr('string'),
visits: DS.attr('number'),
termbinding: DS.hasMany('termbinding', { inverse: 'bookmarks' })
});
export default Bookmark;
Ember Data will try to map inverses for you, however, it is not without faults. It could possibly be that your pluralization of 'bookmarks' on a DS.belongsTo relationship is throwing off its automatic inverse mapping. Typically for belongsTo you would use the singular, 'bookmark'. Conversely, your hasMany would be termbindings: DS.hasMany('termbinding')
Also, if you could show where you're invoking the models that would be greatly appreciated. Typically I find that creating a JSbin at emberjs.jsbin.com helps me isolate the problem and also provides a collaborative space to debug and experiment.
I'm trying out ember at my work to see if we should use it for our future applications I am doing a simple test application and I wanted to try out the relations between the models. This is the code I have that defines the models:
var App = Ember.Application.create();
App.Router.map(function () {
this.resource('index', {path: "/"}, function () {
this.resource("config", {path: "/config/:config_id"});
});
});
App.Store = DS.Store.extend();
App.Conf = DS.Model.extend({
module : DS.attr(),
reports: DS.hasMany('report'),
isClean: function() {
return !this.get('reports').isAny('isClean', false);
}.property('reports.#each')
});
App.Report = DS.Model.extend({
country: DS.attr(),
google_account_id: DS.attr(),
web_property_id: DS.attr(),
custom_source_uid: DS.attr(),
isClean: function() {
return (
this.get('country') != '' &&
this.get('google_account_id') != '' &&
this.get('web_property_id') != '' &&
this.get('custom_source_uid') != ''
);
}.property('country', 'google_account_id', 'web_property_id', 'custom_source_uid')
});
App.ApplicationAdapter = DS.RESTAdapter.extend({
host: 'http://playground.loc/battle_of_frameworks/json.php'
});
…and here is the JSON that is being loaded:
The error I get is:
Error while loading route: TypeError: Cannot set property 'store' of undefined
I Googled the problem and it's usually something about naming your models in plural (ie: App.Reports) which I'm not doing. So I am not sure what the problem is here. Can anyone give any insights?
There are several problems in your code.
Your server doesn't provide the payload expected by Ember Data. I would recommend reading this document about customizing your serializer if you can't generate the proper json payload with your backend.
Ember.js is all about convention over configuration. Right now, you are not following those conventions:
attributes are camelcased
App.Report = DS.Model.extend({
googleAccountId: DS.attr() //instead of google_account_id
});
you don't need to create the index route, it comes for free in Ember. So your router should simply look like:
App.Router.map(function () {
this.resource("config", {path: "/config/:config_id"});
});
Are you sure that your backend expects the Config to be served from /config/:config_id and not /configs/:config_id ?
You declare a config resource. The convention is to have a App.Config model and not App.Conf
In order to clean your code, you can also take advantage of computed properties to DRY your code:
App.Report = DS.Model.extend({
country: DS.attr(),
googleAccountId: DS.attr(),
webPropertyId: DS.attr(),
customSourceUid: DS.attr(),
isClean: Ember.computed.and('country', 'googleAccountId', 'webPropertyId', 'customSourceUid')
});
You also need to pay attention when defining a computed property based on an array. The isClean of Config uses isClean of Report but your computed property observes only the elements of your Report association. The correct way of writing it is:
App.Config = DS.Model.extend({
module : DS.attr(),
reports: DS.hasMany('report'),
isClean: function() {
return !this.get('reports').isAny('isClean', false);
}.property('reports.#each.isClean') //make sure to invalidate your computed property when `isClean` changes
});
I hope this helps.