Ember Data 2 - Submit Two Associated Models in One Form - ember.js

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)

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

Creating a new record not pulling data from template fields

I am attempting to create a new record, however none of the data from the fields is being passed automatically, as I expected Ember to (from what I've read).
My template:
<form {{action save content on="submit"}}>
{{input value=name}}
<button type="submit"}}>Next</a>
From what I've read content is an alias for model and interchanging these makes no difference.
My route:
App.CampaignsNewRoute = Ember.Route.extend({
actions: {
save: function(campaign) {
console.log(campaign.name);
}
},
model: function(controller) {
return this.store.createRecord('campaign');
}
});
And my controller:
App.CampaignsNewController = Ember.ObjectController.extend({
pageTitle: 'New Campaign Setup'
});
When I hit 'Next' it logs undefined. Logging just the campaign shows it's an Ember model, but without the name attribute. name is defined on the campaign model. Setting the input to {{input value=content.name}} places the name attribute within the model returned, but it's still undefined. Am I missing anything in this process? The EmberJS site doesn't show how to do this, from what I can find.
--
As a side note: I was originally using App.CampaignsNewController = Ember.Controller.extend as my model was returning a hash of promises, one of which is an array and Ember didn't like me using either array or object controller. I simplified it to the above to verify it wasn't that which was causing the issue. So any solution taking this into account would be wonderful.
Edit: I can access the template fields by doing this.get('controller').get('name') but surely that is not necessary? Changing my controller to a Ember.Controller.extend also stops that from working, would love to know why. Clarification on best practice here would still be wonderful!
Edit2: this.get('controller.content').get('name') works if the controller is simply an Ember.Controller as opposed to Ember.ObjectController and the template has {{input value=content.name}}. I'll work with but hopefully someone can clarify this is the correct way.
ObjectController is the way to go here. You would have it backed by one particular model, your new model, and you would add additional properties to the controller for use in the template.
Code
App.IndexRoute = Ember.Route.extend({
actions: {
save: function(campaign) {
console.log(campaign.get('color'));
}
},
model: function() {
return Ember.RSVP.hash({
record: this.store.createRecord('color'),
all: this.store.find('color')
});
},
setupController: function(controller, model){
this._super(controller, model.record);
controller.set('allColors', model.all);
}
});
App.IndexController = Em.ObjectController.extend({
});
Template
In the template any time you want to access anything on the model backing the template, you can just access it as if the model is the current scope.
{{name}}
if you want to access any of the properties that exist on the controller you would use the property name that it is on the controller.
{{allColors.length}}
Here's an example:
<form {{action save model on="submit"}}>
Color:{{input value=color}}<br/>
<button type="submit">Next</button>
</form>
<ul>
{{#each item in allColors}}
{{#unless item.isNew}}
<li>{{item.color}}</li>
{{/unless}}
{{/each}}
</ul>
One last tip, always use getters and setters ;)
Ember Data hides the properties, they don't live right on the object, so campaign.name will return undefined forever and ever. If you do campaign.get('name') you'll get a real response.
With the example: http://emberjs.jsbin.com/OxIDiVU/792/edit

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

What's the right way of doing manual form data saving with Ember.js?

I'm trying to create a manually saved form with a moderate number of fields (let's say 20) in Ember.js (not using live bindings) and so far am confused about the correct way / best practice for doing so. I've found the following methods:
http://www.solitr.com/blog/2012/06/ember-input-field-with-save-button/
How to use one-way binding on emberjs?
https://stackoverflow.com/a/16473186/1248965
All of the above methods seem hacky to a degree; they either extend the text field or use a per-field observer, requiring you to list out each one. Is there some other way? Something like the 'unbound' helper, but allowing the auto-model updating magic / validation (via ember-data) on some action (like an 'unbound-until' or 'conditional-bind' or something)? I've gone through all the docs, SO, the github issues, the Ember forum, and the links above, and still feel like I must have missed something.
Basically, a way to say "do everything you would do with a normally bound form/fields, but only on a certain action, rather than in real time."
What you want is a "Buffered Proxy", where you temporarily store all changes to the model (you can catch those using setUnkownProperty) in a proxy object. Once you are happy with the changes, you'd copy all of the proxy data over into the actual object ("flush the data").
App.Heisenberg = {
firstName: 'Walter',
lastName: 'White',
};
App.IndexRoute = Ember.Route.extend({
model: function() {
return App.Heisenberg;
},
setupController: function(controller, model) {
controller.set('content', model);
}
});
App.IndexController = Ember.ObjectController.extend({
proxy: {},
setUnknownProperty: function(key, value) {
console.log("Set the unknown property: " + key + " to: " + value);
this.proxy[key] = value;
console.log(this.proxy);
},
flush: function() {
for(var key in this.proxy)
this.set('model.'+key, this.proxy[key]);
}
});
Template:
<script type="text/x-handlebars" data-template-name="index">
Saved Name: {{firstName}} {{lastName}}<br />
<br />
<form {{ action "saveEdit" on="submit" }}>
First Name: {{input type="text" valueBinding="firstName"}}<br />
Last Name: {{input type="text" valueBinding="lastName"}}<br />
<br />
<button {{ action "flush" }}>Flush</button>
</form>
</script>
This would make for a nice controller Mixin.
See this jsBin for a live example.
I found a workaround, but I'm not 100% happy with it:
In my "editing" template, I have:
<form {{ action "saveEdit" on="submit" }}>
Title: {{input type="text" value=title}}
<input type="submit" value="Save">
<button {{ action "cancelEdit" }}>Cancel</button>
</form>
Then in my associated controller, I do:
cancelEdit: function() {
var entry = this.get('model');
this.set('isEditing', false);
entry.rollback();
},
saveEdit: function() {
var entry = this.get('model');
this.set('isEditing', false);
entry.save().then(
function() {
console.log('Saved!');
}
I simply hide the fields where the "live updating" would show. I still would like to find a way to temporarily turn off the binding until I trigger my "saveEdit" action, since this still seems inelegant.