Ember editable recursive nested components - ember.js

I'm currently trying to build a component that will accept a model like this
"values": {
"value1": 234,
"valueOptions": {
"subOption1": 123,
"subOption2": 133,
"subOption3": 7432,
"valueOptions2": {
"subSubOption4": 821
}
}
}
with each object recursively creating a new component. So far I've created this branch and node components and its fine at receiving the data and displaying it but the problem I'm having is how I can edit and save the data. Each component has a different data set as it is passed down its own child object.
Js twiddle here : https://ember-twiddle.com/b7f8fa6b4c4336d40982
tree-branch component template:
{{#each children as |child|}}
{{child.name}}
{{tree-node node=child.value}}
{{/each}}
{{#each items as |item|}}
<li>{{input value=item.key}} : {{input value=item.value}} <button {{action 'save' item}}>Save</button></li>
{{/each}}
tree-branch component controller:
export default Ember.Component.extend({
tagName: 'li',
classNames: ['branch'],
items: function() {
var node = this.get('node')
var keys = Object.keys(node);
return keys.filter(function(key) {
return node[key].constructor !== Object
}).map(function(key){
return { key: key, value: node[key]};
})
}.property('node'),
children : function() {
var node = this.get('node');
var children = [];
var keys = Object.keys(node);
var branchObjectKeys = keys.filter(function(key) {
return node[key].constructor === Object
})
branchObjectKeys.forEach(function(keys) {
children.push(keys)
})
children = children.map(function(key) {
return {name:key, value: node[key]}
})
return children
}.property('node'),
actions: {
save: function(item) {
console.log(item.key, item.value);
}
}
});
tree-node component:
{{tree-branch node=node}}
Anyone who has any ideas of how I can get this working would be a major help, thanks!

Use:
save(item) {
let node = this.get('node');
if (!node || !node.hasOwnProperty(item.key)) {
return;
}
Ember.set(node, item.key, item.value);
}
See working demo.

I think this would be the perfect place to use the action helper:
In your controller define the action:
//controller
actions: {
save: function() {
this.get('tree').save();
}
}
and then pass it into your component:
{{tree-branch node=tree save=(action 'save')}}
You then pass this same action down into {{tree-branch}} and {{tree-node}} and trigger it like this:
this.attrs.save();
You can read more about actions in 2.0 here and here.

Related

Delete item from ember-tables

I'm trying add a delete button with an ember action from a controller. For some reason Ember.Handlebars.compile('<button {{action "deletePerson"}}>Delete</button> returns a function and not the compiled string.
Here's a jsbin
Here's the relevant portion of code:
App.ApplicationController = Ember.Controller.extend({
columns: function() {
...
buttonColumn = Ember.Table.ColumnDefinition.create({
columnWidth: 100,
headerCellName: 'Action',
getCellContent: function(row) {
var button = Ember.Handlebars.compile('<button {{action "deletePerson" this}}>Delete</button>');
return button; // returns 'function (context, options) { ...'
}
});
...
}.property()
...
After looking through the link from #fanta (http://addepar.github.io/#/ember-table/editable) and a lot of trial and error, I got it working.
Here's the working jsbin.
Here are some key points:
Instead of using getCellContent or contentPath in the ColumnDefinition, you need to use tableCellViewClass and to create a view that will handle your cell
Pass in this to the action on your button — and modify content off that. One gotcha is to edit content, you need to copy it using Ember.copy
Here's the relevant code:
App.ApplicationController = Ember.Controller.extend({
columns: function() {
...
buttonColumn = Ember.Table.ColumnDefinition.create({
columnWidth: 100,
headerCellName: 'Action',
tableCellViewClass: 'App.PersonActionCell'
});
...
}.property(),
onContentDidChange: function(){
alert('content changed!');
}.observes('content.#each'),
...
});
App.PersonActionCell = Ember.Table.TableCell.extend({
template: Ember.Handlebars.compile('<button {{action "deletePerson" this target="view"}}>Delete</button>'),
actions: {
deletePerson: function(controller){
// Will NOT work without Ember.copy
var people = Ember.copy(controller.get('content'));
var row = this.get('row');
// For some reason people.indexOf(row) always returned -1
var idx = row.get('target').indexOf(row);
people.splice(idx, 1);
controller.set('content', people);
}
}
});

Ember.js count position of object in Ember Data Model for simple pagination

I am tryin to count the position of an object. That means which numeric position an item has. That cannot be the primary key because they are not persistent; a record can get deleted.
Asume we have this "model" (a simple array for now, I use Ember Data):
posts = [
{id: 4, number: 104, title: 'Post 4'},
{id: 2, number: 102, title: 'Post 2'},
{id: 3, number: 103, title: 'Post 3'},
];
So at to allow sorting in the postsController we do:
this.controllerFor('posts').set('sortProperties', ['id']); // or maybe just sorting on 'number'
this.controllerFor('posts').set('sortAscending', true);
In the template I want to show the current post and the total number of posts {{currentPostCount}} of {{totalPostCount}}
In postController I have the following computed properties:
App.PostController = Ember.ObjectController.extend({
posts: function () {
return this.store.all('posts');
}.property(),
currentCount: function () {
// if there is an id get the position of the record
// that will be the position count of the post record
var id = this.get('id');
if (id) {
console.log('this item has an id: ' + id);
var count = 0;
var currentCount;
// loop over posts to check at which position the current post is
this.get('posts').filter(function (item) {
count++;
if (item.id == id) {
console.log('yay id found! count is: ' + count)
currentCount = count;
}
});
return currentCount;
}
}.property('posts.#each'),
totalCount: function () {
var posts= this.get('posts');
return posts.get('length');
}.property('posts.#each')
});
Edit: add model:
App.ApplicationRoute = Ember.Route.extend({
model: function() {
this.controllerFor('posts').set('model', this.store.find('post'));
// sort posts by id
this.controllerFor('posts').set('sortProperties', ['id']);
//(...)
}
});
App.PostsController = Ember.ArrayController.extend();
.
Update: full working simple pagination in Ember.js
In your template:
<div id="pagination">
<span>Post {{index}}/{{totalCount}}</span>
<a {{action previousPost this}} href="#">Previous</a>
<a {{action nextPost this}} href="#">Next</a>
</div>
The PostsController:
App.PostsController = Ember.ArrayController.extend({
sortProperties: ['id'], // I sort on ID, but you can sort on any property you want.
sortAscending: true,
assignIndex: function () {
this.map(function (item, index) {
Ember.set(item, 'index', index + 1)
})
}.observes('content.[]', 'firstObject', 'lastObject')
});
Actions in the PostController:
previousPost: function (post) {
var newIndex = (post.index - 1);
var previousPost = this.store.all('post').findBy('index', newIndex);
if (previousPost) {
this.transitionToRoute('post', previousPost);
}
},
nextPost: function (post) {
var newIndex = (post.index + 1);
var nextPost = this.store.all('post').findBy('index', newIndex);
if (nextPost) {
this.transitionToRoute('post', nextPost);
}
}
I have two computed properties in the PostController. However you can better use the following in your PostsController to do it the Ember way, like kingpin2k said. Then you can also omit the posts property.
posts: function () {
return this.store.all('posts');
}.property(),
// totalcount below the page for pagination
totalCount: function () {
var posts= this.get('posts');
return posts.get('length');
}.property('posts.#each'),
You can maintain an index on array content during sorting or removing objects.
In PostsController:
App.PostsController=Em.ArrayController.extend({
assignIndex:function(){
this.map(function(item,index){Em.set(item,'index',index+1)})
}.observes('content.[]','firstObject','lastObject'),
//other contents to follow...
In posts template the property index is available which is dynamically updated when adding or removing or sorting objects.
In Posts Template:
{{#each controller}}
<p>{{index}}. {{name}}</p>
{{/each}}

Collection of objects of multiple models as the iterable content in a template in Ember.js

I am trying to build a blog application with Ember. I have models for different types of post - article, bookmark, photo. I want to display a stream of the content created by the user for which I would need a collection of objects of all these models arranged in descending order of common attribute that they all have 'publishtime'. How to do this?
I tried something like
App.StreamRoute = Ember.Route.extend({
model: function() {
stream = App.Post.find();
stream.addObjects(App.Bookmark.find());
stream.addObjects(App.Photo.find());
return stream;
}
}
where the resource name is stream
But it doesn't work. I am using the latest released Ember 1.0.0 rc 2 and handlebars 1.0.0 rc 3 with jQuery 1.9.1 and ember-data.
Probably the way I am trying to achieve this whole thing is wrong. The problem is even if I am able to use the collection of objects of multiple models to iterate in the template, I would still need to distinguish between the type of each object to display its properties apart from the common property of 'publishtime'.
You can use a computed property to combine the various arrays and then use Javascript's built in sorting to sort the combined result.
Combining the arrays and sorting them
computed property to combine the multiple arrays:
stream: function() {
var post = this.get('post'),
bookmark = this.get('bookmark'),
photo = this.get('photo');
var stream = [];
stream.pushObjects(post);
stream.pushObjects(bookmark);
stream.pushObjects(photo);
return stream;
}.property('post.#each', 'bookmark.#each', 'photo.#each'),
example of sorting the resulting computed property containing all items:
//https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/sort
streamSorted: function() {
var streamCopy = this.get('stream').slice(); // copy so the original doesn't change when sorting
return streamCopy.sort(function(a,b){
return a.get('publishtime') - b.get('publishtime');
});
}.property('stream.#each.publishtime')
});
rendering items based on a property or their type
I know of two ways to do this:
add a boolean property to each object and use a handlebars {{#if}} to check that property and render the correct view
extend Ember.View and use a computed property to switch which template is rendered based on which type of object is being rendered (based on Select view template by model type/object value using Ember.js)
Method 1
JS:
App.Post = Ember.Object.extend({
isPost: true
});
App.Bookmark = Ember.Object.extend({
isBookmark: true
});
App.Photo = Ember.Object.extend({
isPhoto: true
});
template:
<ul>
{{#each item in controller.stream}}
{{#if item.isPost}}
<li>post: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{#if item.isBookmark}}
<li>bookmark: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{#if item.isPhoto}}
<li>photo: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{/each}}
</ul>
Method 2
JS:
App.StreamItemView = Ember.View.extend({
tagName: "li",
templateName: function() {
var content = this.get('content');
if (content instanceof App.Post) {
return "StreamItemPost";
} else if (content instanceof App.Bookmark) {
return "StreamItemBookmark";
} else if (content instanceof App.Photo) {
return "StreamItemPhoto";
}
}.property(),
_templateChanged: function() {
this.rerender();
}.observes('templateName')
})
template:
<ul>
{{#each item in controller.streamSorted}}
{{view App.StreamItemView contentBinding=item}}
{{/each}}
</ul>
JSBin example - the unsorted list is rendered with method 1, and the sorted list is rendered with method 2
It's a little complicated than that, but #twinturbo's example shows nicely how to aggregate separate models into a single array.
Code showing the aggregate array proxy:
App.AggregateArrayProxy = Ember.ArrayProxy.extend({
init: function() {
this.set('content', Ember.A());
this.set('map', Ember.Map.create());
},
destroy: function() {
this.get('map').forEach(function(array, proxy) {
proxy.destroy();
});
this.super.apply(this, arguments);
},
add: function(array) {
var aggregate = this;
var proxy = Ember.ArrayProxy.create({
content: array,
contentArrayDidChange: function(array, idx, removedCount, addedCount) {
var addedObjects = array.slice(idx, idx + addedCount);
addedObjects.forEach(function(item) {
aggregate.pushObject(item);
});
},
contentArrayWillChange: function(array, idx, removedCount, addedCount) {
var removedObjects = array.slice(idx, idx + removedCount);
removedObjects.forEach(function(item) {
aggregate.removeObject(item);
});
}
});
this.get('map').set(array, proxy);
},
remove: function(array) {
var aggregate = this;
array.forEach(function(item) {
aggregate.removeObject(item);
});
this.get('map').remove(array);
}
});

ember.js Ember.Select multiple=true with preselected values

I'm using a Multiselect view:
{{view Ember.Select
multiple="true"
contentBinding="App.filtersProductController"
selectionBinding="App.filtersController.products"
optionLabelPath="content.fullName"
optionValuePath="content.id"
isVisibleBinding="App.filtersController.productListBox"}}
Is it possible to preselect multiple values in the "select" box and to change the selected values programmatically? Background: I want to save different combinations of three "select" boxes settings as bookmarks. When loading a bookmark, I have to set the "select" boxes values.
Thank you
Yes. In your controller you have to create a property to keep the selected value or values when working with Ember.Select.
In the code below I'm setting the Greetings as the content of the select box, in the controller that lists those Greetings (check ApplicationRoute), I also have a property called selectedItems which I'm binding to the Select and I'm using a couple other properties to filter the values I want to pre-select (1 and 3) in case none of the items are already selected when the view loads.
This will render a multiple select box with the items which the id are either 1 or 3 marked as selected. You can see the source here: http://jsfiddle.net/schawaska/Y8P4m/
Handlebars:
<script type="text/x-handlebars">
<h1>Test</h1>
{{view Ember.Select
multiple="true"
selectionBinding="controller.selectedItems"
contentBinding="controller"
optionLabelPath="content.text"
optionValuePath="content.id"}}
</script>
JavaScript:
window.App = Ember.Application.create();
App.Store = DS.Store.extend({
revision: 11,
adapter: 'DS.FixtureAdapter'
});
App.Greeting = DS.Model.extend({
text: DS.attr('string'),
when: DS.attr('date'),
selected: false,
isSelected: function() {
return this.get('selected');
}.property('selected')
});
App.ApplicationController = Em.ArrayController.extend({
preselected: function() {
return this.get('content').filter(function(greeting) {
return greeting.get('id') == 1 ||
greeting.get('id') == 3;
});
}.property('content.#each'),
selectedItems: function() {
if(this.get('selected.length') <= 0) {
return this.get('preselected');
} else {
return this.get('selected');
}
}.property('selected', 'preselected'),
selected: function() {
return this.get('content').filter(function(greeting) {
return greeting.get('isSelected');
})
}.property('content.#each')
});
App.Greeting.FIXTURES = [
{id: 1, text: 'First', when: '3/4/2013 2:44:52 PM'},
{id: 2, text: 'Second', when: '3/4/2013 2:44:52 PM'},
{id: 3, text: 'Third', when: '3/4/2013 2:44:52 PM'},
{id: 4, text: 'Fourth', when: '3/4/2013 3:44:52 PM'}
];
App.ApplicationRoute = Em.Route.extend({
setupController: function(controller) {
controller.set('model', App.Greeting.find());
}
});
I have created a complete example with single and multi "select" elements. You can set defaults and change the selected value programmatically or by using the "select" GUI element. The controller code:
// class for single selects
App.SingleSelectFilterController = Ember.ArrayController.extend({
selection: null,
active: true,
update: function(id) {
this.set("selection", id);
},
getSelectedId: function() {
return this.get("selection");
}
});
// class for multi selects
App.MultiSelectFilterController = Ember.ArrayController.extend({
selection: null,
active: true,
update: function(selectionIds) {
// Workaround: Reinitializing "content". How to do it well?
var contentCopy = [];
for(i = 0; i < this.get("content").length; i++) {
contentCopy.push(this.get("content")[i]);
}
this.set("content", contentCopy);
this.set("selection", selectionIds);
},
selected: function() {
var me = this;
return this.get('content').filter(function(item) {
for(i = 0; i < me.get("selection").length; i++) {
if(me.get("selection")[i] === item.get('id')) { return true; }
}
return false;
});
}.property('content.#each'),
getSelectedIds: function() {
var ids = [];
for(i = 0; i < this.get("selected").length; i++) {
ids.push(this.get("selected")[i].get("id"));
}
return ids;
}
});
// create single and multi select controllers
App.metricController = App.SingleSelectFilterController.create();
App.metricController.set("content", App.filterData.get("metrics"));
App.metricController.set("selection", "views"); // set default value for single select element
App.platformController = App.MultiSelectFilterController.create();
App.platformController.set("content", App.filterData.get("platforms"));
App.platformController.set("selection", ["plat-black"]); // set default value for multi select element
And the complete example:
http://jsfiddle.net/7R7tb/2/
Thanks to MilkyWayJoe for his help!
Perhaps somebody knows how to fix the workaround (see the code comment above)?

Ember.js bind class change on click

How do i change an elements class on click via ember.js, AKA:
<div class="row" {{bindAttr class="isEnabled:enabled:disabled"}}>
View:
SearchDropdown.SearchResultV = Ember.View.extend(Ember.Metamorph, {
isEnabled: false,
click: function(){
window.alert(true);
this.isEnabled = true;
}
});
The click event works as window alert happens, I just cant get the binding to.
The class is bound correctly, but the isEnabled property should be modified only with a .set call such as this.set('isEnabled', true) and accessed only with this.get('isEnabled'). This is an Ember convention in support of first-class bindings and computed properties.
In your view you will bind to a className. I have the following view in my app:
EurekaJ.TabItemView = Ember.View.extend(Ember.TargetActionSupport, {
content: null,
tagName: 'li',
classNameBindings: "isSelected",
isSelected: function() {
return this.get('controller').get('selectedTab').get('tabId') == this.get('tab').get('tabId');
}.property('controller.selectedTab'),
click: function() {
this.get('controller').set('selectedTab', this.get('tab'));
if (this.get('tab').get('tabState')) {
EurekaJ.router.transitionTo(this.get('tab').get('tabState'));
}
},
template: Ember.Handlebars.compile('<div class="featureTabTop"></div>{{tab.tabName}}')
});
Here, you have bound your className to whatever the "isSelected" property returns. This is only true if the views' controller's selected tab ID is the same as this views' tab ID.
The code will append a CSS class name of "is-selected" when the view is selected.
If you want to see the code in context, the code is on GitHub: https://github.com/joachimhs/EurekaJ/blob/netty-ember/EurekaJ.View/src/main/webapp/js/app/views.js#L100
Good answers, however I went down a different route:
SearchDropdown.SearchResultV = Ember.View.extend(Ember.Metamorph, {
classNameBindings: ['isSelected'],
click: function(){
var content = this.get('content');
SearchDropdown.SelectedSearchController.set('content', content);
var loadcontent = this.get('content');
loadcontent.set("searchRadius", $("select[name=radius]").val());
SearchDropdown.LoadMap.load(content);
},
isSelected: function () {
var selectedItem = SearchDropdown.SelectedSearchController.get('content'),
content = this.get('content');
if (content === selectedItem) {
return true;
}
}.property('SearchDropdown.SelectedSearchController.content')
});
Controller:
SearchDropdown.SelectedSearchController = Ember.Object.create({
content: null,
});
Basically stores the data of the selected view in a controller,