Open michielvandergeest opened 9 years ago
It should be noted that when you nest components like this, these <my-child>
components are considered as "inserted content", and they are in fact compiled as siblings of <my-parent>
and then inserted into it. By definition, the host component is not responsible for managing the inserted content.
I'm not sure if you are particularly trying to compose your application this way, but logically you probably want my-child
to be a real child of my-parent
instead of inserted content. Ideally, you pass in the children data as a raw Array to my-parent
, then my-parent
renders my-child
using that Array. With this proper parent-child relationship you can then use the event system or v-ref
normally. Makes sense?
Hi Evan,
Yes, it makes sense. When you say to pass the children in the data array, you're suggesting to first create the child element(s) programatically into a variable and then pass it in the constructor of the parent element?
To give you some context, I'm trying to create a collection of UI elements, to build up my application, and instead of creating large components, I was hoping to be able to divide one UI element in smaller reusable elements that I could then link together. And in some cases the child elements, need to be accessed by the parent or need to pass events to the parent.
It would be nice if I could use custom tags for that, instead of passing raw elements in the data array. That would make building the interface a lot more flexible. I've built a system like this in Angular before, and was planning to release a similar UI collection for VueJS.
Okay, so as of now transcluded components (components inserted as content) are available to the host component in an array as this._transCpnts
. Theoretically you can achieve most of the things you want to do if you are careful. This is pretty low level stuff, but I think your use case probably justifies a more legit api to access/communicate with them.
Thanks! this._transCpnts
indeed provides the access to children I was looking for and I have my first basic prototype working now! I does feel a bit hacky though, and I think it would be nice to have a cleaner API for this, as you suggested.
I'll be playing around with it a bit more this week. If you like, I can keep you posted when I run into other issues or with a suggestion for a public API for these interactions based on my experience.
Great! Yes please keep me updated :)
I am thinking maybe we should add an option so that components can basically opt-in to "compile the content in its own scope". (kinda like Angular's transclude
directive option?) But I worry this will make the use of content insertion indeterministic - say, a user using a component don't know which scope the content is going to be compiled in unless he digs into the component's implementation. But maybe that is just part of the component's interface that a user needs to know (same as the list of accepted props/paramAttributes).
Alternatively we can make it an attribute flag in the template similar to inline-template
, but that would make it pretty verbose and repetitive when using nested components.
Actually in my tests so far, using this._transCpnts
solves the problems I was having. I just looks a bit ugly. Basically I add this.children = this._transCpnts;
in the ready
method of the parent, which makes all the children available and allows me to do <li v-repeat="child: children">
after.
So far this works as expected, it just feels hacky.
I think there are a few options:
children
. Possibly this could cause some naming collision, if you pass a children key to the data array.I should have a working example finished tomorrow morning that illustrates my use case better.
I've put up an example of a tabpanel component, that illustrates the concept of connecting parent and child components to create a single UI element. Please note it's a quick example, I'm planning to work on a set of more solid components in the coming period.
https://github.com/michielvandergeest/vueUI
As you can see I pull in the this._transCpnts
in the ready method of the parent, after which I'm able to easily access each individual tab in the parent tabpanel component.
I thought of another way to make this work prettier: Would it be an idea to have the child automatically call some kind of "register" method on the parent after it has been created?
The register method on the parent would be optional. If the developer needs access to children, she is free to add the method to the parent component and add whatever kind of functionality to register the child as needed.
Example:
// parent component
Vue.extend({
template: '<div></div>',
data: {
myChildren: []
},
// is called each time a child component is added to the parent
// (called by the child, passing itself as a param)
registerChild: function(child)
{
// register the child in any element of choice
this.myChildren.push(child);
// any other functionality that should be called after adding a child
// ...
},
methods: {
//
}
});
One other thing that is a bit ugly in my current implementation is the direct use of this._host
to reference the parent from the child. It would be nice if there would be some kind of this.parent()
method. Which would basically be nothing more than return this._host
.
What do you think?
Thanks! The implementation helps me a lot in understanding what the needs of such components are. The current implementation looks find to me, the only thing I want to mention is that you should not bind to component instances directly as data (as in v-repeat="tab: tabs"
). You should extract a separate pure data array (this.tabsData = this.tabs.map(tab => tab.$data)
) for data binding purposes.
Thanks for the feedback!
I'm working on some additional functionality to the component and run into an issue with that.
I'm wondering how I would be able to create / instantiate a component programatically and insert it into a specific point in the DOM (through a v-el="targetElement" reference for example).
I'm trying something like:
var child = new ChildComponent();
child.$after(this.$$.targetElement);
But this doesn't seem to work very well. The ChildComponent is created (created-callback is called), but it isn't being compiled / inserted into the DOM (the lifecycle stops before the beforeCompiled callback). Also it gives an error "Uncaught TypeError: Cannot read property '__v_trans' of null"
Being able to create, compile and insert Vue Components on the fly through JS would really be very useful in creating a flexible component structure.
Is this possible now within Vue?
You need to give it an element or call $mount: http://vuejs.org/api/
If you provided the el option at instantiation, the Vue instance will immediately enter the compilation phase. Otherwise, it will wait until vm.$mount() is called before it starts compilation.
Yeah, I had tried to pass the el option, but it gave some unexpected results. I think by now I've figured out what goes wrong. Or better, how it works different than I expected.
When you pass an element to the new component instance, it replaces all the content inside that element with the compiled template of the component (even when replace is set to false in the component, by the way). I guess this makes sense when you parse a custom tag in the DOM (like v-child).
But in my case I'd actually like to append the newly create element to the container element. I think it would be enough if we could just add a append: true
option to indicate this behaviour. Possibly it would be nice if we could somehow specify the location where to append (first, last, before / after n-th element).
Check this CodePen for an illustrative example of how it replaces instead of appending: http://codepen.io/pensbymichiel/pen/rVwzbr
Very interesting!
The replace
option only indicates whether the component should replace it's container node in the parent template.
Any HTML inside the container node is considered parent content. If you don't provide a <content></content>
outlet for them then they will be discarded. So, to achieve the append functionality you want, you need to add <content></content>
at the top of your child component's template.
In general, working with the imperative component API requires a lot of understanding of how the internal compilation works. Maybe taking a read of the source code will help.
@michielvandergeest I've pushed some changes to how transcluded components work in the latest dev
branch: https://github.com/yyx990803/vue/compare/850a7e7...dev
vm._transCpnts
and vm._host
are gone - now transcluded components are actually children of the host component. So, for a transcluded component, its $parent
will be the component into which the content is inserted to. e.g.
<tabs>
<tab></tab>
</tabs>
Here for each <tab>
component, its $parent
will be the <tabs>
component, and the <tab>
component can be found in <tabs>
's $children
array. This makes accessing parent/child between these components very straightforward.
In addition, event dispatching/broadcasting will now work properly between transcluded and host components. I believe this change largely solves the issue.
For v-repeat
with template, the $parent
is still not as expectation. For example:
<tabs>
<template v-repeat="3">
<tab></tab>
</template>
<tab id="last"></tab>
<tab v-repeat="3"></tab>
</tabs>
new Vue({
el: "body",
components: {
tabs: {
name: "tabs"
},
tab: {
name: "tab",
ready: function () {
console.log(this.$parent);
}
}
}
});
Only the $parent
s of the last
and the repeat components without template are Tabs
, others are VueComponent
.
@TerenceZ in this case the $parent
points to the repeater instance. If you want to avoid that just use <tab v-repeat="3"></tab>
instead.
You can indeed access the child component with "$children". But is it true you can not get a specific child component through "v-ref"? I add the "v-ref" attribute on the child component, but when i try to get it with "parent.$.name"...i get undefined...
Try parent.$$.name
instead. I think this has recently changed from a single $ to a $$.
No, this doesn't work either. Isn't $$ for v-el?
@PaulKruijt: Yes. You're correct. $$ is for v-el, my bad.
Geen probleem ;-) I will just wait for "the master" to reply.
@PaulKruijt are you trying v-ref
on transcluded components?
Yes, i am... This is the code, for the custom submit button (which is placed in "ui-form" component):
Vue.component('ui-submit',
{
template: '
<div class="ui button submit" v-class="type" v-on="click: click" v-ref="submit">
<i class="icon" v-class="icon" v-if="icon"></i>
<content></content>
</div>
',
replace: true
});
I thought v-ref
should only be placed in component tags, not in it's template:
<ui-submit v-ref="submit"></ui-submit>
no?
That works but then the ui-submit is in the scope of the APP. I want it to be in the ui-form scope.
I have the same need like @PaulKruijt Sometimes you want the sub components to be in the scope of the host component, for example
<vue-form>
<vue-text></vue-text>
<vue-select></vue-select>
</vue-form>
Here the <vue-text>
and <vue-select>
components are in the scope of the app not <vue-form>
while it makes more sense for these components to be private assets for the parent vue-form
component.
I suggest a tag just like inline-template
to be used to make the content render in the host component instead of the parent scope.
Sorry for bumping this, but just curious if anything was done here? I just ran into this issue myself. Maybe I'm mis-using Vue. My example is nearly identical to the OP... ran into same issue.
I would like to know a way for doing this too...
same here
Will this use case be supported by vue 1.0 and/or vue 2.0? I really would like to do this easily:
<tabs>
<tab></tab>
<tab></tab>
<tab></tab>
</tabs>
+1 for that
Having a proper way to communicate between nested Custom Components is crucial when building reusable semantic UI components, the tabs example is a good one.
You should check out the new provide
/ inject
options that Vue 2.2 introduced.
That looks very promising. Thank you!
I'm looking for a way to connect nested components together in VueJS, how to have a parent component be aware of it's children and visa versa. I'm not only looking for a way to pass attributes down from the parent to the children, but also how to pass information 'up' from the children to it's parent.
Imagine the following:
I have 2 components defined in VueJS. Parent and Child
Then consider the following html:
The idea now is that it will create a parent div, and 3 child divs. Which is does. But my issue is that the parent is not aware that it has children. Somehow I would like to 'register' each child in the parent (to make the list of children in the v-repeat for example). Or I would like to be able to call an event from the child on the parent, so the parent can administer how the other children react to an event on one of the children.
I know I can pass down parameters using v-with, but I'm stuck on the part of setting up a real communication between the child and the parent.
Any ideas on how to do something like this in VueJS?