After upgrading to 1.13 I get this exception and I can't figure out what's the issue. I also couldn't find any helpful resource which tackles my issue.
It happens for properties I set in another computed property. But this property is definitely called only once.
I have created a jsbin example: http://emberjs.jsbin.com/roderameya/edit?html,js,console,output
UPDATE
As requested I post some code which is more close to my real implementation.
Ember.Controller.extend({
filter: '',
resultCount: {
total: 0,
matches: 0,
mismatches: 0
},
results: function() {
var items = this.get('model'),
matches = [],
resultCount = {};
// Apply search filter
matches = items.filter(function(item){
// Just a dummy filter function
return true;
});
// We need the total number matched by the filter string
resultCount.total = matches.length;
// The already matched items must be narrowed further down
matches = matches.filter(function(item) {
// Another filter function
return true;
});
resultCount.matches = matches.length;
resultCount.mismatches = resultCount.total - matches.length;
this.set('resultCount', resultCount);
return matches;
}.property('model', 'filter'),
});
Try to have your properties not set other properties, but rather depend on each other:
App.IndexController = Ember.Controller.extend({
count: function() {
return this.get("data.length") || 0;
}.property('data.length'),
data: [1,2,3]
});
Updated jsbin for you.
UPDATE
Basically, your resultCount is a temporary variable that we can get rid of, and the rest are just chaining computed properties together:
updated jsbin for advanced example
code:
// Index controller
App.IndexController = Ember.Controller.extend({
count: Ember.computed('filteredItems.length', function(){
return this.get('filteredItems.length');
}),
data: [
Ember.Object.create({ name: "jim", age: 15 }),
Ember.Object.create({ name: "jeff", age: 42 }),
Ember.Object.create({ name: "eric", age: 7 })
],
filter: RegExp(".*"),
ageFilter: -1,
mismatchCount: Ember.computed('filteredItems.length', 'secondPassFilteredItems.length', function() {
return this.get('filteredItems.length') - this.get('secondPassFilteredItems.length');
}),
filteredItems: Ember.computed('data', 'filter', function() {
var controller = this;
return this.get('data').filter(function(item) {
return item.get('name').match(controller.get("filter"));
});
}),
secondPassFilteredItems: Ember.computed('filteredItems', 'ageFilter', function() {
var controller = this;
var ageFilter = controller.get("ageFilter");
if (Ember.isEqual(ageFilter, -1)) {
return this.get('filteredItems');
} else {
return this.get('filteredItems').filter(function(item) {
// more filtering
return item.get("age") <= ageFilter;
});
}
}),
results: Ember.computed.alias('secondPassFilteredItems'),
actions: {
filterByJ: function() {
this.set('filter', new RegExp("j"));
},
filterByEric: function() {
this.set('filter', new RegExp("eric"));
},
filterAllNames: function() {
this.set('filter', new RegExp(".*"));
},
filterYoungins: function() {
this.set('ageFilter', 15);
},
filterAllAges: function() {
this.set('ageFilter', -1);
}
}
});
Template usage:
<script type="text/x-handlebars" data-template-name="index">
<p>Results found: {{count}}</p>
<p>Diff between first and second filter: {{mismatchCount}}</p>
<p>First Filter:
<button {{action 'filterAllNames'}}>all people</button>
<button {{action 'filterByJ'}}>People with J in name</button>
<button {{action 'filterByEric'}}>People named 'eric'</button>
</p>
<p> Second Filter:
<button {{action 'filterAllAges'}}>all ages</button>
<button {{action 'filterYoungins'}}>15 years old or younger</button>
</p>
<ul>
{{#each results as |item|}}
<li>{{item.name}} is {{item.age}} years old</li>
{{/each}}
</ul>
</script>
Related
I am tryin to count the position of an object. That means which numeric position an item has. That cannot be the primary key because they are not persistent; a record can get deleted.
Asume we have this "model" (a simple array for now, I use Ember Data):
posts = [
{id: 4, number: 104, title: 'Post 4'},
{id: 2, number: 102, title: 'Post 2'},
{id: 3, number: 103, title: 'Post 3'},
];
So at to allow sorting in the postsController we do:
this.controllerFor('posts').set('sortProperties', ['id']); // or maybe just sorting on 'number'
this.controllerFor('posts').set('sortAscending', true);
In the template I want to show the current post and the total number of posts {{currentPostCount}} of {{totalPostCount}}
In postController I have the following computed properties:
App.PostController = Ember.ObjectController.extend({
posts: function () {
return this.store.all('posts');
}.property(),
currentCount: function () {
// if there is an id get the position of the record
// that will be the position count of the post record
var id = this.get('id');
if (id) {
console.log('this item has an id: ' + id);
var count = 0;
var currentCount;
// loop over posts to check at which position the current post is
this.get('posts').filter(function (item) {
count++;
if (item.id == id) {
console.log('yay id found! count is: ' + count)
currentCount = count;
}
});
return currentCount;
}
}.property('posts.#each'),
totalCount: function () {
var posts= this.get('posts');
return posts.get('length');
}.property('posts.#each')
});
Edit: add model:
App.ApplicationRoute = Ember.Route.extend({
model: function() {
this.controllerFor('posts').set('model', this.store.find('post'));
// sort posts by id
this.controllerFor('posts').set('sortProperties', ['id']);
//(...)
}
});
App.PostsController = Ember.ArrayController.extend();
.
Update: full working simple pagination in Ember.js
In your template:
<div id="pagination">
<span>Post {{index}}/{{totalCount}}</span>
<a {{action previousPost this}} href="#">Previous</a>
<a {{action nextPost this}} href="#">Next</a>
</div>
The PostsController:
App.PostsController = Ember.ArrayController.extend({
sortProperties: ['id'], // I sort on ID, but you can sort on any property you want.
sortAscending: true,
assignIndex: function () {
this.map(function (item, index) {
Ember.set(item, 'index', index + 1)
})
}.observes('content.[]', 'firstObject', 'lastObject')
});
Actions in the PostController:
previousPost: function (post) {
var newIndex = (post.index - 1);
var previousPost = this.store.all('post').findBy('index', newIndex);
if (previousPost) {
this.transitionToRoute('post', previousPost);
}
},
nextPost: function (post) {
var newIndex = (post.index + 1);
var nextPost = this.store.all('post').findBy('index', newIndex);
if (nextPost) {
this.transitionToRoute('post', nextPost);
}
}
I have two computed properties in the PostController. However you can better use the following in your PostsController to do it the Ember way, like kingpin2k said. Then you can also omit the posts property.
posts: function () {
return this.store.all('posts');
}.property(),
// totalcount below the page for pagination
totalCount: function () {
var posts= this.get('posts');
return posts.get('length');
}.property('posts.#each'),
You can maintain an index on array content during sorting or removing objects.
In PostsController:
App.PostsController=Em.ArrayController.extend({
assignIndex:function(){
this.map(function(item,index){Em.set(item,'index',index+1)})
}.observes('content.[]','firstObject','lastObject'),
//other contents to follow...
In posts template the property index is available which is dynamically updated when adding or removing or sorting objects.
In Posts Template:
{{#each controller}}
<p>{{index}}. {{name}}</p>
{{/each}}
Caveat: This is part of my first ember app.
I have an Ember.MutableArray on a controller. The corresponding view has an observer that attempts to rerender the template when the array changes. All the changes on the array (via user interaction) work fine. The template is just never updated. What am I doing wrong?
I'm using Ember 1.2.0 and Ember Data 1.0.0-beta.4+canary.7af6fcb0, though I guess the latter shouldn't matter for this.
Here is the code:
var ApplicationRoute = Ember.Route.extend({
renderTemplate: function() {
this._super();
var topicsController = this.controllerFor('topics');
var topicFilterController = this.controllerFor('topic_filter');
this.render('topics', {outlet: 'topics', controller: topicsController, into: 'application'});
this.render('topic_filter', {outlet: 'topic_filter', controller: topicFilterController, into: 'application'});
},
});
module.exports = ApplicationRoute;
var TopicFilterController = Ember.Controller.extend({
topicFilters: Ember.A([ ]),
areTopicFilters: function() {
console.log('topicFilters.length -> ' + this.topicFilters.length);
return this.topicFilters.length > 0;
}.property('topicFilters'),
getTopicFilters: function() {
console.log('getTopicFilters....');
return this.store.findByIds('topic', this.topicFilters);
}.property('topicFilters'),
actions: {
addTopicFilter: function(t) {
if(this.topicFilters.indexOf(parseInt(t)) == -1) {
this.topicFilters.pushObject(parseInt(t));
}
// this.topicFilters.add(parseInt(t));
console.log('topicFilters -> ' + JSON.stringify(this.topicFilters));
},
removeTopicFilter: function(t) {
this.topicFilters.removeObject(parseInt(t));
console.log('topicFilters -> ' + JSON.stringify(this.topicFilters));
}
}
});
module.exports = TopicFilterController;
var TopicFilterView = Ember.View.extend({
topicFiltersObserver: function() {
console.log('from view.... topicFilters has changed');
this.rerender();
}.observes('this.controller.topicFilters.[]')
});
module.exports = TopicFilterView;
// topic_filter.hbs
{{#if areTopicFilters}}
<strong>Topic filters:</strong>
{{#each getTopicFilters}}
<a {{bind-attr href='#'}} {{action 'removeTopicFilter' id}}>{{topic}}</a>
{{/each}}
{{/if}}
var TopicsController = Ember.ArrayController.extend({
needs: ['topicFilter'],
all_topics: function() {
return this.store.find('topic');
}.property('model', 'App.Topic.#each'),
actions: {
addTopicFilter: function(t) {
App.__container__.lookup('controller:topicFilter').send('addTopicFilter', t);
}
}
});
module.exports = TopicsController;
// topics.hbs
<ul class="list-group list-unstyled">
{{#each all_topics}}
<li class="clear list-group-item">
<span class="badge">{{entryCount}}</span>
<a {{bind-attr href="#"}} {{action 'addTopicFilter' id}}>{{topic}}</a>
</li>
{{/each}}
</ul>
your observes should just be controller.topicFilters.[]
And honestly this is a very inefficient way of doing this, rerendering your entire view because a single item changed on the array. If you show your template I can give you a much better way of handling this.
Here's an example, I've changed quite a few things, and guessed on some others since I don't know exactly how your app is.
http://emberjs.jsbin.com/uFIMekOJ/1/edit
I'm using a Multiselect view:
{{view Ember.Select
multiple="true"
contentBinding="App.filtersProductController"
selectionBinding="App.filtersController.products"
optionLabelPath="content.fullName"
optionValuePath="content.id"
isVisibleBinding="App.filtersController.productListBox"}}
Is it possible to preselect multiple values in the "select" box and to change the selected values programmatically? Background: I want to save different combinations of three "select" boxes settings as bookmarks. When loading a bookmark, I have to set the "select" boxes values.
Thank you
Yes. In your controller you have to create a property to keep the selected value or values when working with Ember.Select.
In the code below I'm setting the Greetings as the content of the select box, in the controller that lists those Greetings (check ApplicationRoute), I also have a property called selectedItems which I'm binding to the Select and I'm using a couple other properties to filter the values I want to pre-select (1 and 3) in case none of the items are already selected when the view loads.
This will render a multiple select box with the items which the id are either 1 or 3 marked as selected. You can see the source here: http://jsfiddle.net/schawaska/Y8P4m/
Handlebars:
<script type="text/x-handlebars">
<h1>Test</h1>
{{view Ember.Select
multiple="true"
selectionBinding="controller.selectedItems"
contentBinding="controller"
optionLabelPath="content.text"
optionValuePath="content.id"}}
</script>
JavaScript:
window.App = Ember.Application.create();
App.Store = DS.Store.extend({
revision: 11,
adapter: 'DS.FixtureAdapter'
});
App.Greeting = DS.Model.extend({
text: DS.attr('string'),
when: DS.attr('date'),
selected: false,
isSelected: function() {
return this.get('selected');
}.property('selected')
});
App.ApplicationController = Em.ArrayController.extend({
preselected: function() {
return this.get('content').filter(function(greeting) {
return greeting.get('id') == 1 ||
greeting.get('id') == 3;
});
}.property('content.#each'),
selectedItems: function() {
if(this.get('selected.length') <= 0) {
return this.get('preselected');
} else {
return this.get('selected');
}
}.property('selected', 'preselected'),
selected: function() {
return this.get('content').filter(function(greeting) {
return greeting.get('isSelected');
})
}.property('content.#each')
});
App.Greeting.FIXTURES = [
{id: 1, text: 'First', when: '3/4/2013 2:44:52 PM'},
{id: 2, text: 'Second', when: '3/4/2013 2:44:52 PM'},
{id: 3, text: 'Third', when: '3/4/2013 2:44:52 PM'},
{id: 4, text: 'Fourth', when: '3/4/2013 3:44:52 PM'}
];
App.ApplicationRoute = Em.Route.extend({
setupController: function(controller) {
controller.set('model', App.Greeting.find());
}
});
I have created a complete example with single and multi "select" elements. You can set defaults and change the selected value programmatically or by using the "select" GUI element. The controller code:
// class for single selects
App.SingleSelectFilterController = Ember.ArrayController.extend({
selection: null,
active: true,
update: function(id) {
this.set("selection", id);
},
getSelectedId: function() {
return this.get("selection");
}
});
// class for multi selects
App.MultiSelectFilterController = Ember.ArrayController.extend({
selection: null,
active: true,
update: function(selectionIds) {
// Workaround: Reinitializing "content". How to do it well?
var contentCopy = [];
for(i = 0; i < this.get("content").length; i++) {
contentCopy.push(this.get("content")[i]);
}
this.set("content", contentCopy);
this.set("selection", selectionIds);
},
selected: function() {
var me = this;
return this.get('content').filter(function(item) {
for(i = 0; i < me.get("selection").length; i++) {
if(me.get("selection")[i] === item.get('id')) { return true; }
}
return false;
});
}.property('content.#each'),
getSelectedIds: function() {
var ids = [];
for(i = 0; i < this.get("selected").length; i++) {
ids.push(this.get("selected")[i].get("id"));
}
return ids;
}
});
// create single and multi select controllers
App.metricController = App.SingleSelectFilterController.create();
App.metricController.set("content", App.filterData.get("metrics"));
App.metricController.set("selection", "views"); // set default value for single select element
App.platformController = App.MultiSelectFilterController.create();
App.platformController.set("content", App.filterData.get("platforms"));
App.platformController.set("selection", ["plat-black"]); // set default value for multi select element
And the complete example:
http://jsfiddle.net/7R7tb/2/
Thanks to MilkyWayJoe for his help!
Perhaps somebody knows how to fix the workaround (see the code comment above)?
I have a ContainerView that swaps other views in and out. I use another ContainerView as content. Trying to swap in the nested ContainerView, after I swapped it out, results in the error: Uncaught Error: assertion failed: calling set on destroyed object.
Here's the fiddle: http://jsfiddle.net/hekevintran/bFSKD/
To make the error happen, click "Other Form" and then click "First Form".
I think the error is because views that are removed from ContainerViews are destroyed and the child views of the nested ContainerView are not recreated. What's the right way to fix this example?
Templates:
<script type="text/x-handlebars" data-template-name="box">
<div>
{{#each forms}}
<button {{action "selectForm" this }}>{{this.name}}</button>
{{/each}}
{{view container}}
</div>
</script>
<script type="text/x-handlebars" data-template-name="form">
<form>
{{#each fields}}
<div>
{{this.label}}: {{view this.widget}}
</div>
{{/each}}
</form>
</script>
JavaScript:
App = Ember.Application.create({});
App.BoxController = Ember.Object.extend({
initialForm: null,
currentForm: null,
init: function () {
var form = this.get('initialForm');
this.set('currentForm', form);
this.get('container').set('currentView', form.get('view').create());
},
forms: [],
container: function () {
return Ember.ContainerView.create({
boxController: this,
controllerBinding: 'boxController.currentForm'
})
}.property(),
selectForm: function (form) {
this.set('currentForm', form);
this.get('container').set('currentView', form.get('view').create());
}
});
App.Field = Ember.Object.extend({
value: null,
widgetBaseClass: Ember.TextField,
widget: function () {
return this.get('widgetBaseClass').extend({
field: this,
valueBinding: 'field.value'
});
}.property('widgetBaseClass')
});
App.RangeField = App.Field.extend({
widget: function () {
var field = this;
return Ember.ContainerView.extend({
childViews: [field.get('select1').create(), field.get('select2').create()]
});
}.property('select1', 'select2'),
fromValue: null,
toValue: null,
value: function () {
return [this.get('fromValue.value'), this.get('toValue.value')];
}.property('fromValue', 'toValue'),
choices: [
'1',
'2',
'3',
'4'
],
remainingChoices: function () {
var fromValue = this.get('fromValue');
if (fromValue) {
var choices = this.get('choices');
var index = choices.indexOf(fromValue);
return choices.slice(index + 1);
}
return [];
}.property('fromValue', 'choices'),
select1: function () {
return Ember.Select.extend({
field: this,
valueBinding: 'field.fromValue',
contentBinding: 'field.choices'
});
}.property(),
select2: function () {
return Ember.Select.extend({
field: this,
valueBinding: 'field.toValue',
contentBinding: 'field.remainingChoices',
contentHasChangedOnce: false,
contentChanged: function () {
// Set the initial value only once
if (! this.get('contentHasChangedOnce')) {
this.set('contentHasChangedOnce', true);
this.set('value', this.get('content')[0]);
}
// Reset the value if the chosen value is no longer
// available
if (! this.get('content').contains(this.get('value'))) {
this.set('value', this.get('content')[0]);
}
}.observes('content')
});
}.property()
});
App.Form = Ember.Object.extend({
fieldNames: [],
fields: function () {
var that = this;
var out = [];
_.each(this.get('fieldNames'), function (fieldName) {
out.pushObject(that.get(fieldName));
});
return out;
}.property('fieldNames')
});
aForm = App.Form.create({
name: 'First Form',
fieldNames: [
'a',
'b'
],
a: App.Field.create({label: 'A'}),
b: App.RangeField.create({label: 'B'}),
view: Ember.View.extend({
templateName: 'form'
})
});
var boxController = App.BoxController.create({
initialForm: aForm,
forms: [
aForm,
Ember.Object.create({
name: 'Other Form',
view: Ember.View.extend({
template: Ember.Handlebars.compile('Foobar')
})
})
]
});
var boxView = Ember.View.create({
templateName: 'box',
controller: boxController
});
boxView.append();
The issue is that you are instantiating select1 and select2 when you create the class extending ContainerView inside the widget method of App.RangeField.
Change this code:
App.RangeField = App.Field.extend({
widget: function () {
var field = this;
return Ember.ContainerView.extend({
childViews: [field.get('select1').create(), field.get('select2').create()]
});
}.property('select1', 'select2'),
...
}
to this:
App.RangeField = App.Field.extend({
widget: function () {
var field = this;
return Ember.ContainerView.extend({
init: function() {
this.set('childViews', [field.get('select1').create(), field.get('select2').create()]);
this._super();
}
});
}.property('select1', 'select2'),
...
}
Now you create new child views each time you instantiate widget instead of re-using the same two views which were destroyed the first time you removed them from the DOM.
Given a view with a context like { id: 1, form_id: 5}, I want to create an {{action}} link to the form using the form_id.
My view code looks like:
<script type="text/x-handlebars" data-template-name="group">
{{action showForm form_id href=true}}
</script>
And the action in my router looks like:
showForm: function(router, event) {
var form_id = event.context;
router.transitionTo('root.form', { id: form_id });
},
I get an error that reads:
Uncaught Error: assertion failed: You must specify a target state for event 'showForm' in order to link to it in the current state 'root.index'.
I'm guessing that the problem is with the way I'm setting up the context for transitionTo, but I haven't been able to figure out the correct solution.
Here is the full code to reproduce the problem:
<script type="text/x-handlebars" data-template-name="application">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="group">
{{action showForm form_id href=true}}
</script>
MyApp = Ember.Application.create({
autoinit: false
});
MyApp.router = Ember.Router.create({
root: Ember.Route.extend({
index: Ember.Route.extend({
route: '/',
// Throws error:
// You must specify a target state for event 'showForm' in
// order to link to it in the current state 'root.index'
//
showForm: function(router, event) {
var form_id = event.context;
router.transitionTo('root.form', { id: form_id });
},
// Won't work because form deserialize finds id, not form_id
//showForm: Em.Route.transitionTo('root.form'),
// This won't work either
// showForm: Em.Route.transitionTo('root.form', { id: this.form_id }),
connectOutlets: function( router, context ){
var group = Em.Object.create({ id:1, form_id: 5 });
router.get( 'applicationController' ).connectOutlet( 'group', group );
}
}),
form: Ember.Route.extend({
route: '/form/:id',
serialize: function( router, context ){
return { id: context.id }
},
deserialize: function( router, context ){
var form = Em.Object.create({ id: 5, name: 'my form' });
return MyApp.Form.find( context.id );
},
connectOutlets: function( router, context ){
// left out for fiddle example
}
})
})
});
MyApp.ApplicationController = Ember.Controller.extend({});
MyApp.GroupController = Em.ObjectController.extend({});
MyApp.GroupView = Em.View.extend({ templateName: 'group' });
MyApp.initialize(MyApp.router);
And the cooresponding fiddle:
http://jsfiddle.net/jefflab/LJGCz/
I was able to come up with an ugly solution to this problem using a computed property as the context of my action. The key snippets are:
<script type="text/x-handlebars" data-template-name="group">
<a {{action showForm view.formHash href=true}}>go to form</a>
</script>
MyApp.GroupView = Em.View.extend({
templateName: 'group',
formHash: function() {
return { id: this.get('controller').get('form_id') };
}.property('form_id')
});
And the working fiddle is here:
http://jsfiddle.net/jefflab/pGv8u/
However, after talking to Tom Dale, it is clear that the "right way" to solve to solve this problem is by using materialized objects instead of id values. If you are using Ember data, this is a great use case for the "sideloading" belongsTo feature.