In my app, a user can enter a description of a friend or upvote a description that is already present. Both methods (createDescription and upvoteDescription) persist in the database. upvoteDescription changes the DOM, but createDescription does not. It may be because I'm passing a parameter in the model, but I can't get around that -- the api needs it.
//descriptions route
App.DescriptionsRoute = Ember.Route.extend({
...
model: function () {
var store = this.get('store'),
friend = this.modelFor('friend');
return store.find('description', {friend_id: friend.id});
}
})
//descriptions controller
App.DescriptionsController = Ember.ArrayController.extend({
...
actions: {
createDescription: function () {
var name = this.get('name'),
friend_id = this.get('controllers.friend').get('id'),
store = this.get('store'),
description = store.createRecord('description', {
name: name,
friend_id: friend_id
});
description.save();
},
upvoteDescription: function (description) {
var count = description.get('count');
description.set('count', count + 1);
description.save();
}
}
});
//descriptions template
{{input value=name action="createDescription" type="text"}}
{{#each controller}}
<div {{bind-attr data-name=name data-count=count }} {{action upvoteDescription this}}>
<div>{{name}}</div>
<span>{{count}}</span>
</div>
{{/each}}
find (by query) doesn't actively make sure it has the records that match the query, so you'll have to manually inject it into the results.
createDescription: function () {
var name = this.get('name'),
friend_id = this.get('controllers.friend').get('id'),
store = this.get('store'),
description = store.createRecord('description', {
name: name,
friend_id: friend_id
});
description.save();
this.pushObject(description);
},
or you can use a live record array (filter/all)
model: function () {
var store = this.get('store'),
friend = this.modelFor('friend');
store.find('description', {friend_id: friend.id});
return store.filter('description', function(record){
return record.get('friend_id') == friend.id;
});
}
Related
I have this route in my Ember app:
model: function(params, transition) {
var self = this;
return Ember.RSVP.hash({
photo: self.store.find('photo', params.id),
comments: self.store.query('comment', {id: params.id})
});
},
actions: {
newComment: function(comment) {
var record = this.store.createRecord('comment', comment);
}
}
The template:
{{#each model.comments as |comment|}}
<div class="card">
<div data-userId="{{comment.userId}}">
<b>{{comment.username}}</b>
</div>
<div>
{{comment.content}}
</div>
<span class="hide-on-small-only">{{i18n 'createdAt_lbl'}}: </span>{{format comment.createdAt type='date'}}
</div>
{{/each}}
{{post-comment newComment='newComment' comments=model.comments}}
and the comment model:
export default DS.Model.extend({
commentHash: DS.attr('string'),
content: DS.attr('string'),
createdAt: DS.attr('date'),
username: DS.attr('string'),
userHash: DS.attr('string'),
userId: DS.attr('number'),
});
The post-comment component is the one responsible to call the newComment action:
// post-comment component
var self = this;
// get the new comment content from textarea
var $contentArea = this.$('#postCommentContent');
var content = $contentArea.val();
var newComment = {
userId: localStorage.getItem('userId'),
content: content,
createdAt: moment().format('MMMM Do YYYY, h:mm:ss a')
};
self.sendAction('newComment', newComment);
What I need is to be able to add a new local comment (without persisting it on the server) dinamically and make the template update to show the newly added record without a complete page refresh
Make a modifiable copy of the list of comments and keep it on the controller:
setupController(controller, model) {
this._super(...arguments);
controller.set('comments', model.comments.toArray());
}
The reason you need to make a copy is that the return value from store.query is, for various reasons, unwritable. There may be other ways to make a copy but toArray seems to work well.
Add the new comment to this list after creating it:
actions: {
newComment: function(comment) {
var record = this.store.createRecord('comment', comment);
this.get('comments').pushObject(record);
}
}
In your template, loop over the controller property:
{#each comments as |comment|}}
It should simply work like this:
newComment: function(comment) {
var record = this.store.createRecord('comment', comment);
var commentsModel = this.get('model.comments'); // or this.get('comments'), depending where you action resides
commentsModel.pushObject(record);
this.set('model.comments', commentsModel); // or this.set('comments'), depending where you action resides
}
This only works if you actually have comments. If not, you first need to initialize your comments as an empty array. Otherwise:
newComment: function(comment) {
var record = this.store.createRecord('comment', comment);
var commentsModel = this.get('model.comments'); // or this.get('comments'), depending where you action resides
if(!commentsModel){
commentsModel = Ember.A();
}
commentsModel.pushObject(record);
this.set('model.comments', commentsModel); // or this.set('comments'), depending where you action resides
}
}
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.
In my app, a user can enter a description of a friend or upvote a description that is already present. Both methods (createDescription and upvoteDescription) persist in the database. upvoteDescription changes the DOM, but createDescription does not. It may be because I'm passing a parameter in the model, but I can't get around that -- the api needs it.
//descriptions route
App.DescriptionsRoute = Ember.Route.extend({
...
model: function () {
var store = this.get('store'),
friend = this.modelFor('friend');
return store.find('description', {friend_id: friend.id});
}
})
//descriptions controller
App.DescriptionsController = Ember.ArrayController.extend({
...
actions: {
createDescription: function () {
var name = this.get('name'),
friend_id = this.get('controllers.friend').get('id'),
store = this.get('store'),
description = store.createRecord('description', {
name: name,
friend_id: friend_id
});
description.save();
},
upvoteDescription: function (description) {
var count = description.get('count');
description.set('count', count + 1);
description.save();
}
}
});
//descriptions template
{{input value=name action="createDescription" type="text"}}
{{#each controller}}
<div {{bind-attr data-name=name data-count=count }} {{action upvoteDescription this}}>
<div>{{name}}</div>
<span>{{count}}</span>
</div>
{{/each}}
find (by query) doesn't actively make sure it has the records that match the query, so you'll have to manually inject it into the results.
createDescription: function () {
var name = this.get('name'),
friend_id = this.get('controllers.friend').get('id'),
store = this.get('store'),
description = store.createRecord('description', {
name: name,
friend_id: friend_id
});
description.save();
this.pushObject(description);
},
or you can use a live record array (filter/all)
model: function () {
var store = this.get('store'),
friend = this.modelFor('friend');
store.find('description', {friend_id: friend.id});
return store.filter('description', function(record){
return record.get('friend_id') == friend.id;
});
}
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}}
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);
}
});