Ember.js: programmatically load hasmany relationship - ember.js

Disclamer: I started using Ember.js few days ago
Suppose having these 2 models:
App.Question = DS.Model.extend({
text: DS.attr('string'),
answers: DS.hasMany('answer', {async: true})
});
App.Answer = DS.Model.extend({
question: DS.belongsTo('question'),
text: DS.attr('string')
});
For some reasons I got this JSON (without "answers" list)
{
...
"questions": [
{"id": 1, "text": "Foo?"}, ...
]
}
In the template I want to load and show answers only if explicitly needed
{{#each questions itemController="question"}}
<div class="answer-wrapper">
{{text}}
<button {{action "loadAnswers"}}>Load answers</button>
<ul>
{{#each answers}}
<li>{{text}}</li>
{{/each}}
</ul>
</div>
{{/each}}
How can I do this in the controller's loadAnswer action?
App.QuestionController = Ember.ObjectController.extend({
...
actions: {
loadAnswers: function() {
// programatically load answers
}
}
});
Workaround: I can do this changing the attribute in the template
{{#each loadedAnswers}}
<li>{{text}}</li>
{{/each}}
and defining correctly the action
App.QuestionController = Ember.ObjectController.extend({
...
actions: {
loadAnswers: function() {
// or loaded with ajax...
this.set("loadedAnswers", [
{id: 1, text: "foo"},
{id: 2, text: "bar"}
]);
}
}
});

You can wrap that portion of the template with an if statement. The async relationship won't be fetched unless it's requested.
Controller
App.QuestionController = Ember.ObjectController.extend({
showAnswers: false,
...
actions: {
loadAnswers: function() {
this.set('showAnswers', true);
}
}
});
Template
{{#if showAnswers}}
<ul>
{{#each answers}}
<li>{{text}}</li>
{{/each}}
</ul>
{{/if}}

Related

Ember js filter model hasMany content via checkbox

I am building a small educational app where the structure is as follows -
Exam hasMany Subjects and Subjects hasMany courses.
My model relationships -
App.Exam = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
subjects : DS.hasMany('subject',{ async: true }),
});
App.Subject = DS.Model.extend({
name: DS.attr('string'),
description:DS.attr('string'),
exam: DS.belongsTo('exam', { async: true })
});
Initially I display all the exams and on exam/1 I display all the subjects belonging to that exam.
I am having trouble in filtering the subjects via checkbox
Here is the Demo
Not able to figure out how to do it. Can someone suggest me how to approach this ?
Basically on click of physics checkbox only physics subject should be displayed in the view.
I am basically using the MultipleSelectionFilterComponent from my blog post mentioned in the comments. This component will take care of managing the selection of the different checkboxes and send a filter function to the controller. There you can use the function to filter the data. You can refer to my post for more details.
Here is the working demo.
The code looks like
App.ExamsExamRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('exam', params.exam_id).then(function (exam) {
console.log("found", exam);
return exam;
});
},
setupController: function(controller, model) {
this._super(controller, model);
controller.set('filteredContent', model.get('data.subjects'));
}
});
App.ExamsExamController = Em.ObjectController.extend({
filteredContent: [],
actions: {
filterBySubject: function(filterFn) {
this.set('filteredContent',
this.get('model.data.subjects').filter(filterFn));
}
}
});
<ul class="list-group">
{{#multiple-selection-filter filter-key-path="name"
action="filterBySubject"}}
{{#each subject in model.subjects}}
<li class="">
<label>
<input type="checkbox" class="item-checkbox" {{bind-attr
value=subject.name}}/>
{{subject.name}}
</label>
</li>
{{/each}}
{{/multiple-selection-filter}}
</ul>
<h3>Subjects Details - </h3>
{{#each subject in filteredContent}}
<div class="col-md-3 well">
{{subject.name}}
<br>{{subject.description}}
</div>
{{/each}}
Here is the code to the MultipleSelectionFilterComponent.
App.MultipleSelectionFilterComponent = Em.Component.extend({
selectedItems: [],
click: function(event) {
var el = Em.$(event.target);
var filterFn;
if(el.is('input[type=checkbox]')) {
if(el.is(':checked')) {
this.get('selectedItems').pushObject(el.val());
} else {
this.get('selectedItems').removeObject(el.val());
}
}
if(this.get('selectedItems.length')) {
filterFn = function(item) {
return this.get('selectedItems')
.contains(Em.get(item, this.get('filter-key-path')));
}.bind(this);
} else {
filterFn = function() {return true;};
}
this.sendAction('action', filterFn);
}
});
that is SIMPLY not possible in ember.js
yeahhh it sux

How do I load a fixture into a template with a different model in ember?

The intent of my code is to be able to load various fixtures into a template. For example:
The family template:
{{#each model as family}}
<p>{{family.name}} + "is" + {{family.age}} + "years old"</p>
{{/each}}
The brothers fixture: [{ id:1, name:"Greg", age:40 }]
The sisters fixture: [{ id:1, name:"Mary", age:35 }]
So that when I call:
#/brothers I'd get <p>Greg is 40 years old</p>
vs
#/sisters I'd get <p>Mary is 35 years old</p>
I figure I'm having trouble with 1) the correct routing. 2) usage of {{each}}/{{with}}. 3) usage of fixtures/models. You can find EVERYTHING at my github repo. Thanks in advance for the help!
Application Template - application.hbs:
<br><br>
<div class="container">
<ul class="nav nav-pills" role="tablist">
<li class="active">Home</li>
<li>Brothers</li>
<li>Sisters</li>
</ul>
<div class="container">
{{outlet}}
</div>
</div>
Family template (to go into {{outlet}} of application) - family.hbs:
<h1>Fixtures and Models</h1>
{{#each in model as family}}
<p>{{family.name}} is here</p>
{{else}}
<li>DIDN'T WORK</li>
{{/each}}
Family models and fixtures - family.js:
App.Family = DS.Model.extend({
name : DS.attr(),
age : DS.attr()
});
App.Brothers = DS.Model.extend({
name : DS.attr(),
age : DS.attr()
});
App.Sisters = DS.Model.extend({
name : DS.attr(),
age : DS.attr()
});
App.Brothers.FIXTURES = [
{
"id" : 1,
"name" : "Greg",
"age" : 10
},
{
"id" : 2,
"name" : "Mike",
"age" : 23
}
];
App.Sisters.FIXTURES =
[
{
"id" :1,
"name" :"Mary",
"age" :15
},
{
"id" :2,
"name" :"Gina",
"age" :34
}
];
My app.js file for all the routes:
App = Ember.Application.create({
LOG_TRANSITIONS: true
});
App.Router.map(function() {
this.resource('application');
this.resource('home');
this.resource('brothers');
this.resource('sisters');
});
App.IndexRoute = Ember.Route.extend({
redirect: function() {
this.transitionTo('application');
}
});
App.FamilyRouter = Ember.Route.extend({
});
App.HomeRoute = Ember.Route.extend({
});
App.FamilyRoute = Ember.Route.extend({
});
App.BrothersRoute = Ember.Route.extend({
model: function() {
this.store.find('brothers');
},
renderTemplate: function() {
return this.render('family');
}
});
App.SistersRoute = Ember.Route.extend({
model: function() {
return this.store.find('sisters');
}
});
Again, you can find all the codez here at my github repo
You need two fixes.
First, you're using wrong #each syntax. Change it to this:
<h1>Fixtures and Models</h1>
{{#each family in model}}
<p>{{family.name}} is here</p>
{{else}}
<li>DIDN'T WORK</li>
{{/each}}
Second, you're not returning brothers models. Change to:
App.BrothersRoute = Ember.Route.extend({
model: function() {
return this.store.find('brothers');
},
templateName: 'family'
});

Ember.js clear controller on transitionToRoute call

I have a route that displays a list of parcels, and an Ember.Select that allows the user to select which state's parcels to show.
Model
App.Parcel = DS.Model.extend({
addresses: DS.attr('array')
});
Route
App.ParcelsRoute = Ember.Route.extend({
state: null,
renderTemplate: function () {
this.render({ outlet: 'parcels' });
},
model: function (params) {
state = params.state;
App.ParcelAdapter.state = state;
App.ImageAdapter.state = state;
return Ember.RSVP.hash({
props: this.store.findAll('parcel'),
states: this.store.findAll('state'),
});
},
setupController: function (controller, model) {
controller.set('states', model.states);
controller.set('props', model.props);
controller.set('selectedState', state);
}
});
Controller
App.ParcelsController = Ember.ObjectController.extend({
selectedState: null,
props: null,
states: null,
first: true,
modelReloadNeeded: function () {
if (this.get('selectedState') != undefined && !this.get('first')) {
this.transitionToRoute('/parcels/' + this.get('selectedState'));
}else{
this.set('first', false);
}
}.observes('selectedState')
});
Handlebars
<script type="text/x-handlebars" id="parcels">
{{view Ember.Select content=states optionValuePath="content.id" optionLabelPath="content.id" value=selectedState}}
<input class="search" placeholder="Search"/>
<ul class="list nav">
{{#each props}}
<li>{{#link-to 'parcel' this}}<h3 class="name">{{addresses.0.street_address}}</h3>{{/link-to}}</li>
{{/each}}
</ul>
</script>
When the select transitions to the new route, both the old routes data and new routes are in the model, but if I reload the page, only the current routes data is loaded. Is there a way to clear the DS.RecordArray for props in the controller without a location.reload() call?

Pass Iterating Object to Controller Method

When trying to pass an iterating object to the Controller method, the object is passed as a String.
Template:
<script type="text/x-handlebars" data-template-name="gradebooks">
{{#each}}
{{#link-to "gradebook" this}}{{title}}{{/link-to}}
{{/each}}
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="gradebook">
<h1>Student Grades</h1>
{{#each student in students}}
{{#each assignment in assignments}}
{{student.name}}, {{assignment.name}}: {{getGrade student assignment}}%
{{/each}}
{{/each}}
</script>
Routes:
App.Router.map(function() {
this.resource('gradebooks', function() {
this.resource('gradebook', { path: ':gradebook_id' });
})
});
App.GradebooksRoute = Ember.Route.extend({
model: function() {
return this.store.find(App.Gradebook);
}
});
App.GradebookRoute = Ember.Route.extend({
model: function(params) {
return this.store.find(App.Gradebook, params.gradebook_id);
}
});
Controller:
App.GradebookController = Ember.ObjectController.extend({
getGrade: function(student, assignment) {
console.log(student, assignment);
return 5;
}
});
Model:
App.Gradebook = DS.Model.extend({
title: DS.attr('string'),
students: DS.hasMany('student', { async: true}),
assignments: DS.hasMany('assignment', { async: true})
});
App.Student = DS.Model.extend({
name: DS.attr('string'),
gradebook: DS.belongsTo('gradebook'),
grades: DS.hasMany('grade', { async: true })
});
App.Assignment = DS.Model.extend({
name: DS.attr('string'),
gradebook: DS.belongsTo('gradebook'),
grades: DS.hasMany('grade', { async: true })
});
App.Grade = DS.Model.extend({
score: DS.attr('number'),
student: DS.belongsTo('student'),
assignment: DS.belongsTo('assignment')
});
Currently, the above outputs the string "student" for each student. If I were to have {{getGrade student.id}}, it would output the string "student.id". How could I get it to pass the student as an object?
Given your revision I've revised my answer below to better align:
App.AssignmentController = Ember.ObjectController.extend({
needs: ["student"],
getGrade: function() {
var student = this.get('controllers.student');
//logic to get grade
}.property() //thing to observe for changes
});
<script type="text/x-handlebars" data-template-name="gradebooks">
<h1>Student Grades</h1>
{{#each}}
{{#link-to "gradebook" this}}{{title}}{{/link-to}}
{{/each}}
</script>
<script type="text/x-handlebars" data-template-name="gradebook">
{{#each student in students}}
{{render 'student' student}}
{{/each}}
</script>
<script type="text/x-handlebars" data-template-name="student">
{{name}}
{{#each assignment in assignments}}
{{render 'assignment' assignment}}
{{/each}}
</script>
<script type="text/x-handlebars" data-template-name="assignment">
{{name}}: {{getGrade}}%
</script>
A useful tool is {{log model}} or {{log record}} or {{log ...whatver..}} to understand what data your view is receiving.
If you open the Chrome Ember Inspector you should also see what views, controllers, routes, models, etc. have been defined which will help with understanding.
In short you cannot pass parameters to controllers by using {{getGrade student.id}} - you can define a Handelbars helper to accept parameters but I don't think this is what you are after. The method above worked for me.
Hope this helps

how to create a right structure for nested routes in ember.js?

I need to create an application with routes:
/users - list of users
/users/123 - user info
/users/123/items - list of user items
/users/123/items/456 - items info
I wrote this code here
$(function() {
var App = window.App = Ember.Application.create({LOG_TRANSITIONS: true});
App.Router.map(function() {
this.resource("users", {path: '/users'}, function() {
this.resource("user", {path: '/:user_id'}, function() {
this.resource("items", {path: '/items'}, function() {
this.route("item", {path: '/:item_id'});
});
});
});
});
App.Store = DS.Store.extend({
revision: 11,
adapter: "DS.FixtureAdapter"
});
//ROUTES
App.UsersIndexRoute = Ember.Route.extend({
model: function() {
return App.User.find();
}
});
App.UsersUserIndexRoute = Ember.Route.extend({
model: function(params) {
return App.User.find(params.user_id);
},
setupController: function(controller, model) {
controller.set('content', model);
}
});
//DATA
App.User = DS.Model.extend({
name: DS.attr('string'),
items: DS.hasMany('App.Item')
});
App.Item = DS.Model.extend({
name: DS.attr('string')
});
App.User.FIXTURES = [
{
id: 1,
name: 'Joe',
items: [1, 2]
}, {
id: 2,
name: 'John',
items: [2, 3]
}
];
App.Item.FIXTURES = [
{
id: 1,
name: 'Item 1',
}, {
id: 2,
name: 'Item 2',
}, {
id: 3,
name: 'Item 3',
}
];
return true;
});
And templates:
<script type="text/x-handlebars" data-template-name="application">
<div>
<h1>ember routes</h1>
{{outlet}}
</div>
</script>
<script type="text/x-handlebars" data-template-name="index">
<h2>Hello from index</h2>
{{#linkTo 'users'}}users area{{/linkTo}}
</script>
<script type="text/x-handlebars" data-template-name="users">
<h2>users area</h2>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="users/index">
<h3>users list</h3>
{{#each user in controller}}
{{#linkTo "user" user}}{{user.name}}{{/linkTo}}<br/>
{{/each}}
</script>
<script type="text/x-handlebars" data-template-name="users/user">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="users/user/index">
<h4>user {{name}} index</h4>
</script>
<script type="text/x-handlebars" data-template-name="users/user/items">
<h4>user {{name}} items</h4>
{{outlet}}
</script>
If I go to /users , I see a list of users - its ok.
If I go to /users/1 I see message in browser console: "Transitioned into 'users.user.index'", but the content of the template 'users/user/index' is not showing.
I do not understand why, because I have App.UsersUserIndexRoute
Maybe I missed something?
if i'm not mistaken, using a UserIndexRoute (instead of a UsersUserIndexRoute - routes that are defined as resources usually don't have their parent routes' names prepended) with the model hook
model: function(params) {
return this.modelFor("user");
}
(and a corresponding template) should do the trick.