I was reading http://code418.com/blog/2012/03/26/advanced-emberjs-bindings/ and came across Ember.Binding.and for transform which has deprecated in the current emberjs for Ember.computed. I decided to update the old emberjs 0.9.x fiddle http://jsfiddle.net/Wjtcj/ to work with emberjs 1.x and provided an Ember.computed.and as shown in the new fiddle http://jsfiddle.net/Wjtcj/5/. Though it works, i cant make it return thesame output as the old one but when an improved version of the code http://jsfiddle.net/Wjtcj/28/ fails with
STATEMANAGER: Sending event 'navigateAway' to state root.
STATEMANAGER: Sending event 'unroutePath' to state root.
STATEMANAGER: Sending event 'routePath' to state root.
STATEMANAGER: Entering root.index
<error>
It seems the setSync function is the issue and fails because i am calling computed property on it.
The handlebars template:
<script type="text/x-handlebars" data-template-name="application" >
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="obj" >
{{#each App.ObjController}}
<p>{{this}}</p>
{{/each}}
</script>
update, please use this link for the updated code http://jsfiddle.net/Wjtcj/28/. The code below no more applies
App = Ember.Application.create();
Ember.computed.and = function(dependentKey, otherKey) {
return Ember.computed(dependentKey, otherKey, function(key) {
return get(this, dependentKey) && get(this, otherKey);
});
};
Ember.computed.or = function(dependentKey, otherKey) {
return Ember.computed(dependentKey, otherKey, function(key) {
return get(this, dependentKey) || get(this, otherKey);
});
};
App.ApplicationController = Em.Controller.extend();
App.ApplicationView = Ember.View.extend({
templateName: 'application'
});
App.ObjView = Em.View.extend({
templateName: 'obj'
});
App.ObjController = Ember.ArrayController.extend({
content: [],
user: Ember.Object.create({isAdmin: false, isOwner: false}),
isSelected: false,
isSaveEnabled: false,
canRead: false,
isSaveEnabledBinding: Ember.computed.and('user.isAdmin', 'isSelected'),
canReadBinding: Ember.computed.or('user.isAdmin', 'user.isOwner'),
setSync: function(property, value) {
this.set(property, value);
Ember.run.sync(); // synchronize bindings
this.pushObject('isSaveEnabled = %# ; canRead = %#'.fmt(this.get('isSaveEnabled'), this.get('canRead')));
}
});
App.ObjController.setSync('isSelected', false);
App.ObjController.setSync('user', Ember.Object.create({isAdmin: true, isOwner: false}));
App.ObjController.setSync('isSelected', true);
App.ObjController.setSync('user', Ember.Object.create({isAdmin: false, isOwner: true}));
App.ObjController.setSync('user', Ember.Object.create({isAdmin: false, isOwner: false}));
App.Router = Ember.Router.extend({
enableLogging: true,
location: 'hash',
root: Ember.Route.extend({
index: Ember.Route.extend({
route: '/',
connectOutlets: function(router) {
router.get('applicationController').connectOutlet('application');
}
}),
obj: Ember.Route.extend({
route: '/obj',
enter: function(router) {
console.log("The obj sub-state was entered.");
},
index: Ember.Route.extend({
route: '/',
connectOutlets: function(router, context) {
router.get('applicationController').connectOutlet( 'obj');
}
})
})
})
});
Thanks for any suggestions or fix.
Lots of things going wrong in your example that I'm not sure this will be all that illustrative, but I think this is what you're trying to accomplish:
http://jsfiddle.net/machty/Wjtcj/31/
Important points
It's rare that you ever need to manually call Ember.run.sync() unless you're doing test cases or some other unusual circumstance.
You were trying to cram too many things in ObjController. The intended purpose is to display a list of Users and their privileges; I employed the common pattern of using an ArrayController to manage the list of Users, and then displayed each one with a UserView.
Your original <error> was due to trying to connect applicationController's outlet to... applicationController, hence the recursion and stack overflow
There's a difference between bindings and computed properties. If you're using computed properties, don't put 'Binding' at end of your property
So instead of this:
isSaveEnabledBinding: Ember.computed.and('user.isAdmin', 'isSelected'),
canReadBinding: Ember.computed.or('user.isAdmin', 'user.isOwner'),
Do this
isSaveEnabled: Ember.computed.and('isAdmin', 'isSelected'),
canRead: Ember.computed.or('isAdmin', 'isOwner'),
Related
I'm struggling with limiting a data set represented by an ArrayController that also relies upon an ObjectController as a decorator for actions and computed properties. When I define a computed 'results' property and have it return either 'content' or 'arrangedContent', it seems my ObjectController (itemController) is entirely bypassed, and no references to the 'shipment' model are included.
Route:
App.ShipmentsManifestsRoute = Ember.Route.extend({
model: function() {
return this.store.find('manifest');
}
})
Models:
App.Shipment = DS.Model.extend({
from: DS.attr("string"),
tracking: DS.attr("string"),
manifest: DS.hasMany("manifest", { async: true }),
received: DS.attr("number", {
defaultValue: function() {
return moment(Firebase.ServerValue.TIMESTAMP);
}
})
});
App.Manifest = DS.Model.extend({
upc: DS.attr("string"),
quantity: DS.attr("number", { defaultValue: 1 }),
condition: DS.attr("string", { defaultValue: 'New' }),
status: DS.attr("string", { defaultValue: 'Accept' }),
title: DS.attr("string"),
notes: DS.attr("string"),
shipment: DS.belongsTo("shipment", { async: true }),
});
Controllers:
App.ManifestController = Ember.ObjectController.extend({
actions: {
save: function() {
this.get('model').save();
}
},
receivedDate: function() {
return moment(this.get('shipment.received')).format('YYYY-MM-DD');
}.property('shipment.received')
})
App.ShipmentsManifestsController = Ember.ArrayController.extend({
itemController: 'manifest',
sortProperties: ['shipment.received'],
sortAscending: false,
results: function() {
return this.get('arrangedContent').slice(0, 10);
}.property('arrangedContent.[]')
})
Also worth noting is that my itemController actions essentially don't seem to exist when using 'results' to render my data set. I have some inline editing functionality baked in that calls the 'save' action on the itemController, and Ember is throwing an error that 'save' doesn't exist.
This all of course works as it should if I iterate over {{#each controller}} rather than {{#each results}}.
I guess ultimately the problem is that 'content' doesn't return all of the data elements / properties / computed properties that would otherwise be available on the controller.
Is there a best practice way around this limitation?
UPDATE:
The problem is definitely related to the missing itemController when referencing arrangedContent. When iterating over the ArrayController directly, my View is referencing App.ManifestController as the controller. However, when iterating over arrangedContent, my View is instead referencing App.ShipmentsManifestsController as the controller. Still unsure as to why that is.
UPDATE 2:
Based on this, it looks like my issue is a duplicate of Setting itemController on a filtered subset of an ArrayController's model
A work-around that involves additional handlebars parameters was offered, which I will try. But would still love any input on whether this is intended behaviour or a bug.
I have a list of posts, with a nested summary and full route.:
App.Router.map(function() {
this.resource('post', {path: ':post_id'}, function() {
this.route('summary', {path: '/'});
this.route('full');
});
});
The summary route is at path: /, so when I {{#link-to 'post' somePost}}, it arrives in post.summary.
When I change the model, i.e. route to someOtherPost, I would like to stay in the current leaf route (either summary or full).
How do I do this? I tried willTransition, but I'm not sure how to check if the model changed:
Here's a JSBin with what I have so far: http://emberjs.jsbin.com/benot/2/edit?js,output
This is a great use case for a dynamic link-to.
link-to Code
App.ApplicationController = Em.ArrayController.extend({
postLink: 'post'
});
Template
{{#each item in controller}}
<li>{{#link-to postLink item}}{{item.title}}{{/link-to}}</li>
{{/each}}
You can send an action from each child route, and it will go up the router (http://emberjs.com/guides/templates/actions/) until caught. At the parent route you can change the dynamic value of the link-to and bam, you're good to go.
Route Code
App.PostSummaryRoute = Em.Route.extend({
actions:{
didTransition: function(){
this.send('swapLink', 'post.summary');
}
}
});
App.PostFullRoute = Em.Route.extend({
actions:{
didTransition: function(){
this.send('swapLink', 'post.full');
}
}
});
App.ApplicationRoute = Ember.Route.extend({
model: function() {
return [
App.Post.create({id: '1', title: 'Foo', body: 'Lorem'}),
App.Post.create({id: '2', title: 'Bar', body: 'Ipsum'})
];
},
actions: {
swapLink: function(newLink){
console.log('swapLink ' + newLink);
this.controller.set('postLink', newLink);
}
}
});
http://emberjs.jsbin.com/dekokafu/1/edit
I got problem with initialization of application.
I create jsfiddle which simply works on my desktop but not on jsfiddle.
http://jsfiddle.net/zDSnm/
I hope you will catch the idea.
On the beginining od my aplication I have to get some values from rest and values to Ember.Select.
Depends on what is choosen all my connectOutlets functions use this value.
Here I get some data from REST
$.ajax({
url: 'https://api.github.com/repos/emberjs/ember.js/contributors',
dataType: 'jsonp',
context: this,
success: function(response){
[{login: 'a'},{login: 'b'}].forEach(function(c){
this.allContributors.addObject(App.Contributor.create(c))
},this);
}
})
and put it to my Select View:
{{view Ember.Select
contentBinding="App.Contributor.allContributors"
selectionBinding="App.Contributor.selectedContributor"
optionLabelPath="content.login"
optionValuePath="content.id" }}
{{outlet}}
And in every of my route I need to use this value, which is selected in this selection box
index : Ember.Route.extend({
route: '/',
connectOutlets: function(router){
router.get('applicationController').connectOutlet('oneContributor',App.Contributor.selectedContributor);
}
})
I'd also add observer to this selectedContributor value which calls connectOutlets of currentState (I know I shouldn't do this but I don't know why and how should I do this in properly way)
App.Contributor.reopenClass({
//...
refresh : function(){
App.router.currentState.connectOutlets(App.router);
}.observes('selectedContributor'),
//...
I hope there is some good way to solve such problem.
If there is something not clear please leave comment.
If I understand correctly you want to show the currently selected contributor. One way to do it is to listen for a change in the selected contributor and send a transitionTo action to the Router.
First the router:
index : Ember.Route.extend({
route: '/',
showContibutor: Ember.Route.transitionTo('show'),
showNoneSelected: Ember.Route.transitionTo('noneSelected'),
connectOutlets: function(router){
router.applicationController.connectOutlet({ name: 'contributors', context: App.Contributor.find() });
},
// if no contributor is selected, the router navigates here
// for example, when a default option "Make a selection" is selected.
noneSelected: Ember.Route.extend({
route: '/'
}),
show: Ember.Route.extend({
route: '/:contributor_id'
connectOutlets: function(router, context){
router.applicationController.connectOutlet({name: 'contributor', context: context})
},
exit: function(router) {
// This will remove the App.ContributorView from the template.
router.applicationController.disconnectOutlet('view');
}
})
})
with a template for App.ContributorsView:
{{view Ember.Select
contentBinding="controller"
selectionBinding="controller.selectedContributor"
optionLabelPath="content.login"
optionValuePath="content.id"}}
{{outlet}}
and an ArrayController to manage contributors:
App.ContributorsController = Ember.ArrayController.extend({
onSelectedContributorChange: function() {
var selectedContributor = this.get('selectedContributor');
if (selectedContributor) {
App.router.send('showContributor', selectedContributor);
} else {
App.router.send('showNoneSelected');
}
}.observes('selectedContributor')
});
The user a selects a contributor and the contributorsController tells the router to show the view for the contributor (i.e, show the view App.ContributorView with context selectedContributor).
App.ContributorView = Ember.View.extend({
templateName: 'contributor'
});
controller for selected contributor:
App.ContributorController = Ember.ObjectController.extend({
// define events and data manipulation methods
// specific to the currently selected contributor.
});
Hope this helps.
UPDATE: If you need to show the first record by default, the noneSelected route should look like this:
noneSelected: Ember.Route.extend({
route: '/',
connectOutlets: function(router){
var context = router.contributorsController.get('content.firstRecord');
router.applicationController.connectOutlet({name: 'contributor', context: context})
}
})
And define firstRecord in ContributorsController:
App.ContributorsController = Ember.ArrayController.extend({
firstRecord: function() {
return this.get('content') && this.get('content').objectAt(0)
}.property('content.[]')
});
Haven't tested it, but it should work.
I read the Ember Application Structure guide and now I trying to create a simple one page application with ember.js.
My home page shows a sidebar containing a list of Post objects. When I click on a list-item, on the right of the sidebar I show a read-only version of the selected item. This version has a 'Edit' button, which makes it possible to edit the item. The edit version has a 'Cancel' link to switch back to the read-only version.
I got this all working, however, when navigating back to the read-only version, the url in the address bar is not updated properly. When navigating back to my read-only version I expect the url to change from 'example.com/#posts/123/edit' to 'example.com/#posts/123', but instead I get ''example.com/#posts/undefined'.
I tried to provide a context when calling transitionTo in the 'cancel' event, but that doesn't work.
How can I navigate back to my read-only from while keeping the url pointing to the proper post (example.com/#posts/123)?
Most of my code is identical to the example in the ember guide, my router and 'edit' related code is shown below:
App.EditPostView = Em.View.extend({
templateName: 'edit_post'
});
App.Post = DS.Model.extend({
title: DS.attr('string'),
body: DS.attr('string'),
published: DS.attr('boolean')
});
App.Router = Em.Router.extend({
enableLogging: true,
location: 'hash',
root: Em.Route.extend({
index: Em.Route.extend({
route: '/',
redirectsTo: 'posts.index'
})
}),
posts: Em.Route.extend({
route: '/posts', # example.com/#posts
showPost: Em.Route.transitionTo('posts.show'),
editPost: Em.Route.transitionTo('posts.edit'),
index: Em.Route.extend({
route: '/',
connectOutlets: function(router) {
router.get('applicationController').connectOutlet('posts', App.Post.find());
}
}),
show: Em.Route.extend({
route: '/:post_id', # example.com/#posts/123
connectOutlets: function(router, post) {
router.get('postsController').connectOutlet('post', post);
}
}),
edit: Em.Route.extend({
route: '/:post_id/edit', # example.com/#posts/123/edit
connectOutlets: function(router, post) {
router.get('postsController').connectOutlet({
viewClass: App.EditPostView,
controller: router.get('postController'),
context: post
});
},
}),
cancel: function(router, event) {
router.transitionTo('show'); # Expect this to use 'example.com/#posts/123' but instead it shows 'example.com/#posts/undefined'
}
})
});
# edit_post.handlebars:
<form {{action save on="submit"}}>
...
{{view Em.TextField valueBinding="title"}}
{{view Em.TextArea valueBinding="body"}}
...
<a {{action cancel}} class="btn">Cancel</a>
</form>
You are missing the context in transitionTo calls. You should have something like:
showPost: function (router, event) {
var post = event.context;
Em.Route.transitionTo('posts.show', post);
},
editPost: function (router, event) {
var post = event.context;
Em.Route.transitionTo('posts.edit', post);
},
I've done a sample Ember.js integration with Chosen (https://github.com/harvesthq/chosen)
Coffeescript:
App.ChosenSelectView = Em.Select.extend({
didInsertElement: ->
#_super()
#$().chosen()
# Assumes optionLabelPath is something like "content.name"
#addObserver(#get("optionLabelPath").replace(/^content/, "content.#each"), -> #contentDidChange())
contentDidChange: ->
# 2 ticks until DOM update
Em.run.next(this, (-> Em.run.next(this, (-> #$().trigger("liszt:updated")))))
})
The thing that bothers me is I don't have a good idea about how much time do I need before triggering update on the Chosen widget. From my experiments 2 run loops is ok, but maybe there is a better way for the whole thing?
Full example at jsfiddle: http://jsfiddle.net/oruen/qfYPy/
I think the problem is that your observer is notified kind of too early, meaning that the changes have not yet been written to the DOM.
I've hacked a little around and in the end I came up with a solution, which calls Ember.run.sync() before the event for the chosen plugin is triggered, see http://jsfiddle.net/pangratz666/dbHJb/
Handlebars:
<script type="text/x-handlebars" data-template-name="selectTmpl" >
{{#each items tagName="select" }}
<option {{bindAttr value="id"}} >{{name}}</option>
{{/each}}
</script>
JavaScript:
App = Ember.Application.create({});
App.items = Ember.ArrayProxy.create({
content: [Ember.Object.create({
id: 1,
name: 'First item'
}), Ember.Object.create({
id: 2,
name: 'Second Item'
})]
});
Ember.View.create({
templateName: 'selectTmpl',
itemsBinding: 'App.items',
didInsertElement: function() {
this.$('select').chosen();
},
itemsChanged: function() {
// flush the RunLoop so changes are written to DOM?
Ember.run.sync();
// trigger the 'liszt:updated'
Ember.run.next(this, function() {
this.$('select').trigger('liszt:updated');
});
}.observes('items.#each.name')
}).append();
Ember.run.later(function() {
// always use Ember.js methods to acces properties, so it should be
// `App.items.objectAt(0)` instead of `App.items.content[0]`
App.items.objectAt(0).set('name', '1st Item');
}, 1000);
Michael Grosser posted his working ChosenSelectView here: http://grosser.it/2012/05/05/a-chosen-js-select-filled-via-ember-js
This might work for you on Ember 1.0 and Chosen v0.12:
JavaScript:
App.ChosenSelect = Ember.Select.extend({
chosenOptions: {width:'100%', placeholder_text_multiple: 'Select Editors', search_contains: true},
multiple:true,
attributeBindings:['multiple'],
didInsertElement: function(){
var view = this;
this._super();
view.$().chosen(view.get('chosenOptions'));
// observes for new changes on options to trigger an update on Chosen
return this.addObserver(this.get("optionLabelPath").replace(/^content/, "content.#each"), function() {
return this.rerenderChosen();
});
},
_closeChosen: function(){
// trigger escape to close chosen
this.$().next('.chzn-container-active').find('input').trigger({type:'keyup', which:27});
},
rerenderChosen: function() {
this.$().trigger('chosen:updated');
}
});
Handlebars:
{{view App.ChosenSelect
contentBinding="options"
valueBinding="selectedOption"
optionLabelPath="content.name"
}}