Ember: computed property (object) not updating in view - ember.js

I'm new to ember and am creating a search filtering app. I have my search filter "buckets" set up as controller properties and they are bound nicely to query parameters.
I'm looking to create a "your selected filters" component that summarizes what filters the user has currently active. I'm thinking maybe a computed property is the way to do this? In my controller I created one called selectedFilters:
export default Ember.Controller.extend(utils, {
queryParams: ['filter_breadcrumb','filter_price','filter_size_apparel','filter_color'],
filter_breadcrumb: [],
filter_price: [],
filter_size_apparel: [],
filter_color: [],
selectedFilters: Ember.computed('this{filter_breadcrumb,filter_price,filter_size_apparel,filter_color}', function() {
let filterContainer = {};
for (let bucket of this.queryParams) {
let bucketArray = this.get(bucket);
if (bucketArray.length > 0) { // only add if bucket has values
filterContainer[bucket] = {
'title' : cfg.filterTitles[bucket], // a "pretty name" hash
'values' : bucketArray
};
}
}
return filterContainer;
})
});
The contents of selectedFilters would look something like this when a user has chosen filters:
{
filter_breadcrumb: { title: 'Category', values: [ 'Home > Stuff', 'Garage > More Stuff' ] },
filter_price: { title: 'Price', values: [ '*-20.0' ] },
filter_color: { title: 'Color', values: [ 'Black', 'Green' ] }
}
And then the template would be:
<h1>Selected Filters</h1>
{{#each-in selectedFilters as |selectedFilter selectedValues|}}
{{#each selectedValues.values as |selectedValue|}}
<strong>{{selectedValues.title}}</strong>: {{selectedValue}} <br>
{{/each}}
{{/each-in}}
This actually works (kind of). The view is not updating when filters are added and removed. When I hard-refresh the page, they do show up. I'm wondering why they aren't updating even though the "input" properties to selectedFilters do?
I'm thinking either I'm doing it wrong or perhaps there's a better way to do this. Any help appreciated!

You can't use this for computed property dependent key because it's undefined in that scope.
Arrays and objects defined directly on any Ember.Object are shared across all instances of that object. so initialize it in init(). refer initializing instances ember guide
init(){
this._super(...arguments);
this.set('filter_breadcrumb',[]);
}
For definining computed properties using arrays as dependant key refer ember guide
In your case if you want your computed property to recalculate based array item added/removed or changed to different array then use .[]
export default Ember.Controller.extend(utils, {
queryParams: ['filter_breadcrumb', 'filter_price', 'filter_size_apparel', 'filter_color'],
init(){
this._super(...arguments);
this.set("filter_breadcrumb",[]);
this.set("filter_price",[]);
this.set("filter_size_apparel",[]);
this.set("filter_color",[]);
},
selectedFilters: Ember.computed('filter_breadcrumb.[]','filter_price.[]','filter_size_apparel.[]','filter_color.[]', function() {
let filterContainer = {};
for (let bucket of this.queryParams) {
let bucketArray = this.get(bucket);
if (bucketArray.length > 0) { // only add if bucket has values
filterContainer[bucket] = {
'title': cfg.filterTitles[bucket], // a "pretty name" hash
'values': bucketArray
};
}
}
return filterContainer;
})
});
In case if you want computed property to recalculate based on each individual item change then consider filter_price.#each.price

Figured it out. It appears the brace expansion doesn't work on this. I tried:
selectedFilters: Ember.computed('this{filter_breadcrumb,filter_price,filter_size_apparel,filter_color}', function() {
and
selectedFilters: Ember.computed('this.{filter_breadcrumb,filter_price,filter_size_apparel,filter_color}', function() {
This works tho:
selectedFilters: Ember.computed('filter_breadcrumb', 'filter_price', 'filter_size_apparel', 'filter_color', function() {
But I'm still wondering if this is the recommended way of accomplishing my "filter summary" task.

Related

EmberJS: Computed property awareness of dependent keys

Let's suppose I have an object that has this structure:
{ filters: [{ params: { value: ["abc"] }] }.
How can I write a computed property that is aware of changes to the value property? For example, let's say we take one of the filters and do set(filter, 'params.value', ["abc", "123"]). I've been trying with computed('filters.#each.params.value.[]', ...) but it isn't working
Alright, I guess I know how to help you. First of all it seems to be impossible to have a computed property with deeply nested keys after #each. That's why in order to make this work one has to split it. Something like this:
export default Component.extend({
init() {
this._super(...arguments);
this.o = {filters: [{params: {value: ["abc"]}}]};
},
params: computed('o.filters.#each.params', function () {
return this.o.filters.mapBy('params');
}),
paramsValue: computed('params.#each.value', function () {
let aaa = this.params; // it seems like we need to reference the params anyhow
return JSON.stringify(this.o);
}),
#action
changeClassic() {
let filter = this.o.filters[0];
set(filter, 'params.value', ['abc', '123']);
}
});
I also tried to solve the same problem in case of a glimmer component with #tracked. There I had to use a separate class and make the value field tracked:
class ValueHandler {
#tracked value;
constructor(value) {
this.value = value;
}
}
export default class DeepObjectTestComponent extends Component {
#tracked o = {filters: [{params: new ValueHandler('abc')}]};
// so here I don't need an intermediate computed (hope this works for a more complicated scenario
get paramsValue() {
let aaa = this.o.filters[0].params.value;
return JSON.stringify(this.o);
}
#action
changeGlimmer() {
let filter = this.o.filters[0];
// set(filter, 'params.value', ['abc', '123']);
filter.params.value = ['abc', '123'];
}
}

emberjs component property not updated?

i have two components in my template:
{{ property-pie-chart
models=model.hosts
defaultProp=""
filterByDate=filterByDate
chartData=[]
}}
{{ paged-filtered-list
data=model.hosts
dates=model.dates
page=page
pageSize=pageSize
filterByDate=filterByDate
pagerView=pagerView
initRouteAction=( action 'dateInit' )
dateFilterAction=( action 'filterByDate' )
termFilterAction=(action 'filterByTerm')
sortOrder=sortOrder
sortField=sortField
}}
I send action from paged-filtered-list component to controller, which triggers route transition with filterByDate as parameter:
import Ember from 'ember';
export default Ember.Controller.extend({
queryParams: [
'page',
'pageSize',
'sortField',
'sortOrder',
'filterByDate',
'filterByTerm'
],
filterByDate: "",
filterByTerm: "",
page: 1,
pageSize: 10,
pagerView: 4,
sortField: "",
sortOrder: 'asc',
lala: "",
actions: {
dateInit: function(sortedDates) {
if (!this.get('filterByDate')) {
let params = {
filterByDate: sortedDates.get('firstObject').get('key'),
page: this.get('page'),
pageSize: this.get('pageSize'),
pagerView: this.get('pagerView')
};
this.transitionToRoute('hosts', { queryParams: params});
}
},
filterByDate: function(value) {
if (value) {
let params = {
filterByDate: value,
page: 1,
pageSize: this.get('pageSize')
};
this.transitionToRoute('hosts', { queryParams: params});
}
},
filterByTerm: function(value) {
let params = {
filterByDate: this.get('filterByDate'),
page: 1,
pageSize: this.get('pageSize')
};
if (value) {
params['filterByTerm'] = value;
} else {
params['filterByTerm'] = "";
}
this.transitionToRoute('hosts', { queryParams: params});
}
}
});
Problem is that URL is updated and contains filterByDate, but first component property-pie-chart does not detect that filterByDate property is changed, altough i checked attributes in init/didUpdate methods and parameter is changed, can somebody help and explain what i am doing wrong?
Currently you are not setting filterByDate property in controller.
I would suggest the following approach,
You please declare the below property in corresponding route.js,
queryParams: { page: { refreshModel: true }, pageSize: { refreshModel: true },sortOrder: { refreshModel: true },filterByDate: { refreshModel: true },filterByTerm: { refreshModel: true }}
refreshModel denotes is whenever this property changed,then it will force to refresh the page.
and in controller.js, You don't need to call this.transitionToRoute('hosts', { queryParams: params}); instead you just set required queryParams participating property alone then transition will automatically taken care.
SideNote: It's good if you can change function name filterByTerm filterByDate by the way this is not related to problem
Update:
I am glad you sorted out the problem. but then I want to emphasize what are Computed Properties from ember guides.
In a nutshell, computed properties let you declare functions as properties. You create one by defining a computed property as a function, which Ember will automatically call when you ask for the property. You can then use it the same way you would any normal, static property.
https://guides.emberjs.com/v2.8.0/object-model/computed-properties/#toc_what-are-computed-properties

Ember: create a DS.belongsTo *outside* of a Model

I'm wanting to ideally create a DS.belongsTo / BelongsToRelationship in my own class (which is an Ember.Object, but not a DS.Model), or alternatively recreate the functionality to let me hold a reference to a record in my own class. I don't know if it's possible to use a DS.belongsTo outside of a DS.Model, or if so, how to set it up.
Background:
I have an ember-cli app using ember-data + ember-fire + firebase. One of my models has an attribute which is an object holding "type specific" information for the record. I transform this object into my own class based on the type it describes, and some times that type will have references to other records in the database. In these cases I would like to have a DS.belongsTo property set up in my typeSpecific class that I could link to in the same way as linking to a relationship in a regular model.
Alternative:
After a lot of searching and not finding any information on how to do this I made my own class which got me most of the way there. I've just noticed that although I can change the record it references on the client side and have it update, if I change it on the server-side I don't get updates coming through, so it's back to the drawing board.
If anyone is able to tell me how to make this alternative approach work that would serve the purpose too. The idea with this class is that I pass it a model name and id, and it should create the model reference and then keep model and id in sync if either side changes, and also pass through updates if anything on the model it's connected to gets changed just like a regular relationship would.
export default Ember.Object.extend({
id: null,
table: undefined,
model: undefined,
store: undefined,
init: function() {
this._super();
if(this.id && !this.model) {
this.updateModel();
}
else if(this.model && !this.id) {
this.updateId();
}
},
updateModel: function() {
var self = this;
if( this.get('id') ) {
this.store.find(this.get('table'), this.get('id')).then( function(model) {
self.set('model', model);
});
}
else {
self.set('model', undefined);
}
}.observes('id','table'),
updateId: function() {
if(this.get('model')) {
this.set('id', this.get('model.id'));
}
else {
this.set('id', null);
}
}.observes('model'),
});
Edit: code to manipulate the object above:
//Creating a reference:
this.set('target', ModelPointer.create({store:this.get('store'), table:this.get('targetTable'), id:targetId}));
//or:
this.set('target', ModelPointer.create({store:store, table:'myTable'}));
...
this.set('target.id', '42');
I believe that at the moment if I change either the id or model on the client the other will update automatically, eg:
//either:
this.set('target.id', '43');
//or:
this.store.find('myTable','43').then( function(newModel) {
self.set('target.model', newModel);
});
The problem is that if I log into Firebase and change myTable['42'].name='Fred' then the value showing on my web page which is linked to target.model.name doesn't update to 'Fred'. I suspect that if I set target.model.name to 'Fred' on the client side and save it wouldn't update the value on the server either(?)
The cleanest solution I've come up is to not store the id separately (left to the model itself). I've verified that changes I make in Firebase propagate to the displayed entry just fine.
With this solution setting the referenced model can be done with either its id or simply the model instance itself. See the controller code for examples.
Firstly, for reference, some test data for Firebase:
{
"testModels": {
"1": {
"name": "Model one"
},
"2": {
"name": "The second model"
},
"3": {
"name": "Third is the charm"
}
}
}
Thus its model app/models/test-model.js just needs the name in there.
Here is my belongsTo-like proxy class, I put mine under app/utils/proxy-class.js but it should probably be a Mixin:
import Ember from 'ember';
export default Ember.Object.extend({
remote: null, // reference to the remote DS.Model
store: null, // reference to the actual store
storeModel: null, // name of the model in the store
_watchRemote: function() {
var self = this;
if ( typeof self.get('remote') === 'object' ) {
// do nothing, already an object
if ( ! Ember.isPresent( self.get('store') ) ) {
// but set the store from the model
self.set( 'store', self.get('remote.store') );
}
} else if ( typeof self.get('remote') === 'string' ||
typeof self.get('remote') === 'number' ) {
// it's an id, so fetch the model
self._fetchModel( self.get('remote') );
}
}.observes('remote').on('init'), // on change, and during object init
_fetchModel: function( id ) {
var self = this;
self.store.find( self.get('storeModel'), id ).then(
function( model ) {
self.set( 'remote', model );
}, function ( err ) {
console.error( "couldn't read from the store:", err );
});
},
});
I created this controller, and used the browser console to change the model on the fly to test that model changes are picked up:
import Ember from 'ember';
import proxyClass from '../utils/proxy-class';
export default Ember.Controller.extend({
model: {
remoteFromId: null,
remoteFromModel: null,
},
init: function() {
var self = this;
self.set( 'model.remoteFromId',
proxyClass.create({
remote: 1,
store: self.get('store'),
storeModel: 'test-model',
})
);
self.get('store').find( 'test-model', 2 )
.then( function( model ) {
self.set( 'model.remoteFromModel',
proxyClass.create({
remote: model,
storeModel: 'test-model',
// no store provided here: set from the model
})
);
});
}
});
And the template for the controller:
<p>remoteFromId: {{model.remoteFromId.remote.id}}
{{model.remoteFromId.remote.name}}</p>
<p>remoteFromModel: {{model.remoteFromModel.remote.id}}
{{model.remoteFromModel.remote.name}}</p>

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.