Binding to a model relationship property fails in the each helper - ember.js

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.

Related

Ember Data 2 - Submit Two Associated Models in One Form

Total Ember newb here. My back-end is written with Rails. That piece is fine, so assume all is well on the server side. Here are the specifics:
I have two models
Project (Has Many Project Details)
Project Details (Belongs To Project)
I have confirmed that:
I am able to create a new Project from my projects/new.hbs template
My Index route retrieves all Projects and the Project Details associated with them.
My Show route retrieves an individual Project and the Project Details associated with them.
What I would like to do is, submit a project model along with a project detail model from my projects/new.hbs template.
I understand that the form in new.hbs should be in a component but I am keeping it simple for now. Once, I am able to create my associated models I will work on doing the same as a form component.
Project Model
export default DS.Model.extend({
project_details: DS.hasMany('project_detail', { async: true }),
project_name: DS.attr('string'),
status: DS.attr('string')
});
Project Detail Model
export default DS.Model.extend({
project: DS.belongsTo('project', { async: true }),
project_id: DS.attr('number'),
feature_name: DS.attr('string'),
hours_billed: DS.attr('number'),
available_hours: DS.attr('number'),
amout: DS.attr('number')
});
Projects/New.js Route
export default Ember.Route.extend({
model: function() {
return Ember.RSVP.hash({
projects: this.store.createRecord('project'),
project_details: this.store.createRecord('project_detail')
});
},
actions: {
create: function() {
var self = this;
this.controller.get('model').save().then(
function() {
self.transitionTo('projects.index');
});
}
}
});
Projects/New.hbs
<form {{ action "create" on="submit" }}>
<label for="project_name">Project Name</label>
{{ input value=project_name }}
<label for="status">Status</label>
{{ input value=status }}
<label for="feature_name">Feature Name</label>
{{ input value=feature_name }}
<label for="available_hours">Available Hours</label>
{{ input value=available_hours }}
<label for="hours_billed">Hours Billed</label>
{{ input value=hours_billed }}
<label for="amount">Amount</label>
{{ input value=amount }}
<button type="submit">Save</button>
</form>
Router.js
Router.map(function() {
this.route('home');
this.route('dashboard');
this.route('login');
this.resource('projects', function() {
this.route('new');
this.route('show', {path: '/:id'});
this.route('edit', {path: '/:id/edit'});
});
});
When I submit the form, the action appears to be called, but all data that is submitted is NULL, so evidently, my inputs are not being recognized. I have searched for a couple days and have not seen a solution for submitting multiple models. My code is a result of expanding on a standard POST along with how I handled retrieving my associated models via the RSVP Hash. Any advice would be great. Thanks!
You've got some things that are not wired up correctly.
When you return the following from your route:
model: function() {
return Ember.RSVP.hash({
projects: this.store.createRecord('project'),
project_details: this.store.createRecord('project_detail')
});
},
In your template, you can use {{model.something}} to access that information, in your case you have access to {{model.projects}} and {{model.project_details}}.
You probably want to do the following in your route to setup easier access to your models in the templates.
setupController(controller, model) {
//this._super(controller, model) I'll explain why this is commented
controller.set('project', model.projects);
controller.set('project_details', model.project_details);
}
If you do this, then in your template you can do things like this:
<label>Project Name</label>
{{input value=project.project_name}}
{{project.project_name}}
Which brings to the next thing. When you have in your template the following:
{{ input value=project_name }}
What you're doing is binding the input value to the project_name property of your controller. If you use the Ember Inspector (hint, if you're not using it, you should. It makes life so much easier) you can probably check that the controller has that property project_name with the value you typed.
Before Ember 2.0 there was this thing called the Object Controller (which you don't use anymore) that allowed proxying the model to the controller. Which meant that you could on your route do this:
model() {
return this.store.createRecord('project');
}
And in your template you effectively to this:
<label>Project Name</label>
{{project_name}}
Today you have to do this (Unless you install ember-legacy-controllers which I don't recommend):
<label>Project Name</label>
{{model.project_name}}
As you can see, you need to add the model before the project_name. This is because the model returned from the route is set as a property of the controller by default. That's something we can change, of course. You could also do the following in your route:
setupController(controller, model) {
controller.set('model', model.projects);
controller.set('myProjectDetails', model.project_details);
}
And in the template you could now do the following:
<form {{ action "create" on="submit" }}>
<label for="project_name">Project Name</label>
{{ input value=model.project_name }}
<label for="status">Status</label>
{{ input value=model.status }}
<label for="feature_name">Feature Name</label>
{{ input value=myProjectDetails.feature_name }}
<label for="available_hours">Available Hours</label>
{{ input value=myProjectDetails.available_hours }}
<label for="hours_billed">Hours Billed</label>
{{ input value=myProjectDetails.hours_billed }}
<label for="amount">Amount</label>
{{ input value=amount }}
<button type="submit">Save</button>
</form>
Which means that your save function now works because the "project" is now your model. If you used my example of setupController where I did this:
controller.set('project', model.projects);
Then you would have to change your create action to this:
actions: {
create: function() {
var self = this;
this.controller.get('project').save().then(
function() {
self.transitionTo('projects.index');
});
}
}
Also, do notice that you have your models declared with
//Project
project_details: DS.hasMany('project_detail', { async: true }),
//Project Detail
project: DS.belongsTo('project', { async: true }),
But you're not setting the relations in the code you posted. If you want the models to be related, in your create action you would need something like this (or in any other place really). I'm assuming in the following snippet that my setupController example is in the route so that the project is in the controller as project and the project_details as project_details.
create: function() {
var self = this;
var project = this.controller.get('project');
var projectDetails = this.controller.get('project_details');
project.get('project_details').pushObject(projectDetails);
project_details.set('project', project);
project.save().then(function() {
self.transitionTo('projects.index');
});
}
Assuming you're using Ember Data 2.0, there's also another problem with this. When you save the project it will only make a post request to save the project itself (it won't save the newly created project_details with it). At the moment JSONAPI(the standard Ember Data 2.0 by default uses) does not have a solution for updating multiple resources in one go (see this github issue). This means that you would have to save the project_details first and only then then project (which probably doesn't make sense looking at your models as it seems the project_details only exists under a project.)
It's the current state of affairs, as far as I know. I'm also looking for a good solution for this problem (I've hacked together solutions like only allowing to create a project_detail after the project is created, or having an extra attribute in the project with is a string of the serialized project_detail json and then have the backend to the things it needs)

When is the index.hbs loaded in an Ember app? What is the difference between a component and a template?

I was following this Ember tutorial and this quickly got a lot more complicated. This was the tutorial that I was following.
I am lost as to what is going on. When is the index.hbs getting loaded and why? Here is my code starting with the router.js:
import Ember from 'ember';
import config from './config/environment';
var Router = Ember.Router.extend({
location: config.locationType
});
Router.map(function() {
this.route('todos', { path: '/'}, function() {
this.route('complete');
this.route('incomplete');
});
});
export default Router;
So it looks like our home url will load the todos.js route right? This is my code:
import Ember from 'ember';
export default Ember.Route.extend({
model() {
let todos = [
{
title: 'Learn Ember',
complete: false,
},
{
title: 'Solve World Hunger',
complete: false,
}
];
return todos;
}
});
So this todos.js route is my model right?
I assume ember also loads the todos.hbs template by default? Is that right? Or does it load the app/templates/todos/index.hbs? Which one does it load?
This is my app/templates/todos.hbs code:
<input type="text" id="new-todo" placeholder="What needs to be done?" />
{{#todo-list todos=model}}
{{outlet}}
{{/todo-list}}
This is my app/templates/todos/index.hbs code:
<ul id="todo-list">
{{#each model as |todo|}}
<!-- this loads the component todo-item and passes in a todo as todo -->
{{todo-item todo=todo}}
{{/each}}
</ul>
The tutorial doesn't really explain what is going on here. If the index.hbs gets loaded, does it then load the todo-item component template? If so, this is my app/templates/components/todo-item.hbs:
<input type="checkbox" class="toggle" checked="{{if todo.complete 'checked'}}">
<label class="{{if todo.complete 'completed'}}">{{todo.title}}</label><button class="destroy"></button>
In the event that the app/templates/todos.hbs gets loaded...What is going on in the app/templates/todos.hbs? Are we passing in the model (somehow accessible in the template?) as todos to the todo-list component? Here is the app/templates/components/todo-list.hbs
<section id="main">
{{yield}}
<input type="checkbox" id="toggle-all">
</section>
<footer id="footer">
<span id="todo-count">
<strong>2</strong> todos left
</span>
<ul id="filters">
<li>
All
</li>
<li>
Active
</li>
<li>
Completed
</li>
</ul>
<button id="clear-completed">
Clear completed (1)
</button>
</footer>
Welcome to the wonderful world of Emberjs! first of all, I recommend you to visit the official page of Emberjs. Can you see the sidebar menu? well, be ready to spend some time reading it if you want to understand how Emberjs works. I strongly recommend you to read at least Router, Template, Component and Controller sections to begin with.
Let's see some of the snippets you have provided:
Router.map(function() {
this.route('todos', { path: '/'}, function() {
this.route('complete');
this.route('incomplete');
});
});
This is where you define your routes. Here what you have is the main route called 'todos' but used as the root page (starting at /). After it, there are two more routes: /complete and /incomplete.
model() {
let todos = [
{
title: 'Learn Ember',
complete: false,
},
{
title: 'Solve World Hunger',
complete: false,
}
];
return todos;
}
Here you are defining a model in one route (I assume is the route of todos). Pretty straight, isn't it? if you were using Ember Data for example. you would ask the server for the model here and the route would wait until receive the response.
The reason why you have an index template and a todos template is simple: todos.hbs will contain the {{outlet}} in which every page will be rendered. Imagine it as a wrapper. Whatever comes after / will be wrapped by this todos.hbs, even the index.hbs. You have more info here, in the guides (reason why I recommend you to read it first).
Let's move to another snippet:
{{#todo-list todos=model}}
{{outlet}}
{{/todo-list}}
Here you are using a component to wrap whatever is rendered in the {{outlet}}. You haven't pasted it here, but it should contain in its template at least a {{yield}} to specify where the {{outlet}} will be rendered. That info about {{yield}} can be found here.
Let's move to the next part:
ul id="todo-list">
{{#each model as |todo|}}
<!-- this loads the component todo-item and passes in a todo as todo -->
{{todo-item todo=todo}}
{{/each}}
</ul>
This {{#each}} handlebar, expressed in a block way (that's why it uses the # at the beginning and the / at the end), is a loop that allows you work with each item of your model, defined as todo. What you are doing here is to provide the component todo-item with one item of the model. If your model has 3 todos, todo-item will be rendered 3 times, one for each of them.
Again, I recommend you to follow that tutorial having the emberjs guides opened and whenever you have a doubt, check the guides until you understand the concept and then, move to the next step.

ember-data stores a string instead of a number

In my ember app I want to reuse a model attribute as soon as the form is submitted. But the store seems to keep it as string unless I reload the whole route. I am using this and the following components:
Ember : 1.12.0
Ember Data : 1.0.0-beta.18
jQuery : 1.11.3
/app/models/purchase.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
amount: DS.attr('number'),
createdAt: DS.attr('date', {
defaultValue: function() { return new Date(); }
}),
.. other callback and associations..
});
/app/controllers/ledger/purchases/new.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function () {
return {
newPurchase: this.store.createRecord('purchase', {
name: null,
amount: null,
player: null
})
}
}
});
/app/templates/ledger/purchases/new.hbs
<div class="row">
<div class="col-xs-12">
<h4>New purchase</h4>
<form>
<div class="form-group">
<label for="name" class="sr-only control-label">name</label>
{{input id='name' type="text" value=newPurchase.name placeholder="What" class="form-control"}}
</div>
<div class="form-group">
<label for="amount" class="sr-only control-label">amount</label>
{{input id='amount' type='number' value=newPurchase.amount placeholder="How much" class="form-control"}}
</div>
<div class="form-group">
<button type="submit" class="btn btn-success" {{action "create"}}>create</button>
{{#link-to 'ledger.purchases' tagName="button" class="btn btn-link" }}cancel{{/link-to}}
</div>
</form>
</div>
</div>
/app/controllers/ledger/purchases/new.js
import Ember from 'ember';
export default Ember.Controller.extend({
newPurchase: Ember.computed.alias('model.newPurchase'),
actions: {
create: function() {
var np = this.get('newPurchase');
console.log(Ember.typeOf(np.get('amount')));
........
save np etc...
}
}
});
the console log call clearly shows that the type is a string. The ember inspector shows the same. However data are correctly saved to the backend because after reloading everything is fine. But I need the amount as a number as soon as it is submitted because I use it to make and show the sum of all purchases.
Okay, I think I know what's going on. Setting input type to number won't help here. Value is still recognized as string. Usually when you submit form, backend anyway returns this value formatted as a number and problem's gone. You can see this even when you mock your data with a number, without a backend.
My solution would be to use a computed property for input component. Model:
export default DS.Model.extend({
name: DS.attr('string'),
amount: DS.attr('number'),
createdAt: DS.attr('date', {
defaultValue: function() { return new Date(); }
}),
amountAsNum: Ember.computed('amount', {
get: function () {
return parseFloat(this.get('amount'));
},
set: function (key, value) {
var valueToSet = parseFloat(value);
this.set('amount', valueToSet);
return valueToSet;
}
}),
.. other callback and associations..
});
Template:
{{input id='amount' type='number' value=newPurchase.amountAsNum placeholder="How much" class="form-control"}}
Now, you can check typeof(amount) before save and it'll give you number. Demo on JS Bin.

ember-data: Inheritance in Parent-Child resources

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.)

Ember Interpolation in a href tag in handlebars template

I am trying to make a simple link to google maps with a dynamic address inserted into the href field. I have tried the code below plus tons of other messing around with no luck. How do you interpolate a dynamic ember string in a handlebars href field?
I am using ember,rails, and handlebars.
I know how to use bindAttr if I had the entire url stored with my model but I only have the address. Putting the google url with every model seemed unnecessary if i could just call it once in the view.
<h1>{{name}}</h1>
<div>
<p><a {{bindAttr href='http://maps.com/?1=address'}}>{{address}}</a></p>
</div>
<h1>{{name}}</h1>
<div>
<p><a href='http://maps.google.com/?q={{address}}'>{{address}}</a></p>
</div>
What I used to Fix it
App.Location = DS.Model.extend(
name: DS.attr('string', defaultValue: "")
address: DS.attr('string', defaultValue: "")
fullAddress: (->
"http://maps.google.com/?q=#{#get('address')}"
).property('address')
)
You could do something like this, see demo.
Basically you could create a Mixin for common properties and then mix it in your models.
For example:
App.BaseModel = Ember.Mixin.create({
base: 'http://maps.google.com/?q=',
fullAddress: function(){
return this.get('base') + this.get('address');
}.property('address')
});
App.MyModel = DS.Model.extend(App.BaseModel, {
name: DS.attr('string'),
address: DS.attr('string')
});
So you could later use it in you templates like this:
{{#each model}}
<h1>{{name}}</h1>
<div>
<p><a {{bind-attr href='fullAddress'}}>{{address}}</a></p>
</div>
{{/each}}
Hope it helps.