In anticipation of Routable Components coming soon, I'm attempting to use Components wherever possible in my Ember 2.0 application. I'm running into a confusing issue where I cannot access the parent component's properties from the template when provided in block form. It may be very well that this isn't possible, but wanted to be sure. Here's an example:
Template:
// templates/approvals.hbs
{{#x-secure/schedule/approvals}}
{{#x-secure/layout/full sectionTitle=sectionTitle sectionDescription=sectionDescription}}
...
{{/x-secure/layout/full}}
{{/x-secure/schedule/approvals}}
Component Template:
// templates/components/schedule/approvals.hbs
{{yield}}
Component:
// components/schedule/approvals.js
import Ember from 'ember';
export default Ember.Component.extend({
/////////////////////////////////////
// PROPERTIES
/////////////////////////////////////
sectionTitle: 'Scheduling: Approvals',
sectionDescription: 'Lots of magical , fanstastic stuff.'
});
The issue I'm having is that I'm unable to access sectionTitle and sectionDescription from the parent component (approvals) and pass it into the layout/full component. However, if I remove code from the block of the component and move it to the templates/components/schedule/approvals.hbs template, it works as expected. Is it just not possible to access a parent component's properties from the block form of a component?
Thanks!
It is not possible indeed. The component's properties are available in the component's template, but not in the template that instantiates the component.
If you need the component to make things available, it should yield them explicitly:
// templates/components/schedule/approvals.hbs
{{yield sectionTitle sectionDescription}}
And using the component:
// templates/approvals.hbs
{{#x-secure/schedule/approvals as |title description|}}
{{#x-secure/layout/full sectionTitle=title sectionDescription=description}}
...
{{/x-secure/layout/full}}
{{/x-secure/schedule/approvals}}
Notice the as |x y ...| notation to assign names to yielded values.
Anything may be yielded this way, including this (be careful with that though, that breaks encapsulation) and actions.
Related
I wanted my directive to work as v-if since in my directive I have to check access rights and destroy the element if it does not have access.
Here is my code
Vue.directive('access', {
inserted: function(el, binding, vnode){
//check access
if(hasAccess){
vnode.elm.parentElement.removeChild(vnode.elm);
}
},
});
vue file
<my-component v-access='{param: 'param'}'>
The issue is that i'm applying this directive to a component, it's removing the component but not the execution of functions called by the created/mounted hook.
In the component(my-component) there are functions in mounted/created hook. The execution of these functions are done and I don't want these functions to be executed. Is there a way to stop execution of the mounted/created events?
It is impossible to replicate the behavior of v-if in a custom directive. Directives cannot control how vnodes are rendered, they only have an effect on the DOM element it is attached to. (v-if is special, it's not actually a directive but instead generates conditional rendering code when the template is compiled.)
Though I would avoid doing any of the following suggestions if possible, I'll provide them anyway since it's close to what you want to do.
1. Extend the Vue prototype to add a global method
You definitely need to use v-if to do the conditional rendering. So all we have to do is come up with a global helper method which calculates the access permission.
Vue.prototype.$access = function (param) {
// Calculate access however you need to
// ("this" is the component instance you are calling the function on)
return ...
}
Now in your templates you can do this:
<my-component v-if="$access({ param: 'param' })">
2. Define global method in the root component
This is basically the same as #1 except instead of polluting the Vue prototype with garbage, you define the method only on the root instance:
new Vue({
el: '#app',
render: h => h(App),
methods: {
access(param) {
return ...
}
}
})
Now in your templates you can do this:
<my-component v-if="$root.access({ param: 'param' })">
Now it's clearer where the method is defined.
3. Use a global mixin
This may not be ideal, but for what it's worth you can investigate the viability of a global mixin.
4. Use a custom component
You can create a custom component (ideally functional but it needn't be) that can calculate access for specific regions in your template:
Vue.component('access', {
functional: true,
props: ['param'],
render(h, ctx) {
// Calculate access using props as input
const access = calculateAccess(ctx.props.param)
// Pass the access to the default scoped slot
return ctx.scopedSlots.default(access)
}
})
In your templates you can do this:
<access :param="param" v-slot="access">
<!-- You can use `access` anywhere in this section -->
<div>
<my-component v-if="access"></my-component>
</div>
</access>
Since <access> is a functional component, it won't actually render it's own component instance. Think of it more like a function than a component.
A bit overkill for your situation, but interesting nonetheless if you ever have a more complicated scenario.
In Angular, if we want to bind a property from outside for a custom component, we have to use "#Input" to kind of allow that property to be set from outside (i.e. from consuming component template)
Does EmberJS also have some sort of similar mechanism OR does it allow binding directly from the template (hbs) without adding/marking anything in the component JS? Is there any difference when it comes to Ember Octane V/s the earlier versions of Ember?
yes it allows binding from outside the component without adding anything to the component js
in the component hbs file
<p>{{#attribute}}</p>
from outside
<MyComponent #attribute="attributeValue"/>
also you can get the binded attribute from component js
#tracked mycomponentAttribute = this.args.attribute;
in the component hbs file
<p>{{this.mycomponentAttribute}}</p>
no you don't necessarily have to add the input tag but you have to declare the property inside the component you are trying to pass the property to.
{{#each model as |post|}}
{{blog-post title=post.title body=post.body}}
{{/each}}
the blog-post component, define a property named title and this should work.
I have a component called button-widget and it is registered globally.
Vue.component('button-widget', {
template: `<button>My Button</button>`
})
Now, how can I delete this component permanently?
I am not talking about v-if or $destroy() -- I just want to completely remove this component in such a way that it was never defined so that for example, I get this warning: Unknown custom element: <button-widget> - did you register the component correctly? For recursive components, make sure to provide the "name" option..
There is no public API to do this.
The correct solution is not to register it globally in the first place and instead just use it on a per-component basis by adding it to the components option of each component you want to use it in.
Otherwise you can unregister it like this:
delete Vue.options.components['button-widget']
I have a component who initialized like this
<custom :opts="{map: false}"></custom>
and there is HTML similar to this
<template id="custom">
<div v-if="opts.map">
I'm awesome
</div>
<button v-on:click="show"></button>
</template>
where
function show(){
this.opts = {map:true} // (1) <-- This is working and I could see hidden div
this.opts.map = true // (2) <-- For some reason not working
Vue.set(this.opts, 'map', true) // (3) <-- Still not working
}
So my question is why variant 2 doesn't work and what should I change to make my control react to value reset on a button click. Or a proper explanation why (1) is working, but (2) isn't - also will be accepted as an answer.
The real problem with the code (all 3 versions) is changing a component's property from within a component. In idiomatic Vue, only the parent should change properties. If a component needs to effect a change, it should emit an event to the parent and let the parent make the necessary changes. Otherwise, there is ambiguity in which component "owns" the property.
One Way Data Flow
All props form a one-way-down binding between the child property and the parent one: when the parent property updates, it will flow down to the child, but not the other way around.
Sending Messages to Parents with Events
Can be off base here but I believe this happens because in vue component props are not reactive, so their objects are not being observed in depth. Or rather they are "a little bit reactive", reassigning the root prop does cause the DOM update but is not expected to be done manually and you'll see a warning when doing such on development build:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "..."
And for as why props are not completely reactive in the first place: https://v2.vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow
To work around the whole issue you must pass any necessary props to the component data and if those props were passed as nested objects you might also want to completely avoid mutating them from within the component since it will propagate to the parent which, unless clearly mentioned, can be a source of bad news.
I'm facing a problem which has been mentioned some times ago but those solutions like this one:
What is the proper use of an EventEmitter?
I want to take this example on the shared link to keep it easy:
Let's start:
First, I have those routes in app.module.ts:
{path: 'dash/:project_id', component: DashProject, children: [
{path: '', component: null},
{path: 'task/form', component: TaskForm},
{path: 'task/:task_id', component: TaskView}
As you can see, DashProject is my parent and those other are children. I've also included to the template of DashProject the required
<router-outlet></router-outlet>
part to include children there.
But in this mentioned example I need to include
<child (notifyParent)="getNotification($event)"></child>
Now I made it like this in my parent template:
<child (notifyParent)="getNotification($event)"></child>
<router-outlet></router-outlet>
Problem: When I add <child (notifyParent)="getNotification($event)"></child> to my parent template the child component is already included to the parent, even it's not called by URL routing. When I remove the part, the interaction between parent-child doesn't work anymore.
If I add those to the child template I get a never ending loop and crash.
Can anyone see my problem or know what is causing this error? I saw some examples on the net, like the shared one above, and all were using a similar solution, but it won't work for me.
Thanks in advance!
Kind regards, yadbo
It looks like you may be merging two different techniques.
Use routing when you want to route to a component. For example, route to the dash project page or the task form page. Normally routed components have no selector and are not directly referenced in the HTML. Rather, they appear in the <router-outlet>.
Use nested components when you want to use a component as a child component of another component. For example, display a set of stars instead of a rating in the project page. The nested component must have a selector and that selector is used in the HTML (like your <child> example.)
When using nested components, you can use #input and #output to communicate between the parent and child.
When using routed components, you can communicate between the components by passing parameters (required, optional, or query parameters), by using a shared resolver, or by setting up properties in a service.
Thanks you #DeborahK for your hint, that was I was missing.
Now I'm using a shared service for solving this problem, I'm passing a callback to the shared service which I call from the child.
Here is an example, at least the idea, how it works:
export class SharedService {
private callback:any = null;
public constructor() {
}
public getCallback() {
return this.callback;
}
public setCallback(call) {
this.callback = call;
}
}
Parent:
this._shared.setCallback(this.test.bind(this));
Child:
this._shared.getCallback()();
And yes, it works :)