I am building a simple Ember app, but I have run into difficulty passing an action closure to a child component when that component is rendered in the {{outlet}} of a navigable container.
For context, here is a quick look at the aesthetically-astonishing app I have been building:
I have a roles/role path that displays a component (the yellow section above) with the following markup. Note that the model for this component is an instance of a Role:
// file: app/components/role.hbs
<p>{{#role.name}}</p>
<div>
{{sel-nav-tabs items=this.tabConfig}}
<div class='route-content'>{{outlet}}</div>
</div>
(Where "sel" stands for "someone else's library".)
this.tabConfig is defines in the corresponding class:
// file: app/components.role.js
import Component from '#glimmer/component';
export default class RoleComponent extends Component {
get tabConfig() {
return [
{ label: 'Users', route: 'roles.role.users' },
{ label: 'Privileges', route: 'roles.role.privileges' },
];
}
}
Into the outlet in role.hbs will be rendered the appropriate list component, either users or privileges.
The users list is rendered by the following component. Note that the model is the list of User instances associated with the Role from its parent:
// file: app/components/role/user-list.hbs
<ul>
{{#each #users as |user|}}
<li>
{{user.name}}
{{#sel-button type="toolbar" onActivate=this.removeUser}}
{{sel-icon/remove-circle}}
{{/sel-button}}
</li>
{{/each}}
</ul>
and when the button is clicked it calls an action defined in the RoleUserListComponent class:
// file: app/components/role/user-list.js
import Component from '#glimmer/component';
import { action } from "#ember/object";
export default class RoleUserListComponent extends Component {
#action removeUser(user) {
// remove the user model from the role... but which role?
}
}
The catch is that the relationship between users and roles is many-to-many, so I can't simply unset the user's owner and let Ember Data take care of things. The obvious answer seemed like passing an action closure from the role component to its child user-list component.
Except, there seems to be no way to pass the action closure through the {{outlet}}. What I was hoping for was something like:
{{outlet onActivate=(action removeUser #role)}}
which would pass the closure to any component that was rendered there. I tried instead to use {{yield user}} in the child to let the parent render the delete button and give it the appropriate action, but that also hit the outlet wall.
I also tried to use controllers, which aren't documented that well, probably since their role seems to have been evolving dramatically over Ember's maturation. But while this brief explanation does mention passing down actions, it doesn't go into details, and the few up-to-date examples I found all seem to break when an outlet joins the party.
I'm suspecting that {{outlet}} just plain isn't closure-friendly.
While defining a service would probably work, that doesn't seem to be what services are intended for, and I'd be cluttering up my global space to solve a local problem.
What is the best practice (or, really, any practice) for dealing with getting messages through outlets? I looked for ways to query the earlier parts of the path, but I didn't find any that were defined in the relevant classes.
EDIT to add more detail:
The route template for /roles/role is simply:
// file app/templates/roles/role
{{role role=#model}}
Where the Role component is in the first listing above. (I also added the role.js file contents above.) My reasoning for doing that was that by making a component I created a logical place to put the config (rather than inline helper functions) and it just gave me a sense of tidiness to have all ui elements be in components.
If a refactor can be the anchor to a good solution (essentially copying the entire Role component into the route template), however, I'll happily do it.
{{outlet}} only supports one optional string argument for a named outlet and nothing else, so you won't be able to achieve this through the use of {{outlet}}!
Related
I'm discovering EmberJS and started to migrate an existing website to this framework. I was having an issue with a Bootstrap-based dropdown. This issue actually helped me understand Ember's concepts a bit better but I still have some questions.
I used the ember-bootstrap module to generate this dropdown (among other things) and here is what the code is supposed to be:
{{#bs-dropdown as |dd|}}
{{#dd.button}}
Sort by
{{/dd.button}}
{{#dd.menu as |ddm|}}
{{#ddm.item}}{{#ddm.link-to "index"}}Price low to high{{/ddm.link-to}}{{/ddm.item}}
{{#ddm.item}}{{#ddm.link-to "index"}}Price high to low{{/ddm.link-to}}{{/ddm.item}}
{{/dd.menu}}
{{/bs-dropdown}}
Now, I want some javascript code to be executed when the user clicks on one of the items. After checking the module's documentation, I found where the menu item component was defined and edited its code as follows:
export default Component.extend({
layout,
classNameBindings: ['containerClass'],
/* ... */
actions: {
// My addition
sortByPrice(param){
alert("sorting");
},
// End of the addition
toggleDropdown() {
if (this.get('isOpen')) {
this.send('closeDropdown');
} else {
this.send('openDropdown');
}
},
},
});
Then I updated the hbs file as follows:
{{#dd.menu as |ddm|}}
{{#ddm.item action "sortByPrice" low_to_high}}
{{#ddm.link-to "index" action "sortByPrice" low_to_high}}
Prix croissant
{{/ddm.link-to}}
{{/ddm.item}}
{{/dd.menu}}
This didn't work, and that's why you I added the *action* to the link-to element as well and declared similarly the action on its component file.
import LinkComponent from '#ember/routing/link-component';
export default LinkComponent.extend({
actions: {
sortByPrice(param){
alert("sorting");
console.log("sorting");
},
},
});
As you can see, the *link-to* component extends the LinkComponent one. I eventually understood that it wasn't possible for this element to handle click events natively, as explained in this thread.
Out of frustration, I ended up with a less elegant approach that still does the trick:
{{#bs-dropdown id="sort" as |dd|}}
{{#dd.button}}
Sort by
{{/dd.button}}
{{#dd.menu as |ddm|}}
{{#ddm.item action "sortByPrice" low_to_high}}
<a
class="dropdown-item"
onclick="sortByPrice('low_to_high'); return false;"
href="#"
>
Price low to high
</a>
{{/ddm.item}}
{{/dd.menu}}
{{/bs-dropdown}}
Now here are my questions:
Why is it that defining actions on both the Component file and the hbs one didn't change the result?
Why doesn't the LinkComponent handle click events natively? I get that a link is supposed to redirect users to a new page (which is still arguable), but the DOM event is still fired, so does Ember deliberately ignore it and choose not to let developers handle it? I want to know the logic behind this.
Is there a better approach than my solution?
Thanks.
Cheers for studying EmberJS and posting a beautiful, explicit question!
Your mistakes
Never modify the code inside node_modules/ and bower_components/ folders. If you really need to monkey-patch something, you can do it in an initializer. But your use case does not require monkey patching.
You attempted to define an action in the menu item component, but you apply it in a parent template. That action has to be defined in that parent's template component/controller.
This invocation is incorrect:
{{#ddm.link-to "index" action "sortByPrice" low_to_high}}
Here are the problems:
The ddm.link-to component is supposed to create a link to another route. It does not seem to support passing an action into it.
You're just passing a bunch of positional params to the component. If ddm.link-to did support accepting an action, the correct invocation would look like this:
{{#ddm.link-to "index" argName=(action "sortByPrice" low_to_high)}}
In this case, "index" is a position param and argName is a named param.
low_to_high without quotes is a reference to a property defined on the current scope. You probably meant a string instead: "low_to_high".
Never use JS code in template directly. This you should never do in Ember:
<a onclick="sortByPrice('low_to_high'); return false;">
Instead, pass an action (defined in the local scope: in a component or controller):
<a onclick={{action 'sortByPrice' 'low_to_high'}}>
The onclick property name is optional. An action defined without a property implies onclick (you only need to provide the property name if you need to attach the action to a different event):
<a {{action 'sortByPrice' 'low_to_high'}}>
For the link to be styled properly in a browser, a href attribute is required. But you don't have to pass a value '#' to it. The hash symbol was required in old-school apps to prevent the link from overwriting the URL. Ember overrides URL overwriting for you, so you can simply pass an empty href.
Here's the final correct usage:
<a href {{action 'sortByPrice' 'low_to_high'}}>
Answers to your questions
Why is it that defining actions on both the Component file and the hbs one didn't change the result?
Because you defined them in different scopes.
If you define an action in app/components/foo-bar.js, the action must be applied in app/templates/components/foo-bar.hbs.
If you define an action in app/controllers/index.js, the action must be applied in app/templates/index.hbs.
Why doesn't the LinkComponent handle click events natively? I get that a link is supposed to redirect users to a new page (which is still arguable), but the DOM event is still fired, so does Ember deliberately ignore it and choose not to let developers handle it? I want to know the logic behind this.
In a PWA, you do not do actual page redirects. Such a redirect would reload the whole app.
Instead, the LinkComponent overrides the click and tell the Ember's routing system to perform a transition. Routes must be set up properly and the route passed to the LinkComponent must exist.
It seems that your goal is not to perform a transition but to change a variable, so the LinkComponent is not applicable here. That's unless you wire the sort order property to an URL query param, in which case you can change the sort order by making a transition to a different query param.
Is there a better approach than my solution?
See below for the simplest approach that uses ember-bootstrap's dropdown.
A working example
Controller:
export default Ember.Controller.extend({
isSortAccending: true,
actions: {
changeSortDirection (isSortAccending) {
this.set('isSortAccending', isSortAccending);
}
}
});
Template:
<p>
Current sort order:
{{if isSortAccending "ascending" "descending"}}
</p>
{{#bs-dropdown as |dd|}}
{{#dd.button}}
Sort by
{{/dd.button}}
{{#dd.menu as |ddm|}}
{{#ddm.item}}
<a href {{action "changeSortDirection" true}}>
Price high to low
</a>
{{/ddm.item}}
{{#ddm.item}}
<a href {{action "changeSortDirection" false}}>
Price high to low
</a>
{{/ddm.item}}
{{/dd.menu}}
{{/bs-dropdown}}
Here's a working demo.
I just started using Sapper (https://sapper.svelte.technology) for the first time. I really like it so far. One of the things I need it to do is show a list of the components available in my application and show information about them. Ideally have a way to change the way the component looks based on dynamic bindings on the page.
I have a few questions about using the framework.
First, I'll provide a snippet of my code, and then a screenshot:
[slug].html
-----------
<:Head>
<title>{{info.title}}</title>
</:Head>
<Layout page="{{slug}}">
<h1>{{info.title}}</h1>
<div class="content">
<TopBar :organization_name />
<br>
<h3>Attributes</h3>
{{#each Object.keys(info.attributes) as attribute}}
<p>{{info.attributes[attribute].description}} <input type="text" on:keyup="updateComponent(this.value)" value="Org Name" /></p>
{{/each}}
</div>
</Layout>
<script>
import Layout from '../_components/components/Layout.html';
import TopBar from '../../_components/header/TopBar.html';
let COMPONENTS = require('../_config/components.json');
export default {
components: {
Layout, TopBar
},
methods: {
updateComponent(value) {
this.set({organization_name: value});
}
},
data() {
return {
organization_name: 'Org Name'
}
},
preload({ params, query }) {
params['info'] = COMPONENTS.components[params.slug];
return params;
}
};
</script>
Now my questions:
I notice I can't #each through my object. I have to loop through its keys. Would be nice if I could do something like this:
{{#each info.attributes as attribute }}
{{attribute.description}}
{{/each}}
Before Sapper, I would use Angular-translate module that could do translations on strings based on a given JSON file. Does anyone know if a Sapper/Svelte equivalent exists, or is that something I might need to come up with on my own?
I'm not used to doing imports. I'm more use to dependency injection in Angular which looks a bit cleaner (no paths). Is there some way I can create a COMPONENTS constant that could be used throughout my files, or will I need to import a JSON file in every occurence that I need access to its data?
As a follow-up to #3, I wonder if there is a way to better include files instead of having to rely on using ../.. to navigate through my folder structure? If I were to change the path of one of my files, my Terminal will complain and give errors which is nice, but still, I wonder if there is a better way to import my files.
I know there has got to be a better way to implement what I implemented in my example. Basically, you see an input box beside an attribute, and if I make changes there, I am calling an updateComponent function which then does a this.set() in the current scope to override the binding. This works, but I was wondering if there was some way to avoid the function. I figured it's possible that you can bind the value of the input and have it automatically update my <TopBar> component binding... maybe?
The preload method gives me access to params. What I want to know if there is some way for me to get access to params.slug without the preload function.
What would be really cool is to have some expert rewrite what I've done in the best possible way, possibly addressing some of my questions.
Svelte will only iterate over array-like objects, because it's not possible to guarantee consistent behaviour with objects — it throws up various edge cases that are best solved at an app level. You can do this sort of thing, just using standard JavaScript idioms:
{{#each Object.values(info.attributes) as attr}}
<p>{{attr.description}} ...</p>
{{/each}}
<!-- or, if you need the key as well -->
{{#each Object.entries(info.attributes) as [key, value]}}
<p>{{attr.description}} ...</p>
{{/each}}
Not aware of a direct angular-translate equivalent, but a straightforward i18n solution is to fetch some JSON in preload:
preload({ params, query }) {
return fetch(`/i18n/${locale}.json`)
.then(r => r.json())
.then(dict => {
return { dict };
});
}
Then, you can reference things like {{dict["hello"]}} in your template. A more sophisticated solution would only load the strings necessary for the current page, and would cache everything etc, but the basic idea is the same.
I guess you could do this:
// app/client.js (assuming Sapper >= 0.7)
import COMPONENTS from './config/components.json';
window.COMPONENTS = COMPONENTS;
// app/server.js
import COMPONENTS from './config/components.json';
global.COMPONENTS = COMPONENTS;
Importing isn't that bad though! It's good for a module's dependencies to be explicit.
You can use the resolve.modules field in your webpack configs: https://webpack.js.org/configuration/resolve/#resolve-modules
This would be a good place to use two-way binding:
{{#each Object.values(info.attributes) as attr}}
<p>{{attr.description}} <input bind:value=organization_name /></p>
{{/each}}
Yep, the params object is always available in your pages (not nested components, unless you pass the prop down, but all your top-level components like routes/whatever/[slug].html) — so you can reference it in templates as {{params.slug}}, or inside lifecycle hooks and methods as this.get('params').slug, whether or not a given component uses preload.
This is very basic question but i am not finding it any where.
As per my understanding before rendering template correspondin route or component.js's beforeModel() model() etc functions gets called.
What i want to do:
I want to show image of logged in user on my sidenav. User's data is stored in local storage.
My problems here
I am hoping that setting a variable in model and returning the same will solve my problem, but my model method is not being called at all.
My Code:
Template:
{{#paper-sidenav
class="md-whiteframe-z2"
name="right"
open=leftSideBarOpen2
lockedOpen=leftSideBarLockedOpen
position="right"
onToggle=(action (mut leftSideBarOpen2))}}
{{#paper-toolbar as |toolbar|}}
{{#paper-toolbar-tools}}
<img src="http://example.com/users/{{model.username}}.jpg" />
{{/paper-toolbar-tools}}
{{/paper-toolbar}}
{{#paper-content padding=true}}
Çup?
{{/paper-content}}
{{/paper-sidenav}}
Component.js
import Ember from 'ember';
export default Ember.Component.extend({
beforeModel(){
},
model(){
let user = localStorage.get('user');
console.log(user.username);
return user;
},
actions:{
toggle(propName) {
this.toggleProperty(propName);
}
}
});
In console i am getting error "GET http://example.com/users/.jpg 404 (Not Found)", which certainly says that user.username in my template evaluates to null. i tried debugging my model method in chrome's dev tool and found that model is not getting called
Could you please let me know where i'm mistaking?
Ember component does not have model() and afterModel() hooks, what you need to do, is first access the local storage data in afterModel() hook in route and pass the resolved model to component.
Main Route
export default Ember.Route.extend({
model(){
//load data
},
afterModel(model){
//access local store and set to model
model.set('users',data);
}
});
Main route hbs
{{component componetModel=model}}
inside component
import Ember from 'ember';
export default Ember.Component.extend({
init(){
this.set('users', componetModel.users);
}
});
component hbs
{{#each user in users}}
{{user.name}}
{{/each}}
You really have to read the documentation about component. It clearly state what are the lifecycle hooks for each stage,
init
update
delete
https://guides.emberjs.com/v2.11.0/components/the-component-lifecycle/
The only thing the template have access to from route is model so that you can use model directly inside the corresponding template OR pass it to the component(s).
If you need other property for your component you can either set them inside route's setupController(controller, model) hook OR create a controller for that route.
Illustration below is a sample for one specific route,
NOTE: Dashed line represents the runtime generate controller
See image above each "layer" only have access the property OR action direct inside the upper layer (special case is route auto inject model into runtime generated controller).
You do NOT need to specifically define your own controller because if the route cannot find one it will generate one at runtime.
The component is completed isolated from literally anything (unless you inject anything into it). The only way to work with the component is to pass data and action (usually used to handle events) to it.
Summary
(1) The component can only access the data you specifically passed to it (see code below).
{{component-name
internalName=externalName
}}
(2) The template can only access the data that is available inside the controller, either auto-generated one or via ember g controller controller-name. (model property is an exception)
(3) If you want to your template to have route data, using setupController hook.
I want to insert a component into controller template without using the handlebars helper (component "component-name"... or component-name). Or through a controller in an outlet (or as long as the solution works for a component that wants to insert another component, then it's fine, I don't think outlets work in components).
In other words:
App.IndexController = Ember.Controller.extend({
actions: {
insertComponent: function() {
var component = this.container.lookup("component:my-inserted", { singleton: false });
component.set("layoutName", "components/my-inserted");
// to be like handlebars-inserted component, what do i do here?
}
}
});
You can use test with this: http://emberjs.jsbin.com/popozanare/4/edit?html,js,output
Why?
Thinking of a way of to have clean modal syntax, such as the "openModal" syntax described in the Ember Cookbook: http://guides.emberjs.com/v1.10.0/cookbook/user_interface_and_interaction/using_modal_dialogs/.
The problem is that the source context is lost, as the modal is within the ApplicationRoute. I want the same syntax when calling a modal, but keeping the hierarchy. You can keep the hierarchy using https://github.com/yapplabs/ember-modal-dialog, which requires a mapping of variables... which i don't like either (but will likely implement if I have no other choice).
TD;LR: Want to open modal within the controller/component (context) that called it without scaffolding in the controller/component that called it (mapping variables, etc).
Edit:
On second thought, using a container view might be cleaner than mapping variables, found in this solution: http://jsbin.com/hahohi/1/edit?html,js,output. Still needs scaffolding though. Thanks #user3568719.
That cookbook is a bit outdated, but if you are looking for a "clean" way to handling modals in your app I would suggest named outlets.
Add it to your application or auth template {{outlet "modal"}} and when you want to bring up the modal you can catch the action on the corresponding route and then render into that named outlet like so:
this.render('your-desired-modal-template', {
into: 'auth',
outlet: 'modal'
});
And when you want to dismiss it simply disconnectOutlet like so:
this.disconnectOutlet({
outlet: 'modal',
parentView: 'auth'
});
This is the way we've been going about it, I m open to suggestions/better methods.
I have an action:
{{action create target="controller"}}
which I have targeted to the bound controller (rather than the router) like this:
App.AddBoardController = Ember.Controller.extend
create: ->
App.store.createRecord App.Board, {title: #get "boardName"}
App.store.commit()
//TODO: Redirect to route
How do I redirect back to a route from the controller action?
Use transitionToRoute('route') to redirect inside an Ember controller action:
App.AddBoardController = Ember.Controller.extend({
create: function(){
...
//TODO: Redirect to route
this.transitionToRoute('route_name');
}
...
In fact, this is not Ember idiomatic. From what I know, and what I have learnt from Tom Dale himself, here are some remarks about that code:
First, you should not transitionTo from elsewhere than inside the router: by doing so, you are exposing yourself to serious issues as you don't know in which state is the router, so to keep stuff running, you will quickly have to degrade your design, and by the way the overall quality of you code, and finally the stability of your app,
Second, the action content you are showing should be located inside the router to avoid undesired context execution. The router is indeed a way to enforce a coherent behavior for the whole app, with actions being processed only in certain states. While you are putting the actions implementation into Controllers, those actions can be called at anytime, any including wrong...
Finally, Ember's controllers are not aimed to contain behavior as they rather are value-added wrappers, holding mainly computed properties. If you nevertheless want to factorize primitives, maybe the model can be a good place, or a third party context, but certainly not the Controller.
You should definitely put the action inside the router, and transitionTo accordingly.
Hope this will help.
UPDATE
First example (close to your sample)
In the appropriated route:
saveAndReturnSomewhere: function (router, event) {
var store = router.get('store'),
boardName = event.context; // you pass the (data|data container) here. In the view: {{action saveAndReturnSomewhere context="..."}}
store.createRecord(App.Board, {
title: boardName
});
store.commit();
router.transitionTo('somewhere');
}
Refactored example
I would recommend having the following routes:
show: displays an existing item,
edit: proposes to input item's fields
Into the enclosing route, following event handlers:
createItem: create a new record and transitionTo edit route, e.g
editItem: transitionTo edit route
Into the edit route, following event handlers:
saveItem: which will commit store and transitionTo show route, e.g
EDIT: Keep reading, Mike's answer discusses some of the problems with this approach.
You can just call transitionTo directly on the router. If you are using defaults this looks like App.router.transitionTo('route', context).