ember-data: Inheritance in Parent-Child resources - ember.js

Let's say this is my router setup -
router.js :
App.Router.map(function(){
this.resource('photos', function(){
this.resource('photo', {path: '/:photo_id'}, function(){
//this.route('edit');
});
});
});
photo.js:
App.Photo = DS.Model.extend({
path: DS.attr('string'),
resolution: DS.attr('string'),
author: DS.belongsTo('user'),
dateTaken: DS.attr('date'),
aperture: DS.attr('string'),
focalLength: DS.attr('string'),
.
.
.
exposure: DS.attr('string')
});
photosRoute.js:
App.PhotosRoute = Ember.Route.extend({
model: function(){
return this.store.find('photo');
}
});
photos.hbs:
<div class="container">
<div class="row">
{{#each photo in controller}}
<div class="col-md-4">
{{#link-to 'photo' photo}}{{photo.path}}{{/link-to}} <br />
By: {{photo.author}} <br />
</div>
{{/each}}
</div>
</div>
{{outlet}}
As seen above, I am only using {{photo.path}} and {{photo.author}} in the photos.hbs template to show the list of all photos. However, in this setup a call to /#/photos would fetch all the bunch of fields for every photo from my django REST server - which I am not interested. Is there a way to fetch just a few fields from the photo model for /#/photos and the complete photo model only when I click on individual photo i.e. /#/photos/photo_id
I have a tried a couple of things:
Created a new child resource called 'photoDetail' that extends the original 'photo'. Ideally this should replace the singular 'photo'.
From what I gather {async: true} property holds only for async fetching btween models that have relationships setup between them - but not for individual fields in the photo model like: 'exposure', 'focalLength'.
Any help would be greatly appreciated.

No, there's no partially loaded models in Ember-Data. (In fact, I don't think any of the big Ember-Data alternatives have that feature). For the most part your models should be small enough where loading all of the records won't really matter. By the time you enable GZIP, you probably won't even notice a difference. If you have a special use case where bandwidth is extremely limited, you'll probably just want to write your own persistence library. (You could probably also modify Ember-Data or abuse some of its features to accomplish the same task, but I'd recommend against it.)

Related

Binding to a model relationship property fails in the each helper

Binding to a model relationship property fails in the each helper as demonstrated below:
Here are my models:
//app/models/category.js
export default DS.Model.extend({
name: DS.attr(),
image: DS.belongsTo('image', { async: true }),
});
//app/models/image.js
export default DS.Model.extend({
name: DS.attr('string'),
thumbfullfilepath: DS.attr('string'),
category: DS.belongsTo('category', { async: true })
});
When I run the category model in the each handlebars helper below to retrieve the 'thumbfullfilepath' for an image tag, no value is bound to the img src:
{{#each model as |category|}}
<div class="small-element item">
<div class="cat-name">{{category.name}}</div>
<div class="cat-name edit">{{#link-to 'admin.categories.edit' category}}Edit{{/link-to}}</div>
<span class="entry-thumb">
<img src={{category.image.thumbfullfilepath}} alt="">
</span>
</div>
{{/each}}
However, I have verified the relationship binding works on display of a single model as when I visit the "admin.categories.edit" route which loads a single category model, the {{category.image.thumbfullfilepath}} path is retrieved and reflected in the template. This has led me to believe that for some reason, model relationship bindings fail in the each handlebars helper within templates.
Would someone shed some light here.
## The solution that has worked for me
I created an image component "image-atom" whose component.js is as below:
//pods/components/image-atom.js
export default Ember.Component.extend({
tagName: 'img',
attributeBindings: ['src', 'alt'],
alt: '',
src: Ember.computed(function () {
this.get('source').then((image) => {
this.set('src', image.get('thumbfullfilepath'));
});
return null;
})
});
Which I use like so here below and it works but it feels hacky:
{{#each model as |category|}}
<div class="small-element item">
<div class="cat-name">{{category.name}}</div>
<span class="entry-thumb">
{{image-atom source=category.image alt=""}}
</span>
</div>
{{/each}}
Here below are the environment details:
ember cli version: "2.2.0-beta.2"
ember-data: "^2.2.1"
ember: "2.2.0"
node: "0.12.7"
npm: "2.14.10"
os: "darwin x64 El Capitan"
Let me know.
You might have a better time simply wrapping an {{if helper around you image.
<span class="entry-thumb">
{{#if category.image.thumbfullfilepath}}
<img src={{category.image.thumbfullfilepath}} alt="">
{{/if}}
</span>
the issue is likely the image.thumbfullfilepath is not resolved before the image is trying to render, becuase it is an async promise.
Edit: For the record, working with promises in a computed property in your example, is not recommended. It might be more headache than anything.

Ember - #each pass the instance of the model

For a small webapp I'm trying to do the following:
I have a list of objects (achievement-model)that's being served through a json api
Router
export default Ember.Route.extend({
model:function(){
return this.store.find('achievement');
});
});
Model
export default DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
});
Template
{{#each a in model}}
<div>
<h4>{{a.name}}</h4>
<p>{{a.description}}</p>
<button {{action 'addThis'}}/>
</div>
{{/each}}
The setup of the app is that there is a list of achievements. I want one list of achievements in a database. Every user that logs in can add with the button his own achievements to his profile. If a user logs in he should see the list of all the achievements but the one he already added to his profile should have a green background color and the button removed. I know this can be done with if-statements etc.
The problem however is, how do i pass the specific model to the controller so i can log this to the userprofile? I tried the following:
<button {{action 'addThis' a}}/>
and then in the controller
actions:
addThis: function(obj){
console.log(obj);
});
which logs the object, but somehow I can't acces it to get let's say the name or id to copy it to the user-profile.
I also don't know if this is the best approach for what I'm trying to achieve?
Edit
I think this has something to do with promises. I can see the data is logged in the above console.log. I just don't know how to target it. it's wrapped in _data. I tried the afterModel to wait untill everything's loaded, but that doesn't seem to work.
What you could is to use an ItemController, e.g. which handles each item in the ArrayController,
e.g.
{{#each a in model itemController="achievement"}}
<div>
<h4>{{a.name}}</h4>
<p>{{a.description</p>
<button {{action 'addThis'}}/>
</div>
{{/each}}
Since the itemController is "achievement", by naming convention, the controller becomes
App.AchievementController = Ember.ObjectController.extend({
init: function() {
var name = this.get('name');
var description = this.get('description');
}
});

Ember JS Deep Linking

I have an Ember JS 1.5.1 app with ember-data 1.0.8 beta. There are TWO simple compiled templates the relevant parts are:
index
<div class="container-fluid">
<div class="col-md-2 sidebar">
<ul class="nav nav-sidebar">
{{#each model}}
<li>
{{#link-to 'activities' this}}{{name}}{{/link-to}}
</li>
{{/each}}
</ul>
</div>
<div class="col-md-10 col-md-offset-2">
{{outlet}}
</div>
</div>
activities
<div>
<ul>
{{#each model.activities}}
<div class="row">
<p>activity {{id}} is {{name}}</p>
</div>
{{/each}}
</ul>
</div>
The application is also simple, reduced to a few bits of fixture data and some route functions:
window.App = Ember.Application.create();
App.ApplicationAdapter = DS.FixtureAdapter;
App.Router.map( function(){
this.resource('index', {path: '/'}, function(){
this.resource('activities', { path:':name'}, function(){
this.resource('activity');
});
});
});
App.IndexRoute = Ember.Route.extend({
model: function(){
return this.store.find('role');
}
});
App.ActivitiesRoute = Ember.Route.extend({
model: function(params){
var roles = this.modelFor('index');
return roles.findBy('name', params.name).get('activites');
}
});
App.Role = DS.Model.extend({
name: DS.attr('string'),
activities: DS.hasMany('activity', {async:true} )
});
App.Activity = DS.Model.extend({
name: DS.attr('string')
});
App.Role.FIXTURES = [{
id: 1,
name: 'Management',
activities: [1]
},{
id: 2,
name: 'Analysis',
activities: [1,2]
},{
id: 3,
name: 'Development',
activities: [2]
}]
App.Activity.FIXTURES = [{
id: 1,
name: 'talking'
},{
id: 2,
name: 'doing'
}];
What I get when I navigate to localhost is a simple list of the three roles on the left hand side of the screen and nothing on the right hand side. (as expected)
When I then select a link (such as 'Analysis') the outlet on the right hand side fills with the expected list of two activity names "talking" and "doing".
LHS list RHS pane
========== ========
Management talking
Analysis doing
Development
So far so good.
I noticed that when I hovered over the 'Analysis' link the browser shows the url below as expected
localhost:/#/Analysis
However when I cut and paste this url into the browser address bar directly I only get the left hand side list of links and nothing in the main window. The list of "talking" and "doing" does no appear. There are no errors shown in the browser and ember does not raise and exceptions.
How do you get this simple nested route to refresh all the contents when you directly deep link rather than having to navigate from the root all the time?
When you use link-to and pass it the model, it will skip the model hook supplying the model from the link-to to the route. If you refresh the page, it will hit each route down the tree until it's fetched the models for each resource/route necessary to fulfill the request. So if we look at your routes one at a time it will do this:
Hit the application route, fetch its model if it exists (application route is the root of every Ember app).
Hit your index route, where it will return App.Role.find()
Hit your activites route, where it will return App.Activity.find()
Number 3 is where you real issue lies. Regardless of whether or not that part of the url says Analysis, Management, or Development you will already return App.Activity.find(). You've defined the dynamic slug :name, ember will parse the appropriate part of the url, and pass that part is as an object, in the case of Analysis Ember will pass in { name: 'Analysis' } to your model hook. You will want to take advantage of this, to return the correct model.
App.ActivitiesRoute = Ember.Route.extend({
model: function(params){
var roles = this.modelFor('index');
return roles.findBy('name', params.name);
}
});
Additionally you are using a fairly old version of Ember Data. Here's a small example of how Ember Data should be used with newer versions: http://emberjs.jsbin.com/OxIDiVU/617/edit
As you can see, you no longer declare the store. Additionally you may run into trouble with what would be considered async properties, and might want to read https://github.com/emberjs/data/blob/master/TRANSITION.md

emberjs Cannot clone an Ember.Object that does not implement Ember.Copyable

I am using ember 1.3.1 and ember-data 1.0.0-beta.5. On creating new mode I get following error
Assertion failed: Cannot clone an Ember.Object that does not implement Ember.Copyable
Following is my model code
App.myModel = DS.Model.extend({
name : DS.attr('string'),
age : DS.attr('string')
});
In my create route model function
return Em.Object.create({});
and finally on save I do following
this.store.createRecord('property', this.get('model'));
Although despite the error, my backend service is called successfully and new model is saved.
Please guide.
Thanks
I had the same issue which I fixed by doing the following:
In the model function of the route replace
return Em.Object.create({});
with
return this.store.createRecord('myModel');
and on save replace
this.store.createRecord('myModel', this.get('model'));
with
this.get('model').save();
For the sake of completeness, in the scenario described by #acidleaf this is the solution offered by Yehuda Katz from the ember core team in this video:
Off the Menu: Building a Client-Side With Ember and Rails - Yehuda Katz # Rails Israel 2013
In the route from which you're returning a list of resources to display (i.e the plural version of the resource StoriesRoute, PostsRoute, etc..), you'll returned a filtered list containing those which are not new:
model: function() {
this.store.find('myModel');
return this.store.filter('myModel',function(myModel){
return !myModel.get('isNew');
});
}
I am quite new to Ember and still trying to catch all problems caused when migrating to newer versions of Ember and Ember Data, but...
On one hand I think you have a mistake in last code block and that it should be:
this.store.createRecord('myModel', this.get('model'));
// myModel instead of property
But on the other hand I dont think this will be the problem :-/
anyway, try to look (and compare) to changes for Ember data here: https://github.com/emberjs/data/blob/master/TRANSITION.md
and also on this http://discuss.emberjs.com/t/createrecord-using-this-get-model-throws-an-error/3968 or similiar
hope it helps!
J.
I have ran into this problem while learning Ember. The accepted answer works, but it first creates a new empty record in the store. This was not desired in my application as it displays the empty record in my view.
My Solution
Router
App.ItemsNewRoute = Ember.Route.extend({
setupController: function(controller, model) {
controller.set('content', {});
}
});
Controller
App.ItemsNewController = Ember.ObjectController.extend({
actions: {
save: function() {
this.store.createRecord('item', {
title: this.get('newTitle'),
category: this.get('newCategory')
}).save();
this.transitionToRoute('items');
}
}
});
Template
<script type="text/x-handlebars" data-template-name="items">
<ul class="list-group">
{{#each}}
<li class="list-group-item">{{title}} - {{category}}</li>
{{/each}}
{{outlet}}
<li class="list-group-item">{{#link-to "items.new"}}Add{{/link-to}}</li>
</ul>
</script>
<script type="text/x-handlebars" data-template-name="items/new">
<li class="list-group-item">
{{input class="form-control" value=newTitle placeholder="Title"}}
{{input class="form-control" value=newCategory placeholder="Category"}}
<button class="btn btn-default" {{action "save"}}>Save</button>
</li>
</script>

How to iterate through a hasMany collection in Ember using a different controller for each record

I have three models defined in Ember and all returning json on request:
App.Comment = DS.Model.extend({
discussion: DS.belongsTo('App.Discussion')
});
App.Discussion = DS.Model.extend({
meeting: DS.belongsTo('App.Meeting'),
comments: DS.hasMany('App.Comment')
});
App.Meeting = DS.Model.extend({
discussions: DS.hasMany('App.Discussion')
});
From the route meeting/:id in the meeting controller I want to be able to iterate over the collection of discussions, setting a new discussion controller for each instance. I then need to be able to iterate through each discussions comments, doing the same again. Currently I seem to be able to access the attributes defined on the associations, but if I call a computed property I get an incorrect result returned (for example a count of 0 when it should be 5). I think the problem is something to do with the context of the controller, because I cannot seem to return the values for that object within the controller. It's probably something really simple, but I just can't see it. What am I doing wrong?
Here are the controllers:
App.MeetingController = Ember.ObjectController.extend({
needs: ['discussion']
});
App.DiscussionController = Ember.ObjectController.extend({
commentCount: function(){
return this.get('comments.length');
}.property('comments')
});
Then in the meeting.hbs template:
{{#each discussion in discussions}}
{{ render 'discussion' discussion }}
{{/each}}
And the discussion.hbs template, this works:
<div>
{{ comments.length }}
</div>
but this does not:
<div>
{{ commentCount }}
</div>
Where am I going wrong?
I think the issue is with how you defined the computed property, it looks like the binding didn't work.
Try using .property('comments.#each') instead:
App.DiscussionController = Ember.ObjectController.extend({
commentCount: function(){
return this.get('comments.length');
}.property('comments.#each')
});
Update 10 July 2014: You can now use:
App.DiscussionController = Ember.ObjectController.extend({
commentCount: Ember.computed.oneWay('comments.length')
});