Controlling component data across multiple routes - ember.js

I have a mapping app that has a full-screen map with a sidebar for information. This app has two routes:
one route that should display a list of places with markers on the map, for example /places/
one route that should display a single place with that particular place's marker centered on the map, for example places/1/
My map is currently a Component that is in application.hbs, so it is "outside" of the route templates and persists across route changes. It looks something like:
<div class="page">
<aside class="sidebar">
{{outlet}}
</aside>
<div class="content">
{{places-map ... }}
</div>
</div>
and my routes looks something like:
Router.map(function() {
this.route('index', { path: '/' });
this.route('place', { path: "/place/:place_id" });
this.route('places');
});
So while I have all this set up and working (I can see a list of places and move a single particular place, in both cases with the map in the "background"), I can't understand how my routes can feed information to my component or simply how my routes can communicate with the component that is sitting "outside" of their context?
Is this a possible pattern with Ember and is there a way to achieve it?

Ditto on what #GerDner said about data-down-actions-up.
Starting from the top:
application/controller.js
import Ember from 'ember';
export default Ember.Controller.extend({
somethingDownFromController: null
});
application/route.js
import Ember from 'ember';
const {
set
} = Ember;
export default Ember.Route.extend({
actions: {
sendSomethingUp(something) {
set(this.controllerFor('application'), 'somethingDownFromController', something);
}
}
});
application/template.hbs
<div class="page">
<aside class="sidebar">
{{outlet}}
</aside>
<div class="content">
{{places-map
something=somethingDownFromController
}}
</div>
</div>
place/route.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return {
somethingFromNestedRoute: 'boooooyaaaaah'
}
}
});
place/template.hbs
<button {{action 'sendSomethingUp' model.somethingFromNestedRoute}}>
Send model up
</button>
You might not need to send anything up with the action you're bubbling here. If that's the case then you can just grab what you need from the application controller or route and pass it down into places-map.
places-map/template.hbs
Insert something from the outer context:
<div>{{something}}</div>
Here's an ember-twiddle. I made a few notes in the router.js file that might be useful depending on the exact needs of your application.

The data-down/actions-up Pattern is the answer.
http://www.samselikoff.com/blog/data-down-actions-up/
You hold the data on a toplevel component/controller and pass the data down to child components. Data changes are triggered via actions on the child component and handled by the toplevel component/controller via action bubbling. So you need only one component/controller which knows how to change the data and how to get data.

Related

Ember component call an action in a route or controller

I have a component the main purpose of which is to display a row of items.
Every row has a delete button to make it possible to delete a row. How is possible to pass an action from a template to the component which will trigger an action in a router ?
Here is the template using the component:
#templates/holiday-hours.hbs
{{#each model as |holidayHour|}}
{{holiday-hour holiday=holidayHour shouldDisplayDeleteIcon=true}}
{{/each}}
Here is the component template:
# templates/components/holiday-hour.hbs
...
div class="col-sm-1">
{{#if shouldDisplayDeleteIcon}}
<button type="button" class="btn btn-danger btn-sm mt-1" {{action 'deleteHoliday' holiday}}>
<span class="oi oi-trash"></span>
</button>
{{/if}}
</div>
I'm using the same component to display a row and to create a new item (holiday-hour).
I'm using ember 3.1.2
Thank you
You have to send the actions up from the component to the route. The main way to do this is by adding actions to your component that "send" the action to the parent. Once the action is sent you have to tell the component what action on the route to trigger by passing in the action as a parameter. Below is an example of how to do this.
Component js
# components/holiday-hour.js
...
actions: {
deleteHoliday(){
this.sendAction('deleteHoliday');
}
}
Template for route
#templates/holiday-hours.hbs
...
{{#each model as |holidayHour|}}
{{holiday-hour holiday=holidayHour shouldDisplayDeleteIcon=true deleteHoliday='deleteHoliday'}}
{{/each}}
Route js
#routes/holiday-hours.js
...
actions: {
deleteHoliday(){
//code to delete holiday
}
}
I will try to give a general answer because your question is not giving enough/all info regarding the route actions etc. Long answer short, using closure functions. Assuming this is your route js file routes/holiday-hours.js
import Route from '#ember/routing/route';
export default Route.extend({
model(){ /*... some code */ },
setupController(controller){
this._super(controller);
controller.set('actions', {
passToComponent: function(param) { //.... function logic }
})
}
});
Note: in the above snippet, I'm using setupController to create actions. Alternatively, you can put the actions inside a controller file otherwise actions directly inside the route will throw an error.
So I want the action passToComponent to be called from the component. This is what you do to make it accessible inside the component.
{{#each model as |holidayHour|}} {{holiday-hour holiday=holidayHour shouldDisplayDeleteIcon=true callAction=(action 'passToComponent')} {{/each}}
Now we have passed the action to the component and here's how to call it from the component. Note: I have added a param just to show that it can take a param when called within the component.
import Component from '#ember/component';
export default Component.extend({
actions: {
deleteHoliday: ()=> {
this.get('callAction')() /*Pass in any params in the brackets*/
}
}
});
You will also see demonstrations using sendAction which is rather old and acts more of an event bus that is not very efficient. Read more from this article

Index Route vs Custom Route causes application.hbs model issues

I am working with Ember 2.9, and I'm facing a weird routing + model issue.
I have two routes that are identical (copied and pasted code inside the route) except for the pathing. I have one route which is '/', and another route which is 'my-route'.
import Ember from 'ember';
export default Ember.Route.extend({
model() {
let dict = {
myLog1Model: this.get('store').findAll('my-log1'),
myLog2Model: this.get('store').findAll('my-log2'),
myLog3Model: this.get('store').findAll('my-log3'),
myLog4Model: this.get('store').findAll('my-log4')
};
return dict;
}
});
When I access my ember application from localhost/ all model data is displayed properly, but when I access it from localhost/my-route none of the model data is being passed around in application.hbs
application.hbs
<section class='container-fluid'>
<div class='row'>
<div class='col-md-6'>
{{control-panel model=model}}
</div>
<div class='col-md-6'>
{{log-panel model=model}}
</div>
</div>
</section>
log-panel.hbs
{{myLog1 model=model.myLog1Model}}
{{myLog2 model=model.myLog2Model}}
{{myLog3 model=model.myLog3Model}}
{{myLog4 model=model.myLog4Model}}
Ember tables takes it from here...
myLog1.hbs
{{models-table
data=model
columns=columns
useNumericPagination=true}}
As I stated before this works perfectly fine with 'localhost/', but not with 'localhost/my-route'. Any idea why this is?
I was able to resolve my issue, by loading any application model data in the Application Route. I had no idea that there was a difference between '/', and the application route.

How to create action for my own component

I am creating one ember app.
Flow is like " page1 displays list of feeds item and clicking on any of the feed will take user to page2 showing details about that feed"
What i am doing:
i have one component named app-feed. Template is as below
<div onclick={{action 'click' feed}}>
{{#paper-card class="card-small" as |card|}}
<!-- --> {{card.image src=feed.imagePath class="small-feed-img" alt=feed.title}}<!---->
{{#card.header class="flex-box short-padding" as |header|}}
{{#header.avatar}}
<img class="profile-small" src="http://app.com/users/{{feed.userName}}.jpg" alt="{{feed.name}}" />
{{/header.avatar}}
<span class="tag-sm like-box">
{{feed.likes}} {{paper-icon "thumb_up" size="18"}}
{{feed.commentCount}}{{paper-icon "chat_bubble" size="18"}}
</span>
{{/card.header}}
{{#card.actions class="action-block"}}
{{#paper-button iconButton=true}}{{paper-icon "favorite" size="18"}}{{/paper-button}}
{{#paper-button iconButton=true}}{{paper-icon "share" size="18"}}{{/paper-button}}
{{#paper-button iconButton=true}}{{paper-icon "shopping_basket" size="18"}}{{/paper-button}}
{{/card.actions}}
{{/paper-card}}
</div>
component.js is as below
import Ember from 'ember';
export default Ember.Component.extend({
actions:{
click(feed){
console.log("Click event fired:"+feed.id); //Output is correct in console
this.sendAction("onClick", feed); //sending onClick Action
}
}
});
I'm populating list of this component in one of my route.
Template is as below
{{#app-sidenav user=model}}{{/app-sidenav}}
<div class="content">
<div class="row">
{{#each model as |item|}}
{{#app-feed-small onClick=(action "getDetail" item) class="col-xs-5" feed=item}} {{/app-feed-small}}
{{/each}}
</div>
</div>
route.js is as below
import Ember from 'ember';
export default Ember.Route.extend({
store: Ember.inject.service(),
model(){
//Populating module. Works just fine
} ,
actions:{
getDetails(feed){
console.log("Getting details of "+feed.id);
}
}
});
I have defined getDetails action as mentioned in my template.js of the route still i am getting below error
""Assertion Failed: An action named 'getDetail' was not found in (generated feed.index controller)""
feed.index is my route.
I used same method and modified paper-chip's source to get action corresponding to click on paper-chip's item which worked. But i am not able to do same in my own component.
Please let me know what is missing
Your problem is that in your second last code snippet, the one with your template. You refer to the action as getDetail but in route.js your last code snippet you declare the action as getDetails which is different to the code in your template. It's a common spelling error, one has an "s" st the end whereas the other doesn't.
The actions should be in controllers. And if controller bubbles up then the action in route be called.
For your case you don't need controller.
You can use ember-transition-helper
I assume you have in router.js :
this.route('feeds', function(){
this.route('edit', {path: '/:id'});
});
Now your template is going to be :
{#app-sidenav user=model}}{{/app-sidenav}}
<div class="content">
<div class="row">
{{#each model as |item|}}
{{#app-feed-small onClick=(transition-to "feeds.edit" item) class="col-xs-5" feed=item}} {{/app-feed-small}}
{{/each}}
</div>
</div>
sendAction is an ancient way to calling action inside controller/route.
The new style is to use closure action, which passes action as a value by creating a closure at the time of value passing.
Yes, you are correct. The action has been sendAction is able to bubble up from,
correspond controller -> correspond route -> upper route -> ... -> application route
However, closure action does NOT bubble.
Please refer to Ember component send action to route where #kumkanillam detailed explained how to call action inside route using different method and the differences between sendAction and closure action.
I have also made a sample project and write a simple explanation for it at,
https://github.com/li-xinyang/FE_Ember_Closure_Action

When is the index.hbs loaded in an Ember app? What is the difference between a component and a template?

I was following this Ember tutorial and this quickly got a lot more complicated. This was the tutorial that I was following.
I am lost as to what is going on. When is the index.hbs getting loaded and why? Here is my code starting with the router.js:
import Ember from 'ember';
import config from './config/environment';
var Router = Ember.Router.extend({
location: config.locationType
});
Router.map(function() {
this.route('todos', { path: '/'}, function() {
this.route('complete');
this.route('incomplete');
});
});
export default Router;
So it looks like our home url will load the todos.js route right? This is my code:
import Ember from 'ember';
export default Ember.Route.extend({
model() {
let todos = [
{
title: 'Learn Ember',
complete: false,
},
{
title: 'Solve World Hunger',
complete: false,
}
];
return todos;
}
});
So this todos.js route is my model right?
I assume ember also loads the todos.hbs template by default? Is that right? Or does it load the app/templates/todos/index.hbs? Which one does it load?
This is my app/templates/todos.hbs code:
<input type="text" id="new-todo" placeholder="What needs to be done?" />
{{#todo-list todos=model}}
{{outlet}}
{{/todo-list}}
This is my app/templates/todos/index.hbs code:
<ul id="todo-list">
{{#each model as |todo|}}
<!-- this loads the component todo-item and passes in a todo as todo -->
{{todo-item todo=todo}}
{{/each}}
</ul>
The tutorial doesn't really explain what is going on here. If the index.hbs gets loaded, does it then load the todo-item component template? If so, this is my app/templates/components/todo-item.hbs:
<input type="checkbox" class="toggle" checked="{{if todo.complete 'checked'}}">
<label class="{{if todo.complete 'completed'}}">{{todo.title}}</label><button class="destroy"></button>
In the event that the app/templates/todos.hbs gets loaded...What is going on in the app/templates/todos.hbs? Are we passing in the model (somehow accessible in the template?) as todos to the todo-list component? Here is the app/templates/components/todo-list.hbs
<section id="main">
{{yield}}
<input type="checkbox" id="toggle-all">
</section>
<footer id="footer">
<span id="todo-count">
<strong>2</strong> todos left
</span>
<ul id="filters">
<li>
All
</li>
<li>
Active
</li>
<li>
Completed
</li>
</ul>
<button id="clear-completed">
Clear completed (1)
</button>
</footer>
Welcome to the wonderful world of Emberjs! first of all, I recommend you to visit the official page of Emberjs. Can you see the sidebar menu? well, be ready to spend some time reading it if you want to understand how Emberjs works. I strongly recommend you to read at least Router, Template, Component and Controller sections to begin with.
Let's see some of the snippets you have provided:
Router.map(function() {
this.route('todos', { path: '/'}, function() {
this.route('complete');
this.route('incomplete');
});
});
This is where you define your routes. Here what you have is the main route called 'todos' but used as the root page (starting at /). After it, there are two more routes: /complete and /incomplete.
model() {
let todos = [
{
title: 'Learn Ember',
complete: false,
},
{
title: 'Solve World Hunger',
complete: false,
}
];
return todos;
}
Here you are defining a model in one route (I assume is the route of todos). Pretty straight, isn't it? if you were using Ember Data for example. you would ask the server for the model here and the route would wait until receive the response.
The reason why you have an index template and a todos template is simple: todos.hbs will contain the {{outlet}} in which every page will be rendered. Imagine it as a wrapper. Whatever comes after / will be wrapped by this todos.hbs, even the index.hbs. You have more info here, in the guides (reason why I recommend you to read it first).
Let's move to another snippet:
{{#todo-list todos=model}}
{{outlet}}
{{/todo-list}}
Here you are using a component to wrap whatever is rendered in the {{outlet}}. You haven't pasted it here, but it should contain in its template at least a {{yield}} to specify where the {{outlet}} will be rendered. That info about {{yield}} can be found here.
Let's move to the next part:
ul id="todo-list">
{{#each model as |todo|}}
<!-- this loads the component todo-item and passes in a todo as todo -->
{{todo-item todo=todo}}
{{/each}}
</ul>
This {{#each}} handlebar, expressed in a block way (that's why it uses the # at the beginning and the / at the end), is a loop that allows you work with each item of your model, defined as todo. What you are doing here is to provide the component todo-item with one item of the model. If your model has 3 todos, todo-item will be rendered 3 times, one for each of them.
Again, I recommend you to follow that tutorial having the emberjs guides opened and whenever you have a doubt, check the guides until you understand the concept and then, move to the next step.

Understand routing in Ember.js

I'm really struggling to understand the routing concept in Ember but is more complicate than what it seem. From the doc. you can see you have different route whenever there is different url or path and if you have different path in the same url, easy you just need to create a nested template.
But what about when you have 3 different path in one url?
And what's the difference from this.resource and this.route?
Since live example are always better than pure theory, here my app.
In index or '/' I should render "list template", "new template" and when user click on a list link, the "note template" is render instead "new template".
My router:
Notes.Router.map(function () {
this.resource('index', { path: '/' }, function (){
this.resource('list', {path: ':note_title'});
this.resource('new', {path: '/'});
this.resource('note', { path: ':note_id' });
});
});
My template:
<script type="text/x-handlebars" data-template-name="index">
<div class="wrap">
<div class="bar">
{{input type="text" class="search" placeholder="Where is my bookmark??" value=search action="query"}}
<div class="bar-buttons">
<button {{action "addNote"}}> NEW </button>
</div>
</div>
{{outlet}}
</div>
</script>
<script type="text/x-handlebars" data-template-name="list">
<aside>
<h4 class="all-notes">All Notes {{length}}</h4>
{{#each item in model}}
<li>
{{#link-to 'note' item}} {{item.title}} {{/link-to}}
</li>
{{/each}}
</aside>
</script>
<script type="text/x-handlebars" data-template-name="new">
<section>
<div class="note">
{{input type="text" placeholder="Title" value=newTitle action="createNote"}}
<div class="error" id="error" style="display:none"> Fill in the Title! </div>
{{input type="text" placeholder="What you need to remember?" value=newBody action="createNote"}}
{{input type="text" placeholder="Url:" value=newUrl action="createNote"}}
</div>
</section>
</script>
<script type="text/x-handlebars" data-template-name="note">
<section>
<div class="note">
{{#if isEditing}}
<h2 class="note-title input-title"> {{edit-input-note value=title focus-out="modified" insert-newline="modified"}} </h2>
<p class="input-body"> {{edit-area-note value=body focus-out="modified" insert-newline="modified"}} </p>
{{edit-input-note value=url focus-out="modified" insert-newline="modified"}}
{{else}}
<h2 {{action "editNote" on="doubleClick"}} class="note-title" > {{title}} </h2>
<button {{action "removeNote"}} class="delete"> Delete </button>
<p {{action "editNote" on="doubleClick"}}> {{body}} </p>
{{input type="text" placeholder="URL:" class="input" value=url }}
{{/if}}
</div>
</section>
</script>
Or here the Js Bin: http://jsbin.com/oWeLuvo/1/edit?html,js,output
If my controllers or model are needed I will add that code as well.
thanks in advance
Your example seems to be working.
You just miss dependencies. You haven't included Handlebars and Ember.data
If you'd have checked your javascript console, you'd have seen the error thrown.
working example: http://jsbin.com/oWeLuvo/2/
In Ember a resource and a route are both routes. They have two names in order for Ember to differentiate between what is a resource and a route. In all honesty to remember that they are both routes and to keep your sanity you could refer to them respectively as a 'resource route' and a 'route'. A resource can be nested and have children resources or routes nested within them. Routes on the other hand cannot have nested anything.
Install the Ember Inspector if you are not already using it. It is a chrome extension and will help you with routes, controllers, templates, data and alot of other things with Ember, that you install into the Chrome Web Browser. The last that I heard the Ember Inspector is available in the FireFox Dev Tools as well. https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi?hl=en
So if have a resource, you can nest a resource, and a route. The nested resources will preserve their name space, routes will get appended to nested name space. Remember you can not nest anything within a route.
App.Router.map(function() {
//creating a resource
this.resource('foo', {path: 'somePathYouPut'}, function() {
//nesting stuff inside of the resource above
//could open another nest level here in bar if you want
this.resource('bar');
//can not nest in route. Sad face. Sorry
this.route('zoo');
});
});
Since you can not nest anything into a route your {{outlet}} in the index template does not have any children to look for and by default and render into that {{outlet}}. I believe that is what you think is going to happen.
<script type="text/x-handlebars" data-template-name="index">
<div class="wrap">
<div class="bar">
{{input type="text" class="search"
placeholder="Where is my bookmark??" value=search action="query"}}
<div class="bar-buttons">
<button {{action "addNote"}}> NEW </button>
</div>
</div>
{{outlet}}
</div>
</script>
In your code you referred to the index as a resource, its a route. Since the index is a route, remember you can not nest elements within a route, your code should have looked more like this. Also your resource 'new' path / can be removed as well.
Notes.Router.map(function () {
//this is a route not a resource you had it as resource
//and tried to nest in your code
this.route('index', { path: '/' });
this.resource('list', {path: ':note_title'});
this.resource('new');
this.resource('note', { path: ':note_id' });
});
You get an index at each nested level starting with the top most level which comes from the application level but you don't have to explicitly define them in the Router they are just there. The index route that you get for free at each nested level is associated with its immediate parent by default and render into its parents 'outlet' by default. You could think of the Router looking something like this.
For Illustrative purposes only:
Notes.Router.map(function() {
//think of this as your application level stuff that Ember just does!!
//this is not written like this but to illustrate what is going on
//you would get application template, ApplicationRoute, ApplicationController
this.resource('application', function() {
//this is an index that you get for free cause its nested
//its that index template, IndexController, and IndexRoute you were using
this.route('index', { path: '/' });
this.resource('foo', {path: 'somePathYouPutHere' }, function() {
//since you started another nested level surprise you get another `index`
//but this one is `foo/index` template.
this.route('index', {path: '/'});
this.route('zoo');
});
});
});
The first part of the above exaggerated router example, Ember does automatically behind the scenes, its part of the 'magic' you hear about. It does two things it sets up an Application environment for its self and you get ApplicationRoute, ApplicationController, and a application template which are always there behind the scene. Second it makes that index and you get IndexController, IndexRoute, and a index template that you can use or ignore. So if you do nothing else, no other code that declaring and Ember App in a file like var App = Ember.Application.create(); and open the Ember Inspector and look into routes you will see the above mentioned assets.
Now, the resource foo in the above router is an example of a resource you would make and as you see you get an index in there because you started to nest. As mentioned above you do not have to define the index at each nest level, this.route('index', {path: '/'}); from inside foo could be totally omitted and Ember will still do the same thing. You will end up with foo/index template, FooIndexRoute, FooIndexController along with the expected foo template, FooRoute, and FooController. You can think of thefooindex as a place that says 'hey' before anything else gets rolled into my parentfoo` and gets rendered I can show something if you want, use me.
This is also a good time to highlight what happens with namespace when you nest route in a resource like this.route('zoo') in the above example. The namespace of the route zoo is now going to be appended to resource foo and you end up with foo/zoo template, FooZooRoute and a FooZooController.
If you were to change zoo to a resource nested in the foo resource this.resource('zoo'); the namespace will be keep for zoo. You will end up with 'zoo' template, ZooRoute and a ZooController. The namespace is kept. Ok, enough side tracking what about your App.
You said that you wanted / url of your app to render the list template. In order to accomplish that you have to override the default behavior that happens when Ember boots up. You override the top level / by adding the {path: '/'} to the first resource or route in the Router. From the fake router code above the first index route you get is associate with the application. By default if you do nothing Ember will push that index template into the application template. However that is not what you want you want your list template to be pushed into the application template when you hit the base url of /' for your App.
Notes.Router.map(function() {
this.resource('list', {path: '/'});
this.resource('new');
this.resource('note', { path: ':note_id' });
});
By adding the code {path: '/'} to the first resource like I did above, you are now telling Ember 'hey' when my app url reaches the base url of / or when you boot up use my list template not your default index template and render that into the application template. In addition since the other resources are not nested when your App transitions to those routes via the url they will blow out whats in the application template {{outlet}} and replace themselves in there.
You mentioned about defining a "path" in a route what that does is tell Ember how you want to represent that route in the url. In the above example if you leave the new resource as is, Ember by default will use the routes name as the path, the url would be /new. You can put any name in path for the new resource, this.resource(new, {path :'emberMakesMeKingOfWorld'}); and that name will be represented in the url as /emberMakesMeKingOfWorld and that long thing will still be associated with you new route. Your user might be confused what this url means but behind the scence you would know its tied to your new route. Its just an example but probably good practice to use descriptive names so your user knows what hitting a url in your App is meant to do.
After overriding the default index template that is associated with the application. I moved your code into the application template. The reason for that it seemed as though you wanted that bar with the 'newNote' action to be present all the time. If you want something present all the time in your App, like a navigation bar, footer, im sure you can think of better stuff, place it in the application template.
Here is a JSBIN and I adjusted you code a little
http://jsbin.com/oWeLuvo/8
I hope this helps Happy Coding.