Skip to content
This repository has been archived by the owner on May 3, 2018. It is now read-only.

Connect nested components #179

Open
michielvandergeest opened this issue Jun 6, 2015 · 35 comments
Open

Connect nested components #179

michielvandergeest opened this issue Jun 6, 2015 · 35 comments

Comments

@michielvandergeest
Copy link

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
Copy link
Member

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
Copy link
Author

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
Copy link
Member

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
Copy link
Author

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
Copy link
Member

Great! Yes please keep me updated :)

@yyx990803
Copy link
Member

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
Copy link
Author

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:

  • leave it as is, and consider it as an 'expert hack'
  • do this automatically and always map the children of a component to a variable like children. Possibly this could cause some naming collision, if you pass a children key to the data array.
  • add some kind of mapping option (like v-children-as="kids"), that when set, maps the child components to that given key (this.kids, in this case)
  • have method that the developer can call in the ready method, when needed. Something like this.getChildrenAs('kids'), to store the children in a this.kids variable

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

@michielvandergeest
Copy link
Author

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
Copy link
Member

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
Copy link
Author

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
Copy link
Member

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
Copy link
Author

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
Copy link

Very interesting!

@yyx990803
Copy link
Member

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
Copy link
Member

@michielvandergeest I've pushed some changes to how transcluded components work in the latest dev branch: yyx990803/vue@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
Copy link

TerenceZ commented Jul 7, 2015

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
Copy link
Member

@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
Copy link

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
Copy link
Author

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

@PaulKruijt
Copy link

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

@michielvandergeest
Copy link
Author

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

@PaulKruijt
Copy link

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

@yyx990803
Copy link
Member

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

@PaulKruijt
Copy link

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
Copy link

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
Copy link

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
Copy link

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
Copy link

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
Copy link

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

@pocketmax
Copy link

same here

@Akryum
Copy link
Member

Akryum commented Jul 8, 2016

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
Copy link

tiefenb commented Aug 16, 2016

+1 for that

@tochoromero
Copy link

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
Copy link
Member

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

@tochoromero
Copy link

That looks very promising. Thank you!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests