I have an Ember 2.11 application template with a few component placeholders at the moment (menu, breadcrumbs, related items) and an outlet which displays the main content which works fine. Now that I'm feeling more comfortable with the basics, I'm ready to try getting the breadcrumbs working.
I read about services, but I don't see that it is the right solution for breadcrumbs because it doesn't need to be persistent, it is based off the route. Although it is based off the route, I don't want to use the route literally as I want to use nice titles, and when viewing a specific item, the route doesn't accurately reflect what the breadcrumbs should show.
Since the breadcrumbs is based off the model that is being used for the display, I feel that I should be able to construct a breadcrumb object and then pass that into the component from the application template. I suppose this was the purpose of the controller back in the day. My thought was in the route to construct a breadcrumb object/property and return it with the model like RSVP and then I could access both in the template to pass the appropriate object to the appropriate component. But that seems wrong as the route should return an Ember data object, promise or Javascript array.
My current line of thinking is along these lines.
template/application.hbs
{{bread-crumbs crumbs=model.breadcrumbs}}
{{outlet}}
route/category/show
export default Ember.Route.extend({
model(params) {
let recipe = this.get('store').query('recipe', { category: params.category_id});
let crumbs = [{name: 'Category', link: 'category'},
{name: recipe.category.title, link: 'category.show', target: recipe.category.id}];
return {recipe: recipe, breadcrumbs: crumbs};
}
});
I'm not sure if this is the right way to approach this or if this will cause problems with async data fetching with Ember data. Something like this I would have to define on each route, but I don't have a lot of routes and seems to offer flexibility when I'm displaying a recipe (route is /recipe/recipe_id), but have the breadcrumbs show Home > Categories > Main Dishes > My Awesome Dish.
How would you approach the problem?
Updated 2017-02-10:
It appears that the model is not passed to the application template, only the route template. I'm not sure how to pass data 'down' to the application template.
You could probably create a breadcrumb like this by tracking the elements in a service, but I'd check out the ember-crumbly addon. It seems like it will meet your needs.
Remaining in your thinking line, if you want to pass your model as a variable of your controller in the route that you are accessing you need something like this:
export default Ember.Route.extend({
model(params){
let recipe = ...;
let crumbs = ...;
return {...};
},
setupController(controller, model){
this._super(controller, model);
controller.set('variable', model);
}
});
Having this, in your controller you can access to the crumbs like this:
this.get('variable');
And in then with an Ember.computed you can create a variable for display in your template.
Sorry for the minimize your code but I'm not in my computer.
Another approach is setting the crumbs variable at the init of the application controller, this set the variables defined in that controller global to the application, so you can modify them from other controllers and in the application controller lookup for that changes via didUpdateAttrs() or with didUpadteElement().
Hope this resolve your problem.
Greetings
Related
I have a route defined in the Router as:
...
this.resource('cart', {path: 'my/cart'});
...
MyApp.CartRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('cart');
}
});
In my CartController (ArrayController) I have this line of code in an action (addToCart):
this.transitionToRoute('cart', this.get('model'));
In some other route's template, I call that action:
<button {{action 'addToCart' product target='controllers.cart'}}>Add To Cart</button>
When that button is clicked, I get this error:
Uncaught Error: More context objects were passed than there are dynamic segments for the route: cart
To my understanding, Ember should have recognized that I was passing it a model and skipped the model hook, which is what I want.
This can be fixed by adding a random, useless dynamic segment to my route definition then defining serialize in my CartRoute. However, if there is a better way, I would rather do that.
Am I going about this the wrong way?
In
this.transitionToRoute('cart', this.get('model'));
you're sending some data object to the route cart which doesn't have a dynamic segment. The error message should disappear with the line
this.transitionToRoute('cart');
That said, I've got a feeling that the architecture of your application might be improvable. But I'd need more code to be sure. Anyway, you should have a look if putting the action handler somewhere else and using bubbling actions without the target could be an option. Again, that's just a feeling, I don't know enough of your code to be sure.
A little late to the game here, but it sounds like you may want to have your cart be (/be managed by) a service, rather than being fetched as a model for a bunch of different specific routes. In fact, a shopping cart is the specific example currently used on the Ember docs page explaining the use of Services: https://guides.emberjs.com/release/services/#toc_defining-services
This would allow you to manage the fetching and local caching of the cart at the service level. You'd then either inject that service directly into the relevant controllers, or inject the service into the route, and have the route return the service-managed cart object as it's model.
I am building an admin dashboard using ember. I want to create a reusable chart object of which I can have multiple instances throughout the application. The chart object should have a template consisting of some markup and a canvas element of which I need the id after insertion in the DOM in order to attach the actual chart (chart.js). I have tried several approaches, but I can not seem to figure out the right architecture to do this.
What would be the right architecture in ember to achieve the above?
Thanks!
Ember.Component is your friend
As #raulbrito already mentioned, the best way to go if you want reusable components in ember is indeed to use the new Ember.Component which is heavily based on the new w3 draft for web components and thus beeing future proof.
I've tried to make a simple example on how this could be implemented.
Given a simple route where the model hook returns some static data:
Index Route
App.IndexRoute = Ember.Route.extend({
model: function(){
return Ember.Object.create({
modelOne: data,
modelTwo: data2
});
}
});
data and data2 are simply static objects globally defined for simplicity (as you will see in the demo), but this could be also data coming from a backend or from fixtures etc.
Index template
In the template then we insert our chart component with the line {{line-chart data=model.modelOne}} and as you can see, we also set the data attribute to the index model model.modelOne or model.modelTwo:
<script type="text/x-handlebars" id="index">
<h2>Chart one</h2>
{{line-chart data=model.modelOne}}
<h2>Chart two</h2>
{{line-chart data=model.modelTwo}}
</script>
Component Template
Our component template looks fairly simple because it will render a simple canvas element, but it could be as complex as needed, on how to use Ember.Component please refer also to the docs:
<script type="text/x-handlebars" id="components/line-chart">
</script>
Component Subclass
App.LineChartComponent = Ember.Component.extend({
tagName: 'canvas',
attributeBindings: ['width', 'height'],
width: '480',
height: '360',
data: null,
didInsertElement: function() {
var ctx = this.get('element').getContext("2d");
var myNewChart = new Chart(ctx).Line(this.get('data'));
}
});
Note the naming is important here, Ember knows which subclass powers a component based on its name. For example, if you have a component called line-chart, you would create a subclass called App.LineChartComponent. If your component was called bar-chart-simple, the class name would be App.BarChartSimpleComponent and so on. Ember will look for a class with the camelized name of the component, followed by Component.
So, and since Ember.Component extends from Ember.View we can define all sorts of properties Ember.View supports like tagName. In our case we use canvas because this is what chart.js needs to work. As you can see we have also defined some attributeBindings to control the width and height of the canvas from inside ember. The component has also a data attribute (which could be called whatever you find appropriate) defined on which we later set our model data in the template returned from the IndexRoute model hook. And finally in your didInsertElement hook of our component we initialize the chart passing with this.get('data') the data object to new created Chart.js class.
var ctx = this.get('element').getContext("2d");
var myNewChart = new Chart(ctx).Line(this.get('data'));
And last but not least, please see here for a working example of the above explained.
Hope it helps.
Update in response to your last comment
I've tried to simulate a delay in the resolution of the model hook to mimic a response from a backend, as you can see the template rendering is waiting for the model promise to resolve first. Basically what I've done is to use Ember.run.later with a delay of 2000ms that resolves the promise once timed out:
App.IndexRoute = Ember.Route.extend({
model: function(){
return new Ember.RSVP.Promise(function(resolve) {
Ember.run.later(function() {
var m = Ember.Object.create({
modelOne: data,
modelTwo: data2
});
resolve(m);
}, 2000);
});
}
});
And just for fun I've also added a LoadingRoute to show a spinner while the promise resolution is waiting for data, the LoadingRoute is a less documented feature of ember, you can read more about it here: https://gist.github.com/machty/5647589 under How do I put up a (global) Loading Spinner during a transition w/ Promises?
Plase see here for a updated example: http://jsbin.com/odosoy/145/edit
Update in response to #SamSelikoff's comment
As for the above mentioned LoadingRoute #SamSelikoff pointed out that it's officially documented now: http://emberjs.com/guides/routing/defining-your-routes/#toc_initial-routes
I have some thoughts on this, so just throwing it out there, in case it helps you.
First of all, I would advise you to go and watch Sam Selikoff's presentation on using Ember with D3. All the info here: http://www.samselikoff.com/blog/2013/08/09/ember-d3-simple-dashboards/ . Also, don't miss the comments section on the blog post.
It is a great example on using Ember Views to wrap D3 objects, and can be a good reusable solution. The caveat here is that Ember Views require a backing controller that provides the data. Depending on where in the application you would want to reuse your charts, this might be inconvenience.
The alternative would be to use Ember Components. In that case, you just need to define the Component and associated handlebars template. The good thing about it is that it won't need any backing controller, therefore freeing you from a dependency, which might make it easier for you to add such a component in different places of your application. Without a concrete example, I think it's hard to reach a great conclusion, but maybe this will help you clarify things.
Assume you have the following routes in an Ember application.
App.Router.map(function() {
this.resource('series', function() {
this.resource('serie', { path: '/:serie_id' }, function() {
this.resource('seasons', function() {
this.resource('season', { path: '/:season_id' }, function() {
this.resource('episodes', function() {
this.resource('episode', { path: '/:episode_id' });
})
});
});
});
});
});
How would I link to a specific episode using the linkTo helper that Handlebars provides? In other words, how does Ember figure out what the other parameters of the URL should be, that is, the serie_id and episode_id? The documentation states that I should pass an episode model to the episode route as shown below.
{{#linkTo "episode" episode}}
This is to link to the following URL structure.
/series/:serie_id/seasons/:season_id/episodes/:episode_id/
When I use the linkTo helper like that, Ember throws an error telling me that it cannot call get with id on undefined. I assume that it uses the episode model to figure out what the serie_id and episode_id are and my guess is that the model needs to conform to a specific convention (structure or blueprint) for Ember to find these ids.
These are the aspects that I find most difficult about Ember. It isn't very transparent even if you use Ember in debug mode. Any pointers or references are much appreciated.
UPDATE 1: After some digging, I found out that the route's serialize method is a key element in accomplishing this. However, when I use the linkTo helper as illustrated above, the model passed to the route's serialize method is undefined for some reason (even though it is not when passed to the linkTo helper. The question that led to this discovery can be found here.
UPDATE 2: It turns out that the serieSeason route's serialize method receives the wrong model, an episode instead of a season, when the link is generated. It isn't clear, though, why it is receiving the wrong model. Where does the model parameter of the serialize method come from?
UPDATE 3: The linkTo helper works fine if I return static data from the serialize method of each route involved, which means that the linkTo helper isn't involved in the problem.
It turns out that the answer could be found in the properly documented source of Ember ... because that is what one does after searching the web for several days.
The answer is simple. The linkTo helper accepts more than one model. For each dynamic segment of the destination URL, you pass a corresponding model. Each passed model will become the model of the corresponding route in the destination URL. In the example that I describe above, this results in the following.
{{#linkTo "episode" serie season episode}}
The serie model will be passed to the serie route, the season model to the season route, and the episode model to the episode route. What confuses many developers is that the route's model hook isn't triggered when you use the linkTo helper. This isn't too surprising if you realize that the developer provides (or can provide) the model for the corresponding route by passing one or more models (or zero).
Because there isn't much documentation for deeply nested resources, it wasn't trivial to find out how the linkTo helper does its job under the hood. Diving in Ember's source definitely helps getting up to speed with the framework.
I have a search page where we are getting different types of search results. In the list of search results I would like to use
{{#linkTo 'someResources.someResource' result}}{{result.Name}}{{/linkTo}}
And on the route someResources.someResource I want to use a totally different model than on the search page. How do I do that? When I click on the link for the linkTo it doesn't load the model again, instead it tries to use the model named result here.
So what I would like to do is to reload the model when I navigate to someResources.someResource based on the values in result.
The I do have a model named App.SomeResource and a find method for it that works if I go directly to that page.
Ember will bypass the model() hook when using linkTo as you've discovered. The assumption is that you passed a model to it, so it and will use that(result) as the model.
The next hook you can use is setupController. Since you have a model hook that works on the direct route, you can use call it directly from here.
One caveat is that you need to also allow for the direct route loading where the model will already have loaded.
setupController: function(controller, model) {
if (!model.isModel) {
this.model().then(function(result)) {
controller.set('model', result)
}
}
}
model.isModel is this check via an isModel property on the directly loaded model, which should be absent when passed with linkTo.
Note: the above code assumes that you are returning a Promise in your model() hook.
Since the problem is that I want a full reload of the model when doing the transition using linkTo won't work since that is using the model given to it. The solution to the problem is actually quite simple, just use a regular html a-tag instead. What I ended up doing was this:
<a {{bindAttr href="somePropertyInYourModel"}}>{{someTextProperty}}</a>
The property somePropertyInYourModel is a property containing the url to the new page. If the url is in the ember routes it will be as if you where typing that address in the address bar and pressing enter, but without the full reload of the page.
I think this is something that could be improved in ember, it would be much nicer if I could write something like:
{{#linkToRoute "resourceA.routeB" params="val1,val2,val3"}}Go here{{/linkToRoute}}
given I have this routes set up:
App.Router.map(function() {
this.resource("resourceA", {{path: "/resourceA"}}, function() {
this.route("routeB", {{path: "/:prop1/:prop2/:prop3");
}
});
I would like to get:
Go here
The order of the val1,val2,val3 matters, if the order is changed they should also be changed in the final url.
I've discovered this jsFiddle and I'm now wondering what the best approach would be to display the list of contributors both in the IndexRoute and in all Subroutes of match('/contributor/:contributor_id') with the new v2 Router from Ember.js.
The problem I'm experiencing is that
App.IndexRoute = Ember.Route.extend({
model: function() {
return App.Contributor.find();
}
});
is only fetching the data on the specified / route.
When directly navigating to /#/contributor/[some_id] the data for the IndexRoute doesn't get loaded and the user would first have to navigate to / to see the list of contributors.
The only possible solution I've come to is to have a ContributorsView which gets called called in the application template. Meanwhile I don't know how to populate this view with data, as a view doesn't have the model property which the route has.
I'm not yet confortable with the new router implementation, but from a design point of view, I would use a different structure for the routes, for example by adding a contributors route, accessible from the index, and perhaps directly redirect to contributors.
Then you have always the contributors displayed, and when clicking on one of them you could see the details. A kind of master/detail blocks.
Here the router map:
App.Router.map(function(match){
match('/').to('home');
match('/about').to('about');
match('/contributors').to('contributors', function(match){
match('/').to('contributorsIndex');
match('/:contributor_id').to('contributor', function(match){
match('/').to('contributorIndex');
match('/details').to('contributorDetail');
match('/repos').to('contributorRepos');
});
});
});
Here is what I would do: http://jsfiddle.net/JLHuG/21/ does it work for you ?