Summary
I have a problem with a list displayed by Ember which keeps displaying extra rows each time it is visited. The extra rows are duplicates of those which were initially displayed.
Detail
In an Emberjs 2.13.0 app I have a model that looks like this :
import DS from 'ember-data';
export default DS.Model.extend({
cceIdentifierParent: DS.attr('string'),
cchCceIdParent: DS.attr('string'),
nodeType: DS.attr('number')
});
I have a route, 'diagcctreetoplevelonly', which looks like this :
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.findAll('diagcctreetoplevelonly');
}
});
And a template that looks like this :
{{diag-warningbanner}}
{{#if model.length}}
<table>
<thead>
<tr>
<th>
cceIdentifierParent
</th>
<th>
cchCceIdParent
</th>
<th>
nodeType
</th>
</tr>
</thead>
<tbody>
{{#each model as |treenode|}}
<tr>
<td>
{{treenode.cceIdentifierParent}}
</td>
<td>
{{treenode.cchCceIdParent}}
</td>
<td>
{{treenode.nodeType}}
</td>
</tr>
{{/each}}
</tbody>
</table>
{{else}}
<p id="blankslate">
No Tree Nodes found
</p>
{{/if}}
{{outlet}}
That works fine the first time that 'diagcctreetoplevelonly' is visited - 12 rows are rendered - but on subsequent visits (without the underlying data having changed) the table rendered by the template has 12 extra rows for each time it has been visited.
Can anyone explain what i'm doing wrong ? Thank you.
EDIT: Thanks to the input from #Jeff and #Subtletree I was able to resolve this.
The problem was that the data returned had no 'id' attribute and when I created one the problem went away.
Because of the peculiar nature of the data it didn't actually matter what the id was and I didn't want to make changes to the backend so I created an id dynamically once the data had arrived on the client by creating a model level serializer and overriding the extractId method like this :
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
extractId(modelClass, resourceHash) {
var arrId = [];
arrId.push(resourceHash['attributes']['cceIdentifierParent']);
arrId.push(resourceHash['attributes']['cchCceIdParent']);
arrId.push(resourceHash['attributes']['nodeType']);
var id = arrId.join('|');
return id == null || id === '' ? null : id+'';
},
});
It wouldn't have worked in all (perhaps most ?) situations but for my case this was good enough and resolved the problem.
To provide credit where it's due I got the idea for how to do this from the answer by #Casey here https://stackoverflow.com/a/35738573/364088 .
When ember-data receives records from a server it tries to match them to records already in the store by their id. If no id's are present then it can't find a match so instead of updating them it will just add them.
You could add an id to each record or could fetch the data with ajax and not use ember-data for this model.
Related
I have a basic app having using the Fixture Data Adapter and have a model with some basic seed data.
I am having an issue getting the new emberjs 2.0 to list multiple items in a model. The objects are loaded and showing in them ember inspector but are not looping through and displaying in the browser. I have been able to list one at a time but when i switch to adding multiple items it is blank. Any help would be great.
I have updated with some more information. Here is the route for the list i am trying to list out.
<!--dashboard/route.js -->
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.findAll('dashboard');
}
});
Here is the template file
<!-- dashboard/template.hbs -->
<tbody>
<tr>
{{#each dashboards as |dashboard|}}
<td>dashboards.status</td>
<td>dashboards.orderid</td>
{{/each}}
</tr>
</tbody>
I think I have everything setup right but cant get it to work.
Thanks!
Give this a try for the model hook. It doesn't need to be a hash.
model: function() {
return this.store.findRecord('dashoard', 2);
}
And if you want to return all the dashboards
model: function() {
return this.store.findAll('dashoard');
}
Then in dashboard/template.hbs
{{order-list dashboards=model}}
Now you have access to all your dashboards in your component
{{#each dashboards as |dashboard|}}
<td>dashboard.status</td>
<td>dashboard.orderid</td>
{{/each}}
When you fetch something in a model, it sets the model property on the respective controller.
So, instead of looping through dashboards, you need to do this:
<tbody>
<tr>
{{#each model as |dashboard|}}
<td>dashboards.status</td>
<td>dashboards.orderid</td>
{{/each}}
</tr>
</tbody>
An alternative is to use the setupController hook to set up the dashboards property on the controller. Something like this:
setupController (controller, model) {
this._super(...arguments)
controller.set('dashboards', model)
}
This isn't necessary; you're basically creating a dashboards alias that points to model. So, now, when you loop through dashboards, it loops through the model.
You can find more about how a controller is set up here: http://guides.emberjs.com/v2.0.0/routing/setting-up-a-controller/
How can I specify a class to an item when clicked with Ember. I am using a Handlebars action on the elements of a table to sort by properties. I want to add a class to the property being sorted so I can show the user the current property being sorted. How can I do this?
I have an ember controller shown below:
App.UsersController = Ember.ArrayController.extend
title: 'Users'
count: Ember.computed.alias 'length'
sortProperties: ['username']
actions:
sort: (property) ->
console.log property
if #get('sortProperties')[0] is property
#set('sortAscending', !#get 'sortAscending')
else
#set 'sortProperties', [property]
#set('sortAscending', true)
The controller allows me to click on headings in a table to sort the table. The html is shown below:
<thead>
<tr>
<th>Action</th>
<th class="sort" {{action sort 'last'}}>Last</th>
<th class="sort" {{action sort 'first'}}>First</th>
<th class="sort" {{action sort 'username'}}>Username</th>
<th class="sort" {{action sort 'email'}}>Email</th>
</tr>
</thead>
Create a currentSort property (optional)
First, I created a currentSort property on your App.UsersController which cleans a little bit the code. We'll use it later.
App.UsersController = Ember.ArrayController.extend
sortProperties: ['username']
currentSortBinding: 'sortProperties.firstObject'
actions:
sort: (sort) ->
if sort is #get('currentSort')
#toggleProperty 'sortAscending'
else
#setProperties
currentSort: sort
sortAscending: true
Define a custom view for each <th>
You'll then have to define a custom view for the <th> which will do 2 things:
Have a class active-sort when the sort of the view is the current
Change the controllers current sort when clicking the view
It will looks like this:
App.SortView = Ember.View.extend
template: Ember.Handlebars.compile('{{view.sortName}}')
tagName: 'th'
sortName: null # (will be different for each <th> : `last`, `first`,..)
classNameBindings: [ ':sort', 'isCurrent:active-sort' ]
isCurrent: (->
#get('sortName') is #get('controller.currentSort')
).property('sortName', 'controller.currentSort')
click: ->
var newSort = #get('sortName');
#get('controller').send('sort', newSort);
Here we customized the view class names, and we handled click event on the view.
Insert a custom view for each sort
This is really simple to insert views in templates:
<thead>
<tr>
{{view App.SortView sortName="default"}}
{{view App.SortView sortName="price"}}
{{view App.SortView sortName="alphabetical"}}
</tr>
</thead>
You can test all of this in a working JSBin
I don't know if this is the best workaround, but you could have some "sort flag" that you could bind CSS on.
In your controller (in "classic" javascript) :
sortedByLast : function() { return this.get("sortProperties")[0] === "last" }.property("sortProperties.#each")
// etc ...
In your template :
<th {{bind-attr class=":sort sortedByLast:current-sort"}} {{action sort 'last'}}>Last</th>
So the sort class would always be on, and the current-sort would only be there if it match its proper flag.
I am trying to create a simple page where I loop through a list of phone numbers associated with a Contact. Each phone number has a "number" and a "phone_type".
I've created a View that extends Ember.Select that populates itself with a the list of phone_types. Other than that, is's just a plain Ember.Select:
export default Ember.Select.extend({
thestore: '',
optionLabelPath: 'content.code',
optionValuePath : 'content.code',
didInsertElement: function() {
var vtype = this.get("valuetype");
var vls = this.get("thestore").filter('valuelist', { type: 'phone_type' }, function(vv) {
return vv.get("type") == vtype;
});
this.set("content",vls);
}
});
Here is my code in the template using the "valuelist" view defined above.
{{#each phonenumber in model}}
<tr>
<td> {{phonenumber.number}}</td>
<td>{{phonenumber.phone_type}}</td>
<td>{{view 'valuelist' thestore=store valuetype='phone_type'
selection="{{phonenumber.phone_type}}"
value="phonenumber.phone_type" }}</td>
</tr>
{{/each}}
What I cannot figure out is how to bind the value in the dropdown to the field in each model record I am iterating through in the template. You can see I've tried various things in the code above without any luck.
The property you need is value. However, in your attempts above, you were filling it with literal strings. This happens when you provide a value wrapped in quotes ('...' or "..."). What you need is to give it an identifier, which is value without quotes. So, try this:
{{#each phonenumber in model}}
<tr>
<td>{{phonenumber.number}}</td>
<td>{{phonenumber.phone_type}}</td>
<td>{{view 'valuelist' thestore=store valuetype='phone_type'
value=phonenumber.phone_type }}</td>
</tr>
{{/each}}
As an aside, this is a very unortodox way of doing things. A view shouldn't be tied to a store. Also, I think this will cause your select to be unusable while the values load asynchronously (and potentially crash your app if there is an error).
A conventional way to do this would be to load the list of all phone_types in your setupController hook and then provide it as an argument to Select view.
Controller:
App.MyRoute = Ember.Route.extend({
//...
setupController: function (c, m) {
c.set("model", m);
c.set("phoneTypes", [
"home", "office"
// or whatever, load it from store in model hook and setup here
]);
}
});
Template:
{{#each phonenumber in model}}
<tr>
<td>{{phonenumber.number}}</td>
<td>{{phonenumber.phone_type}}</td>
<td>{{view Ember.Select
content=phoneTypes
value=phonenumber.phone_type }}</td>
</tr>
{{/each}}
Using the following syntax to create a record, adds it to the Store immidiatly:
store.createRecord('post', {title: "Rails is omakase"});
What is the way to successfully store the record on the server and only then add it to the Store?
I would expect store.createRecord to return a promise. Very confusing.
One could argue that having the "potential" records in your local store isn't the problem, but having them show up in a list that is intended for showing saved records.
So, I made a component to use when I only want to show saved records:
app/components/each-saved.js
import Ember from 'ember';
const EachSavedComponent = Ember.Component.extend({
tagName: '',
savedRecords: Ember.computed.filterBy('records', 'isNew', false)
});
EachSavedComponent.reopenClass({
positionalParams: ['records']
});
export default EachSavedComponent;
app/templates/components/each-saved.hbs
{{#each savedRecords as |record|}}
{{yield record}}
{{/each}}
Usage
<div class="CARD">
<table class="LIST">
{{#each-saved model.users as |user|}}
<tr>
<td class="LIST-cell">{{user.name}}</td>
<td class="LIST-cell--right">Remove</td>
</tr>
{{/each-saved}}
</table>
</div>
I don't need it everywhere and there are a few cases where I do want unsaved records to show up in a list.
For what you mentioned I guess you're calling the server directly (instead of through the REST Adapter). If that is the case, you can:
var store = store;
$.ajax(url, {type: 'POST', success: function (savedRecord) {
var pushedRecord = store.push(App.MyModel, savedRecord);
pushedRecord.save();
});
I have a very simple requirement but like with many things in Ember.JS, I'm banging my head against the wall trying to get it implemented.
I have an overview screen where a couple of records are displayed in a table.
To render the overview screen I'm using the following Route
App.LocationsIndexRoute = Ember.Route.extend({
setupController: function(controller) {
var locations = App.Location.find();
controller.set('content', locations);
},
renderTemplate: function() {
this.render('locations.index',{into:'application'});
}
});
This is working fine.
I would now like to conditionally render the overviewtable.
If records are present render the table.
If no records are present display a message.
I tried implementing this using the following controller.
App.LocationsIndexController = Ember.ArrayController.extend({
locationsPresent: function() {
var model = this.get('content');
return model.content.length > 0;
}.property()
});
and the following template
{{#if locationsPresent}}
<table class="table table-hover">
<tr>
<th>Latitude</th>
<th>Longitude</th>
<th>Accuracy</th>
<th></th>
<th></th>
</tr>
{{#each location in model}}
<tr>
<td>{{location.latitude}}</td>
<td>{{location.longitude}}</td>
<td>{{location.accuracy}}</td>
<td>{{#linkTo locations.edit location}}Edit{{/linkTo}}</td>
<td><button {{action removeItem location}}>Delete</button></td>
</tr>
{{/each}}
</table>
{{else}}
No locations present.
{{/if}}
The computed locationsPresent property is called once, before the page is rendered. At that time I assume that the model is still being loaded as the length = 0.
When the page is rendered, the locations from the App.Locations.find() are available but the locationsPresent is not called anymore, meaning the page decided to render the No locations present. message.
I went through the Managing Asyncrony in Ember page and assumed that the computer property locationsPresent would be updated if the underlying model changed (if it was completely loaded) as the page states :
Using a computed property for author eliminated the need to explicitly invoke the computation in a callback when the underlying property changed.
I'd love to know what I'm doing wrong and how I can fix this but more importantly why I seem to be missing some of these core concepts of Ember.JS. If somebody can point me where in the docs / guides this is explained properly I'd love to know.
I think it is a easy fix. You need to add the property you are observing. like so:
locationsPresent: function() {
var length = this.get('content.length');
return length > 0;
}.property('content.#each')
adding the #each is necessary if locationsPresent needs to recalculate wen content is added. I think you can also observe 'content.isLoaded'