In one of my templates I would like to take an integer from the model and create html from it. I have a Customer model property called starRating with the value 3. From that value I would like to inject this html into the Customer template:
<i style="color: #dddddd" class="icon-star"></i>
<i style="color: #dddddd" class="icon-star"></i>
<i style="color: #dddddd" class="icon-star"></i>
<i style="color: #dddddd" class="icon-star-empty"></i>
<i style="color: #dddddd" class="icon-star-empty"></i>
Where do I put the logic which creates that html? I tried adding a computed property to the view but the whole function definition gets injected in the page as plaintext. Creating a helper/component seems excessive for this tiny snippet that will only be used once in the page.
There might be many solutions to do something like a dynamic rating bar, so here is my try.
StarRatingComponent
Define a new component (see here for more info on components) that holds the logic for the star rating, and the best of all it's reusable. Note also that it's dynamic in terms of how many stars the rating bar will show, the number is defined in the customer model (as you will see below) for simplicity, but could come from everywhere:
App.StarRatingComponent = Ember.Component.extend({
maxStars: 0,
starRating: 0,
stars: [],
didInsertElement: function() {
this.initStars();
this.setStars();
},
initStars: function() {
var stars = [], i = 0;
for(i = 0; i < this.get('maxStars'); i++){
stars.pushObject(Em.Object.create({empty:true}));
}
this.set('stars', stars);
},
setStars: function() {
var counts = [], i = 0;
for(i = 0; i < this.get('starRating'); i++){
this.get('stars').objectAt(i).set('empty', counts[i]);
}
}
});
Pseudo Customer model
I've just defined a pseudo model since I don't know how your's looks like, that holds the information:
App.Customer = DS.Model.extend({
starRating: DS.attr('number'),
maxStarRating: DS.attr('number', {defaultValue: 5})
});
star-rating component template
Now let's backup our rating bar with a template that will render based on how the component is parametrized (more on this below)
<script type="text/x-handlebars" id="components/star-rating">
{{#each star in stars}}
<i style="color: #AA2567" {{bindAttr class=":glyphicon star.empty:glyphicon-star-empty:glyphicon-star"}}></i>
{{/each}}
</script>
Implementation
Now having everything setup, the actual implementation is fairly simple, with this line:
{{star-rating starRating=customer.starRating maxStars=customer.maxStarRating}}
we render out component providing the rating value starRating and how many stars the dynamic bar should render with maxStars, as you will see in the demo we use the information randomly generated (for simplicity) in our models:
...
{{#each customer in model}}
<li>Rating: {{customer.starRating}}
{{star-rating starRating=customer.starRating maxStars=customer.maxStarRating}}</li>
{{/each}}
...
Maybe this is not the solution you where after, but I guess it get's you in the right direction on how you could do it.
See here for a working demo.
Hope it helps.
Related
Hi all I have an issue i want to add another item to my model that is dynamically rendered on the UI.
However after i add it It still doest render on the UI. Do I need to call a function after adding it to the list of models?
Here is an example:
http://emberjs.jsbin.com/mugodifiki/edit?html,js,output
on clicking on an image it is suppose to add a predefined model to the array and display it on the UI
This is clearly an issue caused from the integration with owlcarousellibrary. A simple example of dynamically changing the model and rendering the ui, in plain emberjs, can be found here,
http://emberjs.jsbin.com/lodetofere/edit?html,js,output (simply click on the list).
This specific issue is caused due to the modifications of the DOM by the owlcarousel library. So to solve this, it is required to refresh the owlcarousel after changing the model and restoring the initial state of the dom.
Example,
http://emberjs.jsbin.com/qanocidoye/edit?html,js
The content part of the template is actually refreshed when the model changes by toggling a property and the carousel is reinitialized.
hbs
<script type="text/x-handlebars" data-template-name="components/test-component">
{{#if refresh}}
{{partial "owlContent"}}
{{else}}
{{partial "owlContent"}}
{{/if}}
</script>
<script type="text/x-handlebars" data-template-name="_owlContent">
<div class="owl-carousel owl-theme">
{{#each titleModels}}
<div class="item">
<img {{action "owlItemClicked" this on="click" }} {{bind-attr src=img}}>
</div>
{{/each}}
</div>
</script>
js
App.TestComponentComponent = Ember.Component.extend({
// classNames: ['owl-carousel', 'owl-theme'],
items: 8,
touchDrag: true,
mergeFit: false,
center: true,
addClassActive: true,
mouseDrag: true,
slideSpeed : 1000,
margin: 2,
refresh:false,
initCarousel:function(){
var self = this;
var options = {};
options.items = self.get('items');
options.touchDrag = self.get('touchDrag');
options.mergeFit = self.get('mergeFit');
options.center = self.get('center');
options.addClassActive = self.get('addClassActive');
options.mouseDrag = self.get('mouseDrag');
options.slideSpeed = self.get('slideSpeed');
options.margin = self.get('margin');
self._options = options;
self._owl = self.$(".owl-carousel").owlCarousel(options);
},
didInsertElement: function(){
this.initCarousel();
},
refreshCarousel:function(){
var self = this;
Em.run.next(function(){
self.initCarousel();
});
}.observes('refresh'),
actions: {
owlItemClicked: function(titleModel) {
var self = this;
var content = "<div class=\"item dodgerBlue\"><h1>test</h1></div>";
var newModel = Ember.Object.create({ index: 3, img: "http://www.cs.rice.edu/~dwallach/comp314_f99_ga/%257Eavinash/comp314/project3/pretty/pic1s.jpg"});
console.log(this.get('titleModels'));
//THIS ADDS IT TO THE MODEL BUT IS NOT RENDERED ON UI
this.get('titleModels').push(newModel);
alert('new model has been added');
console.log(this.get('titleModels'));
this.toggleProperty("refresh");
}
}
});
You may come up with a more elegant solution but this is the main idea.
I'm having hard time to understand the different ways to bind models to templates in Ember and when use one or the others.
Following different tutorial my code should work but it doesn't.
cal.hbs
<div class="calendar">
{{#each cal}}
{{#if days_label.length}}
{{days_label.[1]}}
{{/if}}
{{test}}
{{/each}}
</div>
cal_model.js
WebCalendar.Cal = DS.Model.extend({
today: DS.attr('date'),
test: DS.attr('number'),
days_label: DS.attr('array'),
months_label: DS.attr('array'),
days_per_month: DS.attr('array')
});
WebCalendar.Cal.FIXTURES = [{
id: 1,
test: 2,
today: new Date(),
days_label: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
months_label: ['January', 'February', 'March', 'April',
'May', 'June', 'July', 'August', 'September',
'October', 'November', 'December'],
days_per_month: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
}];
Router.js
WebCalendar.CalRoute = Ember.Route.extend({
model: function(){
return this.store.find('cal');
}
});
cal_controller.js
WebCalendar.CalController = Ember.ArrayController.extend({
month: function(){
var today = this.get('test');
return today + 1;
}.property('model'),
monthCal: function(year, month){
var today = this.store.find('cal', { today: new Date() });
this.year = today.getFullYear();
this.month = today.getMonth();
},
updateContent: function() { //doesn't work if is called arrangedContent
return this.get('model').content;
}.property('model')
});
For what I understood there are these ways to hook the model:
First
<div class="calendar">
{{#each cal}}
{{#if days_label.length}}
{{days_label.[1]}}
{{/if}}
{{test}}
{{/each}}
</div>
Second
<div class="calendar">
{{#each cal in controller}}
{{#if days_label.length}}
{{days_label.[1]}}
{{/if}}
{{test}}
{{/each}}
</div>
Third
<div class="calendar">
{{#each item in arrangedContent}} //this work if I don't specify arrangedContent in my controller
{{#if item.days_label.length}}//this doesn't work
{{item.days_label.[1]}}//this doesn't work
{{/if}}
{{item.test}}// this works
{{/each}}
</div>
What I want to understand and I can't find good explanation around is: what's the difference between those ways, when use what and what exactly is and does arrangedContent
Thank you very much for any explanation!
ArrangedContent ,if Im not wrong , is an array generated by
EMBER.SORTABLEMIXIN CLASS that is nowday implemented by default to EMBER.ARRAYPROXY CLASS , so your arraycontrollers inherit the arrangedcontent property.
What it really is , an array that is your controller's content , sorted by the properties you'll define in your controller (sortProperties, sortAscending).
So if I have a controller content(that is an array of objects that have the property name) , I can choose to sort them by the name property (sortProperties: ['name'] , thus rendering them in alphabetical order ascending (sortAscending: true).
The difference between your first and second question, is what is passed inside the loop as the current object.
In first you will call {{days_label}} to get the days_label that is equivalent as saying this.days_label.
In your second example though you can call {{days_label}}, but also call cal.days_label that in both cases is the same but it's needed if you were let's say go deeper inside an iteration, as:
{{#each cal in controller}}
{{#if days_label.length}}
{{#each day in days_label}}
{{day.someProperty}}
{{days_label.someOtherProperty}}
{{/each}}
{{/if}}
{{test}}
{{/each}}
In this example I could just do {{#each controller}} ,inside it {{#each days_label}} and then do {{someProperty}} inside it , but then I wouldn't be able to do the same for {{days_label.someOtherProperty}} because this would have changed.
One more thing to consider (very usefull when you are using ember data), is the difference between the model and the content of the controller.
The content is set inside route's setupController (controller, model) but can be overwritten.
UPDATE
Thank you again for your answer :) Right, the code is bit messy now but what i'm try to achieve now is very simple. With the model that I have, I want to display the current month. I try to take 'today' from my model, in my controller, do today.getMonth() and save as property(month), so i should be able to display in the hbs with {{month}}. – Giorgia Sambrotta
What I would do then is move the logic from the controller inside the model.
Some basic things to understand about ember data is that calling this.store.find('resourceName'), makes a request to your api's resoure index route thus returning an array of objects.
This call will be triggered each time you access this route because ember is agnostic of whereas your records have been changed at your server.
So given that you get an array of objects that all have some sort of date in them and you just want to get their month from it , you can define a computed property inside your WebCalendar.Cal model.
Inside WebCalendar.Cal
month: (function() {
var today = this.get('today');
//do something to get the value you want here
}).property('today')
Another thing to consider is that since months_label, days_label and days_per_month are static arrays you really don't need to pass them in each record but instead move them inside your model too, as properties.
So now that we've moved logic to the model you can just call {{month}} inside itteration and this will give you the result of the model's computed property. Also if a cal record happens to change today you will get that change imediately.
The only reason you wouldn't want to do it this way is if you wouldn't want this month property anywhere else except this specific controller.
In this case you would go
Inside WebCalendar.CalController
setMonthToEachCal: function(){
this.get('content').forEach(function(cal) {
today = cal.get('today');
//prepare the month value here and set it in month var
cal.set('month', month);
});
}.observes('content.#each.today'),
I'm new to Ember and am finding some of their concepts a bit opaque. I have a app that manages inventory for a company. There is a screen that lists the entirety of their inventory and allows them to edit each inventory item. The text fields are disabled by default and I want to have an 'edit item' button that will set disabled / true to disabled / false. I have created the following which renders out correctly:
Inv.InventoryitemsRoute = Ember.Route.extend({
model: function(params) {
return Ember.$.getJSON("/arc/v1/api/inventory_items/" + params.location_id);
}
<script type="text/x-handlebars" data-template-name="inventoryitems">
{{#each}}
<div class='row'>
<p>{{input type="text" value=header disabled="true"}}</p>
<p>{{input type="text" value=detail disabled="true"}}</p>
<button {{action "editInventoryItem" data-id=id}}>edit item</button>
<button {{action "saveInventoryItem" data-id=id}}>save item</button>
</div>
{{/each}}
</script>
So this renders in the UI fine but I am not sure how to access the specific model to change the text input from disabled/true to disabled/false. If I were just doing this as normal jQuery, I would add the id value of that specific model and place an id in the text input so that I could set the textfield. Based upon reading through docs, it seems like I would want a controller - would I want an ArrayController for this model instance or could Ember figure that out on its own?
I'm thinking I want to do something like the following but alerting the id give me undefined:
Inv.InventoryitemsController=Ember.ArrayController.extend({
isEditing: false,
actions: {
editInventoryItem: function(){
var model = this.get('model');
/*
^^^^
should this be a reference to that specific instance of a single model or the list of models provided by the InventoryitemsRoute
*/
alert('you want to edit this:' + model.id); // <-undefined
}
}
});
In the Ember docs, they use a playlist example (here: http://emberjs.com/guides/controllers/representing-multiple-models-with-arraycontroller/) like this:
App.SongsRoute = Ember.Route.extend({
setupController: function(controller, playlist) {
controller.set('model', playlist.get('songs'));
}
});
But this example is a bit confusing (for a couple of reasons) but in this particular case - how would I map their concept of playlist to me trying to edit a single inventory item?
<script type="text/x-handlebars" data-template-name="inventoryitems">
{{#each}}
<div class='row'>
<p>{{input type="text" value=header disabled="true"}}</p>
<p>{{input type="text" value=detail disabled="true"}}</p>
<button {{action "editInventoryItem" this}}>edit item</button>
<button {{action "saveInventoryItem" this}}>save item</button>
</div>
{{/each}}
</script>
and
actions: {
editInventoryItem: function(object){
alert('you want to edit this:' + object.id);
}
}
Is what you need. But let me explain in a bit more detail:
First of all, terminology: Your "model" is the entire object tied to your controller. When you call this.get('model') on an action within an array controller, you will receive the entire model, in this case an array of inventory items.
The {{#each}} handlebars tag iterates through a selected array (by default it uses your entire model as the selected array). While within the {{#each}} block helper, you can reference the specific object you are currently on by saying this. You could also name the iteration object instead of relying on a this declaration by typing {{#each thing in model}}, within which each object would be referenced as thing.
Lastly, your actions are capable of taking inputs. You can declare these inputs simply by giving the variable name after the action name. Above, I demonstrated this with {{action "saveInventoryItem" this}} which will pass this to the action saveInventoryItem. You also need to add an input parameter to that action in order for it to be accepted.
Ok, that's because as you said, you're just starting with Ember. I would probably do this:
<script type="text/x-handlebars" data-template-name="inventoryitems">
{{#each}}
<div class='row'>
<p>{{input type="text" value=header disabled=headerEnabled}}</p>
<p>{{input type="text" value=detail disabled=detailEnabled}}</p>
<button {{action "editInventoryItem"}}>edit item</button>
<button {{action "saveInventoryItem"}}>save item</button>
</div>
{{/each}}
</script>
with this, you need to define a headerEnabled property in the InventoryitemController(Note that it is singular, not the one that contains all the items), and the same for detailEnabled, and the actions, you can define them also either in the same controller or in the route:
App.InventoryitemController = Ember.ObjectController.extend({
headerEnabled: false,
detailEnabled: false,
actions: {
editInventoryItem: function() {
this.set('headerEnabled', true);
this.set('detailEnabled', true);
}
}
});
that's just an example how you can access the data, in case the same property will enable both text fields, then you only need one, instead of the two that I put . In case the 'each' loop doesn't pick up the right controller, just specify itemController.
An application I'm developing includes a User model, and users have scores. I'm trying to create a "leaderboard" page that will rank users according to their scores. I anticipate thousands of people using the app, so I'd like the rankings to be viewable in three ways:
A Top 100 view that simply lists the top 100 users.
A You view that lists the 10 people above you, then you, then the 10 people below you.
A Friends view that shows you and your friends.
I'm not sure what the "best" way to go about this is in Ember. My attempt so far seems like I'm repeating a lot of code and/or putting things in the wrong place. Here's my code:
Routes:
App.Router.map(function() {
this.resource('leaderboard', function() {
this.route('you', { path: '/' });
this.route('friends');
this.route('top');
});
});
App.LeaderboardYouRoute = Ember.Route.extend({
model: function() {
var users = App.store.find(App.User);
// TODO: sort users by descending score
// TODO: filter out users with an index too far from the user's
return users;
}
});
App.LeaderboardFriendsRoute = Ember.Route.extend({
model: function() {
var users = App.store.find(App.User);
// TODO: sort users by descending score and filter out non-friends
return users;
}
});
App.LeaderboardTopRoute = Ember.Route.extend({
model: function() {
var users = App.store.find(App.User);
// TODO: sort users by descending score and take the top 100
return users;
}
});
leaderboard.handlebars:
<section id="leaderboard">
<nav>
<ul>
<li>{{#linkTo "leaderboard.you"}}You{{/linkTo}}</li>
<li>{{#linkTo "leaderboard.friends"}}Friends{{/linkTo}}</li>
<li>{{#linkTo "leaderboard.top"}}Top 100{{/linkTo}}</li>
</ul>
</nav>
{{ outlet }}
</section>
leaderboard_top.handlebars, leaderboard_you.handlebars, leaderboard_friends.handlebars:
{{#each user in model}}
<li data-user-id="">
<span class="score">{{ user.score }}</span>
<div class="photo"></div>
<span class="name">{{ user.name }}</span>
<div class="clearboth"></div>
</li>
{{/each}}
It seems redundant to me to have three different templates with the exact same code. Similarly, I'm going to have to write a lot of the same code that basically says, "Sort users by descending score". Is there a better way to do this?
You can easily use the same template for different controllers/views. Which template is used, can be defined in the view. Example:
ExampleView = Ember.View.extend({
templateName : "theSameTemplateAgainAndAgain"
});
hth, ph
Update
As the first two comments of this answer point out, the solution may still be improved if the views are not used at all. Define the template to be rendered directly on the Route as shown below.
App.ExampleRoute = Ember.Route.extend({
renderTemplate: function() {
this.render('theSameTemplateAgainAndAgain');
}
});
I have an ember.js app. which shows a list of objects (let's call them Post objects) using posts.handlebars. Each list item contains a link to show the details for that Post (post.handlebars).
Both the list item and the detail page contain a delete link which removes the object from the collection of Posts. Since there's no difference in implementation except for the label that show the link, it makes sense to keep it DRY.
The current code is working:
# router
App.Router = Em.Router.extend({
...
"delete": function(router, event) {
var post = event.context;
if (confirm("Are you sure you want to delete the post with title '" + (post.get('title')) + "'?")) {
post.deleteRecord();
post.store.commit();
App.router.transitionTo('posts.index');
}
}
});
# posts.handlebars
<ul>
{{#each post in controller}}
<li>
{{post.title}}
<a {{action delete post}}>x</a>
</li>
{{/each}}
</ul>
# post.handlebars
<p>{{title}}</p>
<a {{action delete content}}>Destroy</a>
But I don't want to repeat the code that contains the delete action.
My next best guess is to define a view and re-use this in both templates.
However, now I'm not able to pass the Post object as context to the action by moving it to a view (I might be doing something rong).
By moving the event from the router to the view I got it working, but that doesn't feel right.
My current solution looks like this:
App.DeletePostView = Em.View.extend({
mouseUp: function(event) {
var id, post;
id = this.get('content.id');
post = App.Post.find(id);
if (confirm("Are you sure you want to delete the post with title '" + (post.get('title')) + "'?")) {
post.deleteRecord();
post.store.commit();
App.router.transitionTo('posts.index');
}
}
});
# posts.handlebars
<ul>
{{#each post in controller}}
<li>
{{post.title}}
{{#view App.DeletePostView contentBinding="post"}}
x
{{/view}}
</li>
{{/each}}
</ul>
# post.handlebars
<p>{{title}}</p>
<div>
{{#view App.DeletePostView contentBinding="this"}}
Destroy
{{/view}}
</div>
Does anyone know if there is a better approach if I want to re-use a handlebars action helper?