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"
}}
Related
ive searched for this and have not found an answer. I have 2 routes: "Index", which creates/updates an expense list, and "Charts", which charts the values of the expenses.
In order to handle the expense charts, I have the following function:
getData: function() {
var expenses = this.store.all('expense');
expenses.update();
var retarr = Ember.A();
expenses.forEach(function(expense) {
retarr.pushObject({
label: expense.get('name'),
value: expense.get('amount'),
group: 'expense'
});
});
return retarr;
}.property()
This is then passed to the ember-charts component in the Charts route.
<script type="text/x-handlebars" id='charts'>
<div class="chart-container">
{{horizontal-bar-chart data=getData}}
</div>
However, if I create/delete an expense in the "Index" route and hten transition to the "Charts" route, the DS.RecordArray doesn't update despite calling the "update()" function. As such, the chart does not reflect the created/deleted changes until the page is refreshed.
How do I fix this so the RecordArray auto updates along with the chart? I've broken my head for over two days trying different things. Thanks!
Your property getData should be bound to anything, if this something is an array you should use #each. For example as you can see here:
remaining: function() {
var todos = this.get('todos');
return todos.filterBy('isDone', false).get('length');
}.property('todos.#each.isDone')
I suggest you another approch, let's modify your model:
App.Chart = DS.Model.extend({
// fieds here...
label: function() {
return this.get("name");
}.property("name"),
value: function() {
return this.get("amount");
}.property("amount"),
group: function() {
return "expense";
}.property(),
)};
In your route set myCharts property:
App.ChartRoute = Ember.Route.extend({
setupController: function(controller, model) {
this._super(controller, model);
var charts = this.store.find("chart");
controller.set("myCharts", charts);
}
});
Then you could use your horizontal chart:
<div class="chart-container">
{{horizontal-bar-chart data=myCharts}}
</div>
Note: I didn't tested this code but it should work
I'm trying to create a reusable generated element that can react to changing outside data. I'm doing this in an included view and using computed.alias, but this may be the wrong approach, because I can't seem to access the generic controller object at all.
http://emberjs.jsbin.com/nibuwevu/1/edit
App = Ember.Application.create();
App.AwesomeChartController = Ember.Object.extend({
data: [],
init: function() {
this.setData();
},
setData: function() {
var self = this;
// Get data from the server
self.set('data', [
{
id: 1,
complete: 50,
totoal: 100
},
{
id: 2,
complete: 70,
total: 200
}
]);
}
});
App.IndexController = Ember.Controller.extend({
needs: ['awesome_chart']
});
App.ChartView = Ember.View.extend({
tagName: 'svg',
attributeBindings: 'width height'.w(),
content: Ember.computed.alias('awesome_chart.data'),
render: function() {
var width = this.get('width'),
height = this.get('height');
var svg = d3.select('#'+this.get('elementId'));
svg.append('text')
.text('Got content, and it is ' + typeof(content))
.attr('width', width)
.attr('height', height)
.attr('x', 20)
.attr('y', 20);
}.on('didInsertElement')
});
And the HTML
<script type="text/x-handlebars">
<h2> Welcome to Ember.js</h2>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="index">
<h2>Awesome chart</h2>
{{view App.ChartView width=400 height=100}}
</script>
For what it's worth, this didn't seem to work as a component, either. Is the ApplicationController the only place for code that will be used on multiple pages? The 'needs' seems to work, but the nested view can't access it. If I make a proper Ember.Controller instance to decorate the view, that doesn't seem to work either.
Any help is much appreciated.
Update:
I can't edit my comment below, but I found a good answer on how to use related, and unrelated, models in a single route.
How to use multiple models with a single route in EmberJS / Ember Data?
Firstly, your controllers should extend ObjectController/ArrayController/Controller
App.AwesomeChartController = Ember.Controller.extend({...});
Secondly when you create a view the view takes the controller of the parent, unless explicitly defined.
{{view App.ChartView width=400 height=100 controller=controllers.awesomeChart}}
Thirdly you already had set up the needs (needed a minor tweak), but just as a reminder for those reading this, in order to access a different controller from a controller you need to specify the controller name in the needs property of that controller.
App.IndexController = Ember.Controller.extend({
needs: ['awesomeChart']
});
Fourthly from inside the view your computed alias changes to controller.data. Inside the view it no longer knows it as AwesomeChart, just as controller
content: Ember.computed.alias('controller.data')
Fifthly inside your on('init') method you need to actually get('content') before you attempt to display what it is. content doesn't live in the scope of that method.
var content = this.get('content');
http://emberjs.jsbin.com/nibuwevu/2/edit
First, AwesomeChart does sound like it's gonna be a reusable self-contained component. In which case you should better user Ember.Component instead of Ember.View (as a bonus, you get a nice helper: {{awesome-chart}}).
App.AwesomeChartComponent = Ember.Component.extend({ /* ... */ });
// instead of App.ChartView...
Second, for AwesomeChart to be truly reusable, it shouldn't be concerned with getting data or anything. Instead, it should assume that it gets its data explicitly.
To do this, you basically need to remove the "content:" line from the awesome chart component and then pass the data in the template:
{{awesome-chart content=controllers.awesomeChart.data}}
Already, it's more reusable than it was before. http://emberjs.jsbin.com/minucuqa/2/edit
But why stop there? Having a separate controller for pulling chart data is odd. This belongs to model:
App.ChartData = Ember.Object.extend();
App.ChartData.reopenClass({
fetch: function() {
return new Ember.RSVP.Promise(function(resolve) {
resolve([
{
id: 1,
complete: 50,
total: 100
},
{
id: 2,
complete: 70,
total: 200
}
]);
// or, in case of http request:
$.ajax({
url: 'someURL',
success: function(data) { resolve(data); }
});
});
}
});
And wiring up the model with the controller belongs to route:
App.IndexController = Ember.ObjectController.extend();
App.IndexRoute = Ember.Route.extend({
model: function() {
return App.ChartData.fetch();
}
});
Finally, render it this way:
{{awesome-chart content=model}}
http://emberjs.jsbin.com/minucuqa/3/edit
I'm evaluating Emberjs in the context of whether we could port some of our code to it but maintain the same api so my first day really looking at it.
I'm using the Tom Dale tutorial but WITHOUT ember-data. I think I have kinda figured out how to get data into the app (thx #kingping2k). I just need to get this to save / update.
I have a doneEditing action which gets called when I click on it as seen by console but how do I get a reference to the model. Looking at the controller docs (http://emberjs.com/api/classes/Ember.Controller.html), I don't see a really obvious property like model or something. How would I tell the PostController to save the post that it is getting back in its route? Also, do people normally use jQuery promises to do something else after the save has completed here(I'm assuming yes)?
I've included the relevant code with the doneEditing action at the bottom where I'm looking for help:
thx for any help
Model:
Hex.Post = Ember.Object.extend({
id: null,
body: null,
isEnabled: null,
createdAt: null,
save: function(data){
console.log("you want to save this item");
$.post( "api/post", data, function( data ) {
// something here
});
}
});
View:
<script type="text/x-handlebars" id="post">
{{#if isEditing}}
{{partial 'post/edit'}}
<button {{action 'doneEditing'}}>Done</button>
{{else}}
<button {{action 'edit'}}>Edit</button>
{{/if}}
<h1>{{id}}</h1>
{{outlet}}
</script>
Route:
Hex.PostRoute = Ember.Route.extend({
model: function(params) {
console.log('called with: ' + params.post_id);
return Hex.Post.findById(params.post_id);
}
});
Controller:
Hex.PostController = Ember.ObjectController.extend({
isEditing: false,
actions:{
edit: function() {
this.set('isEditing', true);
},
doneEditing: function() {
this.set('isEditing', false);
console.log("this gets called");
//this.get('content').save();
//this.save();
//console.log("here: " + this.model.id);
//this.model.save(); //doesn't work ???
// this.post.save(); //doesn't work ???
//this.get('store').commit(); // prob no
}
}
});
when you return a model from the model hook it's then passed to the setupController in the route. The default implementation of setupController does this, controller.set('model', model)
setupController:function(controller, model){
controller.set('model', model');
}
so to get the model within the context of the controller just get that property.
var model = this.get('model')
I would return the promise, then you can trigger something on save/failure etc
save: function(){
console.log("you want to save this item");
return Ember.$.post( "api/post", JSON.stringify(this));
}
doneEditing: function() {
this.set('isEditing', false);
var model = this.get('model');
model.save().then(function(){alert('saved');},
function(){alert('failure');});
}
And generally you'll put save in the reopen
Hex.Post.reopen({
save: function(){
console.log("you want to save this item");
return Ember.$.post( "api/post", JSON.stringify(this));
}
});
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'),
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.