I'm trying to figure out how to build a small app consisting of a list where you can select multiple items and toggle to select all/none and see the number of currently selected rows.
I believe that the "selected" state should not be part of the model objects, but I cannot really figure out how to do it.
This is my current setup (which doesn't work obviously yet)
Runnable code http://jsfiddle.net/jacobk/rU35G/1/
var App = Ember.Application.create();
App.ApplicationRoute = Ember.Route.extend({
model: function() { return Ember.A(["Foo", "Bar", "Baz"]); }
});
App.ApplicationController = Ember.ArrayController.extend({
allSelected: false,
selectedCount: function() {
return 0;
}.property()
});
App.RowController = Ember.ObjectController.extend({
isSelected: false
});
<script type="text/x-handlebars" data-template-name="application">
<h3>{{ selectedCount }} rows selected.</h3>
<label>
{{view Ember.Checkbox checkedBinding="allSelected"}}
Toggle select all
</label>
<hr/>
<ul>
{{#each controller itemController="row"}}
<li {{bindAttr class="isSelected"}}>
{{view Ember.Checkbox checkedBinding="isSelected"}} {{this.content}}
</li>
{{/each}}
</ul>
</script>
Should the individual "row items" be controlled using a custom view per row, or a custom controller like in the fiddle above
How to propagate the "select all" from the ArrayController to all the individual controllers (or views if that's a better fit)
I'm trying to understand when to use bindings, observers, properties, "needs" etc. and when its appropriate to use controllers vs views and so on. I've yet to grok the general flow of information/data in ember apps.
e.g. should the ArrayController from my example above iterate over the "contained" views/controllers and change the "selected" state when the "select all" check box is toggled OR should all the "sub controllers" observe/"have bindings to" the ArrayController and change themselves when it changes, and if so, how should I propagate data the opposite direction. How would the ArrayController get "all currently selected" rows?
I would love to see the "canonical solution" for this.
No need of row controller. #each, computed property and checkedbinding can be utilized to solve this as shown below. isSelected has to be defined in content of the arraycontroller:
App.ApplicationController = Ember.ArrayController.extend({
allSelected: function(key, value) {
if ( value !== undefined ) {
// when check box is ticked, this gets executed
this.setEach( 'isSelected', value );
return value;
} else {
//as a computed property
return !!this.get( 'length' ) &&
this.everyProperty( 'isSelected', true );
}
}.property('#each.isSelected')
selectedCount: function() {
return 0;
}.property()
});
I agree about keeping the selected state out of the model. You need to define the itemController in the Ember.ArrayController.
here is a working example. http://jsbin.com/sunat/3/edit
App.RowController = Ember.ObjectController.extend({
isSelected: false
});
App.IndexController = Ember.ArrayController.extend({
itemController: 'row',
selectAll: function(key, value) {
if (arguments.length == 2) {
this.setEach('isSelected', value);
return value;
} else {
return this.isEvery('isSelected', true);
}
}.property('#each.isSelected')
});
#template
<script type="text/x-handlebars" id="index" >
<label>
{{input type="checkbox" checked=selectAll}}
Toggle select all
</label>
<hr/>
<ul>
{{#each}}
<li>
{{input type="checkbox" checked=isSelected}} {{name}}
</li>
{{/each}}
</ul>
</script>
Related
My templates:
<form name="spam_list" {{action 'spamDecide' on='submit'}}>
{{#each model as |spam|}}
<tr>
<td>{{input type="checkbox" name="trash[]" checked=spam.isCheck}}</td>
</tr>
{{/each}}
<button {{action 'checkAll'}}>Check All</button>
</form>
My routes:
model(){
// return 10 spams
},
My controller:
actions:{
spamDecide: function(model){
var isCheck = model.getEach('isCheck');
console.log(isCheck);
},
checkAll: function(model){
var model = this.get('model');
model.setEach('isCheck',true);
},
}
Currently action checkAll is work fine, set all checkbox into checked. My question is how can I know which checkbox is checked when I manually check them.
For example I set checkbox 1 5 7 is checked, then get the equivalent spam.id with each check?
If I read correctly, what you're looking for is to get a list of checked objects in spamDecide. You should be able to do it along these lines:
actions:{
spamDecide: function(model){
var checkedModels = model.filterBy('isCheck');
console.log(checkedModels);
},
}
While Array#getEach (alias for Array#mapEach) returns an array of each item's isCheck property, filterBy returns the items that have the isCheck property set to true.
If you want the ids you can then do checkedModels.mapBy('id').
I hope that was helpful!
My aim is to pass filtered data to my controller and then to my template. I've tried not using the filter and everything works as expected. If I even try to use a filter that lets everything through, I don't get any data. I've even tried using false instead of true and fiddling with the argument list of the filter.
I'm using ember-data fixtures to test this. I'm following the name conventions so much of the work is done for me under the hood. That all seems to be working though (otherwise the first example should also have a problem).
Works (arrives in the controller and eventually gets rendered on the page):
App.DomainDirRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('domain_dir');
}
});
Fails (controller gets an empty array):
App.DomainDirRoute = Ember.Route.extend({
model: function(params) {
return this.store.filter('domain_dir', function(item){
return true;
});
}
});
UPDATE (ATTEMPT 1):
Okay, so I've tried a couple of things based on Sam Selikoff's answer. I've defined 4 properties (2 filters, one map, one plain copy) in the controller and tried to display each in the mockup page. Only the property copyDomain gives a result.
Properties in controller:
filteredDomains: Ember.computed.filterBy('domain', 'domain', true),
upperCaseDomains: Ember.computed.map('domain', function(domain, index) {
return domain.toUpperCase() + '!';
}),
filteredDomains2: function() {
return this.get("model").filterBy('domain', true);
}.property('model.#each.domain'),
copyDomains: function(){
result = [];
this.forEach(function(item) {
result.pushObject(item);
})
console.log(result);
return result;
}.property('model.#each.domain')
Mockup:
<ul>
<li>filteredDomains</li>
{{#each domainDir in controller.filteredDomains}}
<li>domainDir.domain</li>
{{/each}}
</ul>
<ul>
<li>filteredDomains2</li>
{{#each domainDir in controller.filteredDomains2}}
<li>domainDir.domain</li>
{{/each}}
</ul>
<ul>
<li>upperCaseDomains</li>
{{#each domainDir in controller.upperCaseDomains}}
<li>domainDir.domain</li>
{{/each}}
</ul>
<ul>
<li>copyDomains</li>
{{#each domainDir in controller.copyDomains}}
<li>domainDir.domain</li>
{{/each}}
</ul>
Filtering is generally done at the controller/component level. store.find makes an AJAX request. Is your goal to only retrieve the filtered subset of data from the server, or to filter the data you already have at the view layer?
Typically if you're just wanting to do some live filtering, you'll do it in the controller. Leave your model hook as this.store.find('domain_dir') and add a filter in your controller:
App.DomainDirController = Ember.Controller.extend({
filteredDomains: function() {
return this.get("model").filterBy('someProp', true);
}.property('model.#each.someProp')
});
You should also check out the computed macros for some shorthands:
App.DomainDirController = Ember.Controller.extend({
filteredDomains: Ember.computed.filterBy('model', 'someProp');
});
Now in your template, you can do
{{#each domain in filteredDomains}}
...
After looking into Ryan Niemeyer's brilliant knockout-postbox.js
I made some minor adapting and want the menu to open with a preselected section. Hence I added a variable to the initialization of the selectedSection observable as seen below.
var MenuModel = function() {
var preselected = "Profile";
this.name = ko.observable().subscribeTo("nickName");
this.sections = ["Profile", "Notifications","Users","Projects"];
this.selectedSection = ko.observable(preselected).publishOn("section");
};
This selects the desirable section, however the contents of the section remains invisible.
Here's the preselected sections viewModel:
var ProfileModel = function() {
//apply a filter to turn the value that we receive into a boolean
this.visible = ko.observable().subscribeTo("section", function(newValue) {
return newValue === "Profile";
});
//some more code - syncing and subscribing/publishing observables.
};
The HTML goes like this:
<div id="menu">
<h2><span data-bind="text: name"></span>'s Settings</h2>
<ul class="nav nav-pills" data-bind="foreach: sections">
<li data-bind="css: { active: $root.selectedSection() === $data }">
</li>
</ul>
</div>
<div id="profile" data-bind="visible: visible">
<h3>Profile</h3>
<label>Nick name: <input id="nick" data-bind="value: nickName" /> </label>
<label>Email: <input data-bind="value: emailAddress" /></label>
</div>
The question is, how can I trigger the visible observable of the ProfileModel with a preselected setting of MenuModel's selectedSection observable, such that the contents are shown?
Full fiddle: http://jsfiddle.net/AsleG/b6gwn6ak/
The subscribeTo helper can take a boolean as a second argument to indicate that you want to initialize it using the last published value. Your compare function can then be passed as the third arg. It would look like:
//apply a filter to turn the value that we receive into a boolean
this.visible = ko.observable().subscribeTo("section", true, function(newValue) {
return newValue === "Profile";
});
New Ember user here,
I am having an issue trying to get a dropdown view to be populated with initial values from a model on transition to any one of multiple edit routes. I am currently using fixture data...
My router is setup as such:
StoryTime.Router.map(function () {
this.resource('projects', function(){
this.resource('project', { path: '/:project_id' }, function(){
this.resource('stories', function(){
this.resource('story', { path: '/:story_id' }, function(){
this.route('edit');
});
});
this.route('edit');
this.route('report');
this.route('export');
});
this.route('new');
});
});
with a route for editing a story as:
StoryTime.StoryEditRoute = Ember.Route.extend({
setupController: function (controller, model) {
controller.set('model', this.controllerFor('story').get('model'));
}
});
and my controller setup as:
StoryTime.StoryEditController = Ember.ObjectController.extend({
needs:['story', 'project'],
actors: Ember.computed.alias('controllers.project.actors'),
selectedActor: null,
updateActor: function(){
var actor = this.get('selectedActor'),
model = this.get('model');
model.set('actor', actor);
}.observes('selectedActor'),
actions: {
//actions...
}
});
and my template has this piece in question in it:
<div class="form-group">
<label class="control-label col-sm-2 text-left no-padding">Actor:</label>
<div class="controls col-sm-10">
{{model.actor.name}}: {{model.actor.id}}
{{view Ember.Select
name = "actorSelect"
content = actors
optionLabelPath = "content.name"
optionValuePath = "content.id"
selectionBinding = "selectedActor"
class = "form-control"
}}
</div>
</div>
My preferable setup would have:
The model being given to the controller (set by the story edit route to be the model given to the stories route) populate the dropdown box's initial value, but then binds the subsequent selection to the controllers attribute for processing, and
The controller changes the "selectedActor" attribute to null again on transition to another edit route.
Right now, this is not happening. Here's what is happening:
When I navigate to .../stories/1/edit for example, the dropdown is not populated to the model's actor value, but rather the first thing in the list
If I change the value it changes the controller's model's actor, as expected.
However, on subsequent transitions to ../stories/2/edit the dropdown is still populated with the old selected value due to "selectedActor" being set to it.
Can anyone illuminate as to what I am missing here? I feel like there has to be way to both work with the Ember Select view and a way to reset attributes of a particular route or controller on transition. Am I incorrect on this thinking?
Thanks for any insight!
Yeah, fanta is right. You just need to remove a whole bunch of code from your controller and modify your template slightly. Replace your controller and template like so:
StoryTime.StoryEditController = Ember.ObjectController.extend({
needs:['story', 'project'],
actors: Ember.computed.alias('controllers.project.actors'),
actions: {
//actions...
}
});
<div class="form-group">
<label class="control-label col-sm-2 text-left no-padding">Actor:</label>
<div class="controls col-sm-10">
{{actor.name}}: {{actor.id}}
{{view Ember.Select
name = "actorSelect"
content = actors
optionLabelPath = "content.name"
optionValuePath = "content.id"
selectionBinding = actor
class = "form-control"
}}
</div>
</div>
Note you don't need model references in your template. Ember will automatically pass the references back to the underlying model. The primarily job of the controller is to decorate and provide the model (and handle actions & events) to the template / views.
I recently started using Ember.js. In my small application I currently have problems regarding Ember.computed.alias, because an {{#if}}-section is updated properly, but the bind-attr helper in the same template is not updated accordingly.
The application controller and the action influencing the value look as follows:
App.ApplicationController = Ember.ObjectController.extend({
isEditing: false,
actions: {
toggleEdit: function() {
var a = this.get('isEditing');
this.set('isEditing', !a);
}
}
});
The controller taking care of the template causing problems:
App.CategoriesController = Ember.ArrayController.extend({
needs: ['application'],
isEditing: Ember.computed.alias('controllers.application.isEditing'),
general: function() { // example depending on the alias
var result = this.filterBy('type', 1);
if (!this.get('isEditing')) {
result = result.filterBy('isHidden', false);
}
return result;
}.property('#each.type', '#each.isHidden', 'isEditing'),
// ......
The related template:
<ul id="categories">
{{#if isEditing}}YES!{{else}}NO!{{/if}}
{{#each general}}
<li {{bind-attr class=":general isEditing:editing"}}>
{{name}}
</li>
{{/each}}
</ul>
When the action toggleEdit is triggered, the {{#if}} section is updated and swaps between YES! and NO!, but the editing class is not applied to the list element. I tried encapsulated the alias into another property of the controller depending on the alias, but without success.
I assume it's a beginners mistake, but I can't figure out what I am overlooking.
Thanking you in anticipation.
isEditing is no longer in scope, use controller.isEditing, sorry phone response
Here's an example that would keep it in scope, but I'm fully qualifying it just to show you.
{{#each item in general}}
<li {{bind-attr class=":general controller.isEditing:editing"}}>
{{item.name}}
</li>
{{/each}}