vuejs / Discussion

Vue.js discussion
167 stars 17 forks source link

Connect nested components #179

Open michielvandergeest opened 9 years ago

michielvandergeest commented 9 years ago

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

var Parent = Vue.extend({
    template: '<div class="parent"><ul><li v-repeat="child: children"><li>{{child.name}}</li></ul><content></content></div>'
});

var Child = Vue.extend({
    template: '<div class="child"><h3>{{name}}</h3><content></content></div>'
});

Vue.component('my-parent', Parent);
Vue.component('my-child', Child)

Then consider the following html:

<my-parent>
    <my-child name="Child 1">
        This is the first child
    </my-child>
    <my-child name="Child 2">
        This is the second child
    </my-child>
    <my-child name="Child 3">
        This is the third child
    </my-child>
</my-parent>

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?

yyx990803 commented 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?

michielvandergeest commented 9 years ago

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.

yyx990803 commented 9 years ago

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.

michielvandergeest commented 9 years ago

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.

yyx990803 commented 9 years ago

Great! Yes please keep me updated :)

yyx990803 commented 9 years ago

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.

michielvandergeest commented 9 years ago

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:

I should have a working example finished tomorrow morning that illustrates my use case better.

michielvandergeest commented 9 years ago

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?

yyx990803 commented 9 years ago

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.

michielvandergeest commented 9 years ago

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?

yyx990803 commented 9 years ago

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.

michielvandergeest commented 9 years ago

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

thelinuxlich commented 9 years ago

Very interesting!

yyx990803 commented 9 years ago

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.

yyx990803 commented 9 years ago

@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.

TerenceZ commented 9 years ago

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 $parents of the last and the repeat components without template are Tabs, others are VueComponent.

yyx990803 commented 9 years ago

@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.

PaulKruijt commented 9 years ago

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...

michielvandergeest commented 9 years ago

Try parent.$$.name instead. I think this has recently changed from a single $ to a $$.

PaulKruijt commented 9 years ago

No, this doesn't work either. Isn't $$ for v-el?

michielvandergeest commented 9 years ago

@PaulKruijt: Yes. You're correct. $$ is for v-el, my bad.

PaulKruijt commented 9 years ago

Geen probleem ;-) I will just wait for "the master" to reply.

yyx990803 commented 9 years ago

@PaulKruijt are you trying v-ref on transcluded components?

PaulKruijt commented 9 years ago

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
});
azamat-sharapov commented 9 years ago

I thought v-ref should only be placed in component tags, not in it's template:

<ui-submit v-ref="submit"></ui-submit>

no?

PaulKruijt commented 9 years ago

That works but then the ui-submit is in the scope of the APP. I want it to be in the ui-form scope.

themsaid commented 9 years ago

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.

mikerogne commented 8 years ago

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.

dbpolito commented 8 years ago

I would like to know a way for doing this too...

pocketmax commented 8 years ago

same here

Akryum commented 8 years ago

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>
tiefenb commented 8 years ago

+1 for that

tochoromero commented 7 years ago

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.

LinusBorg commented 7 years ago

You should check out the new provide / inject options that Vue 2.2 introduced.

tochoromero commented 7 years ago

That looks very promising. Thank you!