Emberjs. Building a Model with Ember.Object - ember.js

Forgive me as I am new to Ember. I have, to me , a fairly complex json object that I am working with.
https://gist.github.com/bungdaddy/11152304
My attempt is to build a Model with Ember.Object, use reopenClass for several methods that will return 'sections' of the JSON object that I can use in my handlebars template.
var Prequalification = Ember.Object.extend();
Prequalification.reopenClass({
template: function(){
return $.getJSON("http://myurl/prequalification")
.then(function(response){
var prequalification = [];
var template = response.collection.template.data
template.forEach(function(data){
prequalification.push(Prequalification.create(data));
});
console.log(template);
return prequalification;
});
},
businessType: function(){
//Here is where I would like to build a method that will pull from template, the particula JSON that I need and return below.
return ["Me","Myself","I"];
}//I wish to further extend from here, other methods that I may need to fulfill model requirements.
});
I most likely will need an ArrayController to manage these models. Thing I have read so much, compiled so many different variations that I am quite lost. Any clarity in all this would be a great help to me. I can handle the simple JSON objects, it's the complex models that are kicking my ^&&^$^(*^#

Related

How do I call a controller function from a template in Ember?

Let's say I have a template which iterates over a collection of items, and I want to call a function with each item which is specific to the controller, and not a model-level concern:
{{#each people as |person|}}
icon name: {{findIconFor(person)}}
{{/each}}
I'd like to define findIconFor in the controller, because this is something specific to this particular view.
export default Ember.Controller.extend({
findIconFor: function(person) {
// figure out which icon to use
}
);
But that doesn't work. The template fails to compile. Parse error: Expecting 'STRING', 'NUMBER', 'ID', 'DATA', got 'INVALID'
What is the "ember way" to do this?
As i spent almost entire day on a similar problem here is my solution.
Because Ember for some reason just doesn't allow you to run a controller functions directly from the template (which is ridiculous and ties your hands in some very stupid ways and i don't know who on earth decided this is a good idea ...) the thing that makes most sense to me is to create an universal custom helper, that allows you to run functions from the template :) The catch here is that you should always pass the current scope (the "this" variable) to that helper.
So the helper could be something like this:
export default Ember.Helper.helper(function([scope, fn]) {
let args = arguments[0].slice(2);
let res = fn.apply(scope, args);
return res;
});
Then, you can make a function inside your controller, that you want to run, for example:
testFn: function(element){
return element.get('name');
}
and then in your template you just call it with the custom helper:
{{#each items as |element|}}
{{{custom-helper this testFn element}}}
{{/each}}
The first two arguments to the helper should always be "this" and the name of the function, that you want to run, and then you can pass as many extra arguments as you wish.
Edit: Anyway, every time when you think you need to do this, you should think if it will not be better to create a new component instead (it will be in 90% of the cases)
I'd use a computed property in the controller:
iconPeople: Ember.computed('people.#each', function(){
var that = this;
return this.get('people').map(function(person){
return {
'person': person,
'icon': that.findIconFor(person)
};
});
})
Now you could get the icon from {{person.icon}} and the name from {{person.person.name}}. You might want to improve on that (and the code is untested), but that's the general idea.
If the icon is something associated with a person, then since the person is represented by a model, it is best to implement it as a computed property on the person model. What is your intent in trying to put it into the controller?
// person.js
export default DS.Model.extend({
icon: function() { return "person-icon-" + this.get('name'); }.property('name')
..
};
Then assuming that people is an array of person:
{{#each people as |person|}}
icon name: {{person.icon}}
{{/each}}
The alternative provided by #jnfingerle works (I assume you figured out that he is proposing that you loop over iconPeople), but it seems like a lot of extra work to go to to create a new array containing objects. Does the icon depend on anything known only to the controller? If not, as I said, why should the logic to compute it be in the controller?
Where to put things is a a matter of philosophy and preference. Some people like bare-bones models that contain nothing more than fields coming down from the server; other people compute state and intermediate results in the model. Some people puts lots of stuff in controllers, whereas others prefer light-weight controllers with more logic in "services". Personally, I'm on the side of heavier models, lighter controllers, and services. I'm not claiming that business logic, or heavy data transformations, or view preparations should go in the model, of course. But remember, the model represents an object. If there's some interesting facet to the object, whether it come down from the server or be computed somehow, to me it makes a lot of sense to put that in the model.
Remember also that controllers are part of a tightly-coupled route/controller/view nexus. If there's some model-specific thing that you compute in one controller, you might have to then add it to some other controller that happens to be handling the same model. Then before you know it you're writing controller mixins that share logic across controllers that shouldn't have been in them in the first place.
Anyway, you say your icon comes from an "unrelated data store". That sounds asynchronous. To me, that hints that maybe it's a sub-model called PersonIcon which is a belongsTo in the person model. You can make that work with the right mix of adapters and serializers for that model. The nice thing about that approach is that all the asynchronicity in retrieving the icon is going to be handled semi-magically, either when the person model is created, or when you actually need the icon (if you say async: true).
But perhaps you're not using Ember Data, or don't want to go to all that trouble. In that case, you could consider adorning the person with the icon in the route's model hook, making use of Ember's ability to handle asynchronous model resolution, by doing something like the following:
model: function() {
return this.store.find('person') .
then(function(people) {
return Ember.RSVP.Promise.all(people.map(getIcon)) .
then(function(icons) {
people.forEach(function(person, i) {
person.set('icon') = icons[i];
});
return people;
})
;
})
;
}
where getIcon is something like
function getIcon(person) {
return new Ember.RSVP.Promise(function(resolve, reject) {
$.ajax('http://icon-maker.com?' + person.get('name'), resolve);
});
}
Or, if it is cleaner, you could break the icon stuff out into an afterModel hook:
model: function() { return this.store.find('person'); },
afterModel: function(model) {
return Ember.RSVP.Promise.all(model.map(getIcon)) .
then(function(icons) {
model.forEach(function(person, i) {
person.set('icon') = icons[i];
});
})
;
}
Now Ember will wait for the entire promise to resolve, including getting the people and their icons and sticking the icons on the people, before proceeding.
HTH.

Proper way to set multiple models on route; depending on user authentication?

I'm currently working on an Ember app and it is coming along fine but since I am new to MVC applications in general there are a lot of concepts that don't come naturally to me.
I am currently trying to return two models for my index route. I referred to another SO question (EmberJS: How to load multiple models on the same route?) for the correct method and it has worked great.
My problem is now that I need to only set one of the two models only if the user is authenticated. I am using ember-simple-auth, and currently this is what I've got:
// app/routes/index.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
if (this.get('session.isAuthenticated')) {
var _this = this;
this.get('store').find('user', this.get('session.uid')).then(function(user) {
_this.set('model.entries', user.get('entries'));
});
}
return Ember.RSVP.hash({
newEntry: this.get('store').createRecord('entry', {
body: 'Write here ...'
})
});
}
});
For some reason, this does not work. After my route is loaded, the model only has the 'newEntry' property and not an 'entries' property, although the promise does get fulfilled (I put console.logs inside to prove it).
What could be happening? And is this the best way to accomplish this?
There is a set of data that you always want to load, for every user. Do that in the model hook, that is actually the data for the route.
There is another piece of info that you want to add only if a condition is met (authentication). Do that in the afterModel hook.
...is provided the route's resolved model...
http://emberjs.com/api/classes/Ember.Route.html#method_afterModel
So, now you can append or remove data from the model. Or take any relevant action depending on the data that you received.

Adding objects to hasMany error

My form has body and subject inputs, and an input for tags, so the user can enter any number of tags (saved to tagList) and then submit the request. Problem: JSON.stringify(z) does something like this
New request:{"subject":"this is subject","body":"this is body","tags":["fixture-0","fixture-1"]}
instead of getting the tags to be the text I entered, I get fixture-0...
import Ember from "ember";
export default Ember.ArrayController.extend({
tagList: [],
actions: {
addRequest: function() {
var z = this.store.createRecord("Request", {body: this.get("body"), subject: this.get("subject")
});
this.get("tagList").forEach(function(entry){
console.log("adding tag to request: "+entry.get("tagt"));
z.get("tags").pushObject(entry);
});
console.log("New request:" + JSON.stringify(z));
z.save();
},
addTag: function(){
console.log("adding " + this.get("tag"))
var t = this.store.createRecord("tag", {tagt: this.get("tag")});
this.get("tagList").pushObject(t)
}
}
});
First of all, I don't think you can rely on JSON.stringify to convert your records to JSON properly, that's generally the job of a serializer. (Although I guess the toJSON method on the object could defer to the serializer, but I don't think it does.)
Second, this is expected behavior for Ember-Data. The names of the text aren't in the JSON because they don't need to be. You have a hasMany relationship, which means that the record only keeps references (IDs) to tag objects. Keeping the actual text in the object would be duplicating that information.
As a side note, judging by the fact that you're using Request as a model type name, I can say with a pretty good degree of certainty that you're using Ember-Data incorrectly. This may be part of the reason you aren't expecting Ember-Data to behave the way it is. I would suggest reading Ember's guide on models to better understand what Ember-Data is for, and why it's probably not a good fit for your use case.

Saving nested models

I have two models like this:
App.Build = DS.Model.extend({
allegiance: DS.attr('string'),
profession: DS.attr('string'),
skills: DS.hasMany('skill')
});
App.Skill = DS.Model.extend({
name:DS.attr('string'),
value:DS.attr('number')
});
In my app, I have controls to set the allegiance, profession, and values of each skill (there's up to 55).
Then in the actions hash of my application controller, I have an action to save the build model to the server.
save:function(){
var store = this.get('store');
var skills = this.get('controllers.skills').get('model');
console.log(skills);
var build = store.createRecord('build',{
profession:1,
allegiance:1,
skills:skills
});
build.set('skills',skills);
build.save();
console.log('Saved!');
}
But when the build model is sent to the server the skills property is an empty array:
{"build":{"allegiance":"1","profession":"1","skills":[]}}
I'm sure I'm doing something wrong, but I can't figure out what and can't find any good documentation about it. An additional note, all I care about submitting is the skill id and value.
Any help will be greatly appreciated!
UPDATE:
Following Daniel's suggestion, I've edited the save function to use pushObjects to put the skills into the Build model, then save it. It's working better now. The generated post data is like this now:
{"build":{
"allegiance":1,
"profession":1,
"skills":["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31","32","33","34","35","36","37","38","39","40","41","42","43","44","45","46","47","48","49","50","51","52","53","54","55"]}}
That being a list of the skill ids. None of the other attributes are submitted in the post. I've tried iterating over skills, creating a new object, and just pushing in the id and value, which are the only parts I need, but that gives me an error. Something like, can not use undefined, must be type skill.
This seems like something Ember data should handle natively. Is there something I'm missing to get it to send the other skill attributes in the request?
Thanks!!
If anyone else is interested, I solved the issue by overriding the serlizer with a custom serliazer for the Build model like this:
App.BuildSerializer = DS.RESTSerializer.extend({
serializeHasMany: function(record, json, relationship) {
if(relationship.key === 'skills') {
var skills = record.get('skills');
var block = [];
skills.forEach(function(skill, index) {
var current = {};
current.id = skill.get('id');
current.value = skill.get('value')
block[index] = current;
});
json['skills'] = block;
} else {
return this._super(record,json,relationship);
}
}
});
UPDATE:
There's a much easier way to do this now using the DS.EmbeddedRecordsMixin like this:
App.BuildSerializer = DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin,{
attrs: {
skills: 'records'
}
});
Is the skills model a RecordArray? That's the underlying model Ember data uses. You might try creating the record then using pushObjects after the fact.
var build = store.createRecord('build',{
profession:1,
allegiance:1
});
build.get('skills').pushObjects(skills);
additionally, save returns a promise, so in order to properly handle the successful save versus failure you can handle it like this.
build.save().then(
function(){
console.log('Saved!');
},
function(){
console.log('Failed to save');
});

Templating engine that binds model attributes to view?

When rendering a template through a Backbone view you will often end up with some code that looks something like this:
ShirtView = {
template: JST["/templates/shirt_template"],
el: ".shirt-element"
render: function() {
var html = this.template({color: this.model.color, size: this.model.size});
this.$el.html(html);
}
}
This is all well and good and your template will render with the attributes you wanted. But if this.model.color changes then it will not be reflected in the view. You can then use something like modelbinder to explicitly bind elements in the view to your model, but this means introducing extra code to your view.
What I am wondering is if there are any templating engines, like Moustache or Handlebars, that automatically updates the elements belonging to the fields in the attributes object as the model changes, without me having to specify it in the view?
As the comments have suggested, there are several libraries you can use for this ... but I'd like to suggest that you don't need to. I work on a Backbone-powered site with thousands (heck, probably tens or hundreds of thousands) of lines of code, and all we use is our own custom base class.
In essence, all you need to do is:
var TemplatedView = Backbone.View.extend({
render: function() {
this.renderTemplate();
}.
renderTemplate: function() {
this.$el.html(this.template(this.model.toJSON()));
}
});
then you can make any new view a templated one with just:
var ShirtView = TemplatedView.extend({
template: JST["/templates/shirt_template"],
el: ".shirt-element"
});
or, if you have some custom render logic, you just need to call renderTemplate:
var AdvancedShirtView = TemplatedView.extend({
template: JST["/templates/shirt_template"],
el: ".shirt-element",
render: function() {
this.model.fold();
this.renderTemplate();
this.model.press();
}
});
Now we have a few enhancements beyond that (eg. if someone specifies a "rawTemplate" property on one of our views, our renderTemplate will compile it in to a proper template), but that's the beauty of rolling your own solution for something like this: you get exactly what you want.
Or you can use a library :-) But personally, for something that's both so simple and so integral to your site, I don't see why you'd want to.