ember - hide created.uncommitted records - ember.js

I need a little help about ember-data record creation.
My app is a basic one, with books and reviews, based on Code School Tutorial
The app is using the RESTadapter
There is a form on the book page to write and send a review
<div class="row">
<div class="col-sm-09">
<h4>Reviews</h4>
{{#each review in reviews}}
<div class="col-sm-3">
<p>{{review.text}}</p>
<p class="text-info">{{review.reviewedDate}}</p>
</div>
{{else}}
<p class="text-muted">No Reviews Yet. Be the first to write one!</p>
{{/each}}
</div>
<div class="col-sm-3">
<h4>Review</h4>
{{textarea valueBinding='review.text'}}
<br>
<button {{action 'createReview'}} class='btn-primary'>Review</button>
{{#if review.text}}
<br><h4>Preview</h4>
<p class="text-muted">{{review.text}}</p>
{{/if}}
</div>
My Controller
App.BookController = Ember.ObjectController.extend({
logoAvailable: 'images/instock.jpg',
logoUnavailable: 'images/unavailable.jpg',
logoAvailability: function () {
if (this.get('isAvailable'))
return this.logoAvailable;
else
return this.logoUnavailable;
}.property('isAvailable'),
review: function () {
return this.store.createRecord('review', {
book: this.get('model')
});
}.property('model'),
actions: {
createReview: function () {
var controller = this;
this.get('review').save().then(function (review) {
controller.set('text', '');
controller.get('model.reviews').addObject(review);
}, function (error) {
console.error(error);
controller.set('text', '');
review.unloadRecord();
});
}
}
});
My Models:
App.Book = DS.Model.extend({
title: DS.attr('string'),
isbn: DS.attr('string'),
summary: DS.attr('string'),
isAvailable: DS.attr('boolean'),
featured: DS.attr('boolean'),
author: DS.belongsTo('author', {async: true}),
reviews: DS.hasMany('review', {async: true}),
// computed properties
image: function () {
return 'images/books/' + this.get('id') + '.jpg';
}.property('id'),
isNotAvailable: function () {
return !this.get('isAvailable');
}.property('isAvailable')
});
App.Review = DS.Model.extend({
text: DS.attr('string'),
reviewedAt: DS.attr('date'),
book: DS.belongsTo('book'),
//Computed Properties
reviewedDate: function () {
return moment(this.get('reviewedAt')).format('LLL');
}.property('reviewedAt')
});
The controller creates a new "review" object for each opened book, and will eventualy save it when the button is pressed.
This is working, but
My uncommited record is shown in the Book reviews list, even before submiting it, (as if it was a live preview), and before the call of controller.get('model.reviews').addObject(review);
What's wrong with my code, and how can I only display commited records (sucessful save() call).

You can filter the reviews by the isNew flag.
in your BookController you could do something like this:
committedReviews: function () {
return this.get('model.reviews').filter(function(review) {
return !review.get('isNew');
});
}.property('model.reviews')

Related

Count generated by model (through pivot table) not shown

I am trying to get the count of how many people participated in a scan.
at this moment i need 2 counts:
Total participants count
Total finished count
i made a pivot table in Ember that holds specific data about the participant, like he finished the scan or not.
My model:
import DS from 'ember-data';
var inflector = Ember.Inflector.inflector;
inflector.irregular('scan', 'scan');
var scanModel = DS.Model.extend({
title: DS.attr('string'),
mailInviteSubject: DS.attr('string'),
mailInviteMessage: DS.attr('string'),
mailResponseAddress: DS.attr('string'),
dateDeadline: DS.attr('date'),
scanGroup: DS.belongsTo('scanGroup', {async: true}),
questionGroups: DS.hasMany('question-group', {async: true}),
/**
* Participants
*/
scanParticipants: DS.hasMany('scanParticipants', {async: true}),
participants: Ember.computed('scanParticipants.#each.participants', function () {
return this.get('scanParticipants').then(function(scanParticipants) {
return scanParticipants.mapBy('participant');
})
}),
participantsCount: Ember.computed('scanParticipants', function () {
return this.get('scanParticipants').then(function (participants) {
return participants.get('length');
});
}),
participantsCountFinished: Ember.computed('scanParticipants', function () {
return this.get('scanParticipants').then(function (participants) {
return participants.filterBy('finished', true).get('length');
});
}),
isClosed: Ember.computed('dateDeadline', function () {
let scanDate = moment(this.get('dateDeadline'));
let date = moment();
if(scanDate < date)
return true;
else
return false;
}),
});
export default scanModel;
I have the following template:
<td>{{input type="checkbox" checked=isChecked action="scanChecked"}}</td>
<td>
<h2>{{scan.title}}</h2>
Verloopt op {{format-date scan.dateDeadline format="DD MMMM"}} <small class="_muted">({{#if scan.isClosed}}Afgerond{{else}}Over{{/if}} {{format-date scan.dateDeadline type='remaining'}} geleden)</small>
</td>
<td>
<h2>{{scan.participantsCount}}</h2>
<small class="_muted">uitgenodigd</small>
</td>
<td>
<h2>{{scan.participantsCountFinished}}</h2>
<small class="_muted">voltooid</small>
</td>
<td class="_align-right">
{{#link-to 'scangroup.scan' scan.id class="btn btn-inverted btn-small"}}Bekijk{{/link-to}}
</td>
The problem now is, is that {{scan.participantsCount}} and{{scan.participantsCountFinished}} shows [Object Object] in my template instead of the counts.
But if i log the counts in the promise i get the good count that should be displayed in the template.
How does it come that it show's [Object Object] instead of the count, and how can i make it possible to show the count?
Thanks in advance!
Kindly regards,
Pascal
You are observing the property itself instead of the array. This means that they will only re-compute when scanParticipants change value, like this.set('scanParticipants', someValue) and not when it updates. To fix this you can do Ember.computed('scanParticipants.[]', function() {.
You are seeing [Object Object] because that's the promise that's being returned from the computed properties. When you return a promise from a computed property you need to wrap it in a PromiseObject or PromiseArray, but Ember Data does this for you already for relationships.
There are a couple of approaches possible to fix this problem:
You can use {{scan.scanParticipants.length}} in the template
You can declare participantsCount: Ember.computed.alias('scanParticipants.length')
You can do participantsCount: Ember.computed('scanParticipants.[]', function () { return this.get('scanParticipants.length'); })
Notice that you also have an Ember.computed.filterBy method.

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

Ember.js: programmatically load hasmany relationship

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}}

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

Save foreign key to other Model with hasMany relation

I'm having the following problem.
In my app I have a screen to make a new Site. But when I save the new site via an action on the controller, the languages-property isn't sent with the POST-request to the server.
The template for adding a new Site is this:
<form class="form-horizontal">
<div class="control-group">
<label class="control-label" for="name">Name</label>
<div class="controls">
{{view Ember.TextField valueBinding="name"}}
</div>
</div>
<div class="control-group">
<label class="control-label" for="languages">Languages</label>
<div class="controls">
{{view Ember.Select contentBinding="controllers.languages" selectionBinding="languages" optionValuePath="content.id" optionLabelPath="content.description" multiple="true"}}
</div>
</div>
<div class="form-actions">
<button {{ action "createSite" }} class="btn btn-primary">Save</button>
</div>
I defined my Store like this:
App.Store = DS.Store.extend({
revision : 12,
adapter : DS.RESTAdapter.extend({
namespace : 'rest'
})
});
This is my controller:
App.SitesNewController = Em.ObjectController.extend({
needs: ['languages'],
name: null,
languages: null,
createSite : function() {
var self = this;
var name = this.get('name');
var languages = this.get('languages');
// Create the new Site model
var s = App.Site.createRecord({
name : name
});
$.each(languages,function(i,lang) {
s.get('languages').addObject(lang);
});
this.get('store').commit();
}
});
This is the Site-model
App.Site = DS.Model.extend({
name : DS.attr('string'),
languages : DS.hasMany('App.Language')
});
Language-model:
App.Language = DS.Model.extend({
description : DS.attr('string')
});
The POST-request data sent to my server is this:
{
"site":{"name":"test"}
}
So I miss the language-property. Actually I expect a language_ids property with an array of id's.
When I edit my RESTAdapter-configuration like this:
DS.RESTAdapter.map('App.Site', {
languages: { embedded: 'always' }
});
Now the POST-request data is:
{
"site": {
"name":"test",
"languages":[{
"id":2,"description":"English"
},{
"id":3,"description":"Deutsch"
}]
}
}
The languages are know embedded in the request-data. This is no problem, at the backend I get the id before I save it. But know it expects the language-data to be embedded in the GET-responses also.
What is the way to send just the id's in the POST-data? I want it to be something like this:
{
"site": {
"name":"test",
"languages":[2,3]
}
}
This answer is largely derived from this other StackOverflow answer.
App.Store = DS.Store.extend({
revision : 12,
adapter : DS.RESTAdapter.extend({
namespace : 'rest',
serializer: DS.RESTSerializer.extend({
addHasMany: function (hash, record, key, relationship) {
var type = record.constructor,
name = relationship.key,
serializedHasMany = [],
manyArray, embeddedType;
embeddedType = this.embeddedType(type, name);
if (embeddedType !== 'always') { return; }
manyArray = record.get(name);
manyArray.forEach(function (record) {
serializedHasMany.push(record.get('id'));
}, this);
hash[this.singularize(key) + '_ids'] = serializedHasMany;
}
})
})
});
For reference, you might review the JSONSerializer, which the RESTSerializer mostly inherits from. The code for what addHasMany does can be found here.
Note that, for the above snippet, the only lines that really differ are the last several. Rather than serializing embedded records, ids are pushed to the hash under the singularized key (I would have used RESTSerializer#keyForHasMany if it didn't have its own check for embedded types.