All children has rendered in collectionview Emberjs - ember.js

hi I am trying to build Coverflow in emberjs using Collectionview
I guess i will be needing css calculations after all children has rendered but i am not able to do it as call back is not triggered in my case
entire bin enter link description here
my collection view looks like this
App.CoverflowView = Ember.CollectionView.extend({
items: null,
itemSize: null,
props: ['width', 'Width', 'left', 'Left'],
itemWidth: null,
itemHeight: null,
duration: null,
current: null,
elementId: 'coverflow',
createChildView: function (viewClass, attrs) {
viewClass = App.CoverView;
return this._super(viewClass, attrs);
},
onChildViewsChanged: function (obj, key) {
console.log('check');
var length = this.get('childViews.length');
if (length > 0) {
Ember.run.scheduleOnce('afterRender', this, 'childViewsDidRender');
}
}.observes('childViews'),
childViewsDidRender: function () {
console.log('childViewsDidRender - count of paragraphs: ');
}
});

Regardless the needs of coverflow, i'm assuming it is required to fire the function for each view. In this setup the observer will not fire for property childViews for each child view and at first glance of the related code i don't see a property that gets set in order to observe it. So i propose a simple solution, to call the function from createChildView since this has been overriden and also keep observing the childViews property if the content changes dynamically at some point.
Example,
http://emberjs.jsbin.com/weleqabu/1 (look at the console)
createChildView: function(viewClass, attrs){
viewClass = App.CoverView;
this.onChildViewsChanged();
return this._super(viewClass,attrs);
}

Related

SelectAll feature with Ember ObjectController and ItemController

I am trying to implement a multiselect row in a table. The parent controller is just an object controller. It has a model, and the view iterates over the recordset of the model as individual rows.
I have implemented an itemController for all the rows in the model. That works.
But for the 'selectAll' functionality, in the parent controller, I am not able to get hold of all the items (individually). Do you have any idea how to go abt it?
Here's my work so far :
export
default Ember.ObjectController.extend({
// parent Controller
itemController: 'checkbox',
selectAll: function(key, value) {
var items = this.get('model.items');
if (arguments.length == 2) {
this.setEach('isSelected', value); //setEach is throwing an error sine it comes from ArrayController where as I am using ObjectController as the parent controller type
return value;
} else {
return this.isEvery('isSelected', true); //isEvery is also throwing error for the same reason
}.property('model.items.#each.isSelected')
And my Item Controller (checkboxcontroller) is as follows :
export default Ember.ObjectController.extend({
isSelected: false,
selectedListOfItems: [],
isSelectedChange: function() {
var selectedListOfItems = this.get('selectedListOfItems');
var itemId = this.get('id'); // comes from the model.items.id
debugger;
if (this.get('isSelected')) {
// add itemId to the selected array
var index = selectedListOfItems.indexOf(itemId);
if (index > -1) {
selectedListOfItems.splice(index, 1, itemId);
} else {
selectedListOfItems.push(itemId);
}
} else {
// remove itemId from the selected array
var index = selectedListOfItems.indexOf(itemId);
if (index > -1) {
selectedListOfItems.splice(index, 1);
}
}
this.set('selectedListOfItems', selectedListOfItems);
}.observes('isSelected')
});
My doubt is how do I do selectAll on the parent controller (that is of ObjectController type) that selects all the checkboxes of all the children. I am not sure if the info I've provided above is enough. Kindly let me know if you need more info. Thanks in advance
I got it working by adding a listener to the child (ItemController) that listens for any change in the parent's variable.
Here's what I did :
parentControllerDidChange: function() {
if (this.get('parentController.selectedAllItems')) {
this.set('isSelected', true);
} else {
this.set('isSelected', false);
}
}.observes('parentController.selectedAllItems')
That did the trick. Now If I toggle the boolean variable on the parent controller, all the children react. Ember the beauty !

Mutual Exclusion in an Ember ArrayController

I have a list of items in an array controller. Clicking on an item makes it "active", but I’d like only one item to be active at any a time (akin to radio buttons).
I have this working by storing the active item in a computed property, then toggling its active state in an action on the array controller, see: http://emberjs.jsbin.com/wavay/2/edit
However, this doesn’t handle the case where an item is made active by some other means i.e. not through the action.
I have experimented with observing the isActive change (using .observesBefore('#each.isActive')), and flipping the state of the activeItem, but of course, this approach causes an infinite loop.
Is there a better way?
This can be solved using a combination of Ember.reduceComputed and observers.
The removedItem and addedItem callbacks in Ember.reduceComputed are given access to the object which has changed, as well as instanceMeta, which can be used to store the “active” item:
App.IndexController = Ember.ArrayController.extend({
itemController: 'item',
activeItem: Ember.reduceComputed('#this.#each.isActive', {
initialValue: null,
removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) {
if (item.get('isActive')) {
var previousActiveItem = instanceMeta.activeItem;
if (previousActiveItem) previousActiveItem.set('isActive', false);
return instanceMeta.activeItem = item;
}
return instanceMeta.activeItem = null;
},
addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) {
return instanceMeta.activeItem;
}
})
…
However if activeItem is not accessed anywhere, removedItem and addedItem will never be called, and therefore items will remain active until they are manually toggled. To fix this, an observer can be set up to call this.get('activeItem') whenever an isActive property is changed:
setActiveItem: function () {
this.get('activeItem');
}.observes('#each.isActive')
See the updated jsbin: http://emberjs.jsbin.com/wavay/3/edit?js,output
Related: David Hamilton’s presentation on Array Computing Properties.
A possible solution based on your toggleActive implementation is available here.
This solution works if the "active" flag is updated only with the toggleActive controller method. So far the controller represents the state, it makes sense that provides the api to update its data correctly.
App.IndexController = Ember.ArrayController.extend({
itemController: 'item',
activeItem: function() {
return this.findBy('isActive');
}.property('#each.isActive'),
toggleActiveModel: function(model) {
var controller = this.findBy('model', model);
this._toggleActive(controller);
},
_toggleActive: function(controller) {
var previouslyActive = this.get('activeItem');
if(previouslyActive && previouslyActive !== controller) {
previouslyActive.set('isActive', false);
}
controller.set('isActive', !controller.get('isActive'));
},
actions: {
toggleActive: function(controller) {
this._toggleActive(controller);
},
toggle: function(modelValue) {
this.toggleActiveModel(modelValue);
}
}
});

How to dynamically add and remove views with Ember.js

I am trying to create an interface for traversing tables in a relation database. Each select represents a column. If the column is a foreign key, a new select is added to the right. This keeps happening for every foreign key that the user accesses. The number of selects is dynamic.
I made a buggy implementation that has code that manually adds and removes select views. I think it probably can be replaced with better Ember code (some kind of array object maybe?), I'm just not sure how to best use the framework for this problem.
Here's my JSBin http://jsbin.com/olefUMAr/3/edit
HTML:
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="Ember template" />
<meta charset=utf-8 />
<title>JS Bin</title>
<script src="http://code.jquery.com/jquery-1.9.0.js"></script>
<script src="http://builds.emberjs.com/handlebars-1.0.0.js"></script>
<script src="http://builds.emberjs.com/tags/v1.1.2/ember.js"></script>
</head>
<body>
<script type="text/x-handlebars" data-template-name="my_template">
{{view fieldSelects}}
</script>
<div id="main"></div>
</body>
</html>
JavaScript:
App = Ember.Application.create();
var TemplatedViewController = Ember.Object.extend({
templateFunction: null,
viewArgs: null,
viewBaseClass: Ember.View,
view: function () {
var controller = this;
var viewArgs = this.get('viewArgs') || {};
var args = {
template: controller.get('templateFunction'),
controller: controller
};
args = $.extend(viewArgs, args);
return this.get('viewBaseClass').extend(args);
}.property('templateFunction', 'viewArgs'),
appendView: function (selector) {
this.get('view').create().appendTo(selector);
},
appendViewToBody: function () {
this.get('view').create().append();
}
});
var DATA = {};
DATA.model_data = {
"Book": {
"fields": [
"id",
"title",
"publication_year",
"authors"
],
"meta": {
"id": {},
"title": {},
"publication_year": {},
"authors": {
"model": "Author"
}
}
},
"Author": {
"fields": [
"id",
"first_name",
"last_name",
"books"
],
"meta": {
"id": {},
"first_name": {},
"last_name": {},
"books": {
"model": "Book"
}
}
}
};
var Controller = TemplatedViewController.extend({
view: function () {
var controller = this;
return this.get('viewBaseClass').extend({
controller: controller,
templateName: 'my_template'
});
}.property(),
selectedFields: null,
fieldSelects: function () {
var filter = this;
return Ember.ContainerView.extend({
controller: this,
childViews: function () {
var that = this;
var selectedFields = filter.get('selectedFields');
var ret = [];
var model = 'Book';
selectedFields.forEach(function (item, index, enumerable) {
var selection = item;
if (model) {
var select = that.makeSelect(model, that.getPositionIndex(), selection, true).create();
ret.pushObject(select);
model = DATA.model_data[model].meta[selection].model;
}
});
return ret;
}.property(),
nextPositionIndex: 0,
incrementPositionIndex: function () {
this.set('nextPositionIndex', this.get('nextPositionIndex') + 1);
},
getPositionIndex: function () {
var index = this.get('nextPositionIndex');
this.incrementPositionIndex();
return index;
},
setNextPositionIndex: function (newValue) {
this.set('nextPositionIndex', newValue+1);
},
makeSelect: function (modelName, positionIndex, selection, isInitializing) {
var view = this;
return Ember.Select.extend({
positionIndex: positionIndex,
controller: filter,
content: DATA.model_data[modelName].fields,
prompt: '---------',
selection: selection || null,
selectionChanged: function () {
var field = this.get('selection');
// Remove child views after this one
var lastIndex = view.get('length') - 1;
if (lastIndex > this.get('positionIndex')) {
view.removeAt(this.get('positionIndex')+1, lastIndex-this.get('positionIndex'));
view.setNextPositionIndex(this.get('positionIndex'));
}
if (! isInitializing && DATA.model_data[modelName].meta[field].model) {
var relatedModel = DATA.model_data[modelName].meta[field].model;
view.pushObject(view.makeSelect(relatedModel, view.getPositionIndex()).create());
}
// Reset ``isInitializing`` after the first run
if (isInitializing) {
isInitializing = false;
}
var selectedFields = [];
view.get('childViews').forEach(function (item, index, enumerable) {
var childView = item;
var selection = childView.get('selection');
selectedFields.pushObject(selection);
});
filter.set('selectedFields', selectedFields);
}.observes('selection')
});
}
});
}.property()
});
var controller = Controller.create({
selectedFields: ['authors', 'first_name']
});
$(function () {
controller.appendView('#main');
});
Approach:
I would tackle this problem using an Ember Component.
I have used a component because it will be:
Easily reusable
The code is self contained, and has no external requirements on any of your other code.
We can use plain javascript to create the view. Plain javascript should make the code flow easier to understand (because you don't have to know what Ember is doing with extended objects behind the scenes), and it will have less overhead.
Demo:
I have created this JSBin here, of the code below.
Usage
Add to your handlebars template:
{{select-filter-box data=model selected=selected}}
Create a select-filter-box tag and then bind your model to the data attribute, and your selected value array to the selected attribute.
The application:
App = Ember.Application.create();
App.ApplicationController = Ember.ObjectController.extend({
model: DATA.model_data,
selected: ['Author','']
});
App.SelectFilterBoxComponent = Ember.Component.extend({
template: Ember.Handlebars.compile(''), // Blank template
data: null,
lastCount: 0,
selected: [],
selectedChanged: function(){
// Properties required to build view
var p = this.getProperties("elementId", "data", "lastCount", "selected");
// Used to gain context of controller in on selected changed event
var controller = this;
// Check there is at least one property. I.e. the base model.
var length = p.selected.length;
if(length > 1){
var currentModelName = p.selected[0];
var type = {};
// This function will return an existing select box or create new
var getOrCreate = function(idx){
// Determine the id of the select box
var id = p.elementId + "_" + idx;
// Try get the select box if it exists
var select = $("#" + id);
if(select.length === 0){
// Create select box
select = $("<select id='" + id +"'></select>");
// Action to take if select is changed. State is made available through evt.data
select.on("change", { controller: controller, index: idx }, function(evt){
// Restore the state
var controller = evt.data.controller;
var index = evt.data.index;
var selected = controller.get("selected");
// The selected field
var fieldName = $(this).val();
// Update the selected
selected = selected.slice(0, index);
selected.push(fieldName);
controller.set("selected", selected);
});
// Add it to the component container
$("#" + p.elementId).append(select);
}
return select;
};
// Add the options to the select box
var populate = function(select){
// Only populate the select box if it doesn't have the correct model
if(select.data("type")==currentModelName)
return;
// Clear any existing options
select.html("");
// Get the field from the model
var fields = p.data[currentModelName].fields;
// Add default empty option
select.append($("<option value=''>------</option>"));
// Add the fields to the select box
for(var f = 0; f < fields.length; f++)
select.append($("<option>" + fields[f] + "</option>"));
// Set the model type on the select
select.data("type", currentModelName);
};
var setModelNameFromFieldName = function(fieldName){
// Get the field type from current model meta
type = p.data[currentModelName].meta[fieldName];
// Set the current model
currentModelName = (type !== undefined && type.model !== undefined) ? type.model : null;
};
// Remove any unneeded select boxes. I.e. where the number of selects exceed the selected length
if(p.lastCount > length)
for(var i=length; i < p.lastCount; i++)
$("#" + p.elementId + "_" + i).remove();
this.set("lastCount", length);
// Loop through all of the selected, to build view
for(var s = 1; s < length; s++)
{
// Get or Create select box at index s
var select = getOrCreate(s);
// Populate the model fields to the selectbox, if required
populate(select);
// Current selected
var field = p.selected[s];
// Ensure correct value is selected
select.val(field);
// Set the model for next iteration
setModelNameFromFieldName(field);
if(s === length - 1 && type !== undefined && type.model !== undefined)
{
p.selected.push('');
this.notifyPropertyChange("selected");
}
}
}
}.observes("selected"),
didInsertElement: function(){
this.selectedChanged();
}
});
How it works
The component takes the two parameters model and selected then binds an observer onto the selected property. Any time the selection is changed either through user interaction with the select boxes, or by the property bound to selected the view will be redetermined.
The code uses the following approach:
Determine if the selection array (selected) is greater than 1. (Because the first value needs to be the base model).
Loop round all the selected fields i, starting at index 1.
Determine if select box i exists. If not create a select box.
Determine if select box i has the right model fields based on the current populated model. If yes, do nothing, if not populate the fields.
Set the current value of the select box.
If we are the last select box and the field selected links to a model, then push a blank value onto the selection, to trigger next drop down.
When a select box is created, an onchange handler is hooked up to update the selected value by slicing the selected array right of the current index and adding its own value. This will cause the view to change as required.
A property count keeps track of the previous selected's length, so if a change is made to a selection that decreases the current selected values length, then the unneeded select boxes can be removed.
The source code is commented, and I hope it is clear, if you have any questions of queries with how it works, feel free to ask, and I will try to explain it better.
Your Model:
Having looked at your model, have you considered simplifying it to below? I appreciate that you may not be able to, for other reasons beyond the scope of the question. Just a thought.
DATA.model_data = {
"Book": {
"id": {},
"title": {},
"publication_year": {},
"authors": { "model": "Author" }
},
"Author": {
"id": {},
"first_name": {},
"last_name": {},
"books": { "model": "Book" }
}
};
So field names would be read off the object keys, and the value would be the meta data.
I hope you find this useful. Let me know if you have any questions, or issues.
The Controller:
You can use any controller you want with this component. In my demo of the component I used Ember's built in ApplicationController for simplicity.
Explaination of notifyPropertyChange():
This is called because when we are inserting an new string into the selected array, using the push functionality of arrays.
I have used the push method because this is the most efficient way to add a new entry into an existing array.
While Ember does have a pushObject method that is supposed to take care of the notification as well, I couldn't get it to honour this. So this.notifyPropertyChange("selected"); tells Ember that we updated the array. However I'm hoping that's not a dealbreaker.
Alternative to Ember Component - Implemented as a View
If you don't wish to use it in Component format, you could implement it as a view. It ultimately achieves the same goal, but this may be a more familiar design pattern to you.
See this JSBin for implementation as a View. I won't include the full code here, because some of it is the same as above, you can see it in the JSBin
Usage:
Create an instance of App.SelectFilterBoxView, with a controller that has a data and selected property:
var myView = App.SelectFilterBoxView.create({
controller: Ember.Object.create({
data: DATA.model_data,
selected: ['Author','']
})
});
Then append the view as required, such as to #main.
myView.appendTo("#main");
Unfortunately your code doesn't run, even after adding Ember as a library in your JSFiddle, but ContainerView is probably what you're looking for: http://emberjs.com/api/classes/Ember.ContainerView.html as those views can be dynamically added/removed.
this.$().remove() or this.$().append() are probably what you're looking for:
Ember docs.

ComputedProperty doesn't get updated

I have this test application which should print "filtered" and "changed" each time the applications view is clicked, because a computed property is called. However the property binding is only triggered when updating the property with an empty array:
window.App = Ember.Application.create({
Settings: Ember.Object.create({
filter: []
}),
ApplicationView: Ember.View.extend({
click: function(event) {
filter = App.get('Settings.filter');
console.dir(filter);
if (App.get('Room.filtered')) {
filter = filter.filter(function(item) {
return item != App.get('Room.name');
});
} else {
filter.push(App.get('Room.name'));
}
App.set('Settings.filter', filter);
}
})
});
Room = Ember.Object.extend({
name: "test",
_filterer: function() {
console.dir("changed");
}.observes('filtered'),
filtered: function() {
console.dir("filtered");
filter = App.get('Settings.filter');
for (var i = 0; i < filter.length; i++) {
if (filter[i] == this.get('name')) return true;
}
return false;
}.property('App.Settings.filter', 'name').volatile()
});
App.Room = Room.create();
setTimeout(function() {App.set('Settings.filter', ['test']); }, 3000);
Here is the jsbin: http://jsbin.com/edolol/2/edit
Why is the property binding only triggered when setting the Setting to an empty array? And why does it trigger when doing it manually in the timeout?
Update
Here is a working jsbin: http://jsbin.com/edolol/11/edit
When you're going to add/remove items from an array, and not change the entire array, you need to inform the computed property to observe the items inside the array, not only the array itself:
The syntax is:
function() {
}.property('App.settings.filter.#each')
The reason it was working with setTimeout is because you were replacing the entire array instead of the items inside it.
I fixed your jsbin: http://jsbin.com/edolol/10/edit
I fixed some minor other stuff such as filter.push is now filter.pushObject (Using Ember's MutableArray).
And after changing the filter array (filter = filter.filter()) you need to set the new filter variable as the property: App.set('Settings.filter', filter);
The Problem is that I have used .push() to add to App.Settings.filter and .filter() to remove from it. The first approach does not create a new array, the latter does. Thats why removing from that array has worked, but not adding.
I assume that using Ember.ArrayProxy and an observer for .#each would have worked. But thats out of my knowledge. This little problem is solved by just creating a new array though.
filter = App.get('Settings.filter').slice(0);

Bootstrap typeahead results into a div, possible?

I'm trying to fit typeahead results into a particular div on my page. I get the JSON callback data but I don't know how to use it in order to populate a particular div. The process function has the only effect of listing the results, whatever the length it takes, just under the search field.
Here is my code, do you know how to exploit the callback data in order to populate a particular div ?
$('#search').typeahead({
source: function(query, process) {
$.ajax({
url: '/action/search.php?action=autocomplete',
type: 'POST',
data: 'query=' + query,
dataType: 'JSON',
async: true,
success: function(data) {
//process(data);
},
minLength: 1
});
}
});
There is actually a really simple way to get the results into a specific page element, however, I'm not sure it's actually documented.
Searching through the source code shows that the option menu can be passed in, which seems to be intended to allow you to define what the wrapping menu element will look like, however, you can pass in a selector, and it will use this as the target.
Given the html fragment:
<input id='#typeahead' type='text'>
<h2>Results</h2>
<ul id="typeahead-target"></ul>
You could use the following to get the results to appear within the ul element:
$('#typeahead').typeahead({menu: '#typeahead-target'});
I had exact issue. I have written detailed article about the same.
go through the article : http://www.wrapcode.com/bootstrap/typeahead-json-objects/
When you click on particular result from search query results. You can use updater function to populate data with selected JSON object values..
$("#typeahead").typeahead({
updater: function(item){
//logic on selected item here.. e.g. append it to div in html
return item;
}
});
Use this function :
$("#typeahead").typeahead({
source: function (query, process) {
var jsonObj = //PARSED JSON DATA
states = [];
map = {};
$.each(jsonObj, function (i, state) {
map[state.KeyName] = state;
states.push(state.KeyName); //from JSON Key value pair e.g. Name: "Rahul", 'Name' is key in this case
});
process(states);
},
updater:function (item) {
$('#divID').html(" " + map[item].KeyName); //from JSON Key value pair
return item;
// set more fields
}
});
first create css class named .hide {display:none;}
$(typeahead class or id name).typeahead(
{
hint: false,
highlight: true,
minLength: 1,
classNames: {
menu: 'hide' // add class name to menu so default dropdown does not show
}
},{
name: 'names',
display: 'name',
source: names,
templates: {
suggestion: function (hints) {
return hints.name;
}
}
}
);
$(typeahead class or id name).on('typeahead:render', function (e, datum)
{
//empty suggestion div that you going to display all your result
$(suggestion div id or class name').empty();
var suggestions = Array.prototype.slice.call(arguments, 1);
if(suggestions.length){
for(var i = 0; i < suggestions.length; i++){
$('#result').append(liveSearch.template(
suggestions[i].name,
suggestions[i].id));
}
}else{
$('#result').append('<div><h1>Nothing found</h1></div>');
}
});