vuejs / Discussion

Vue.js discussion
167 stars 17 forks source link

Instantiate components dynamically #254

Closed yutto closed 9 years ago

yutto commented 9 years ago

How can I create instances of existing components on the fly and add them to DOM? In my case I want to use template string with all the needed data as props generated on the server side (inline template). Seems like a basic task, but I can't seem to get it to work.

Background: I have an application that makes use of Vue for one of its features, so I have to connect it with existing back end.

Existing components are rendered from back end using inline-template to keep them in one place, example Template:

<custom-element id="content[dynamic id]" element-type="[dynamic type]" get-action="[dynamic api url to ajax contents]" ... inline-template>
    <div>
        ... some markup, v- bindings etc
    </div>
</custom-element>

So the component is self contained and receives everything it needs as props in the inline template, actual content is then loaded on created event using passed API url, same with saving etc.

It works fine with existing elements, they are loaded from server and (after some time spent) play with Vue nicely, but when I try to use the same Template to create a new component dynamically, it does not work.

.success(function (html) {
                  // html is the same template for a new element received by executing a create request via api method
                  // what can I do here to create the component and add it to dom
                  })

I've tried few tricks incl. using '$addChild' (the only method that does not belong to an existing instance) and then $after to insert it into DOM ("TypeError: Cannot read property '__v_trans' of null" error), and plain append with jQuery like that $("#some-id").append(html) (element is appended, but ignored by Vue).

Seems I'm just missing something or doing it wrong, help appreciated!

Here is the application structure in case it makes a difference:

var Vue = require('vue');
...

    window.vm = new Vue({
        el: '#parent-id',
    data: {...},
    methods: {...creating an element goes here...},        
    components: {
        'custom-element': require('path/to/element')
    }
});

And the required element is just a module.exports with Vue options object.

PavelPolyakov commented 9 years ago

Hi @yutto ,

I think I had a similar case, you can check my findings here: https://github.com/vuejs/Discussion/issues/247

Regards,

yutto commented 9 years ago

Hi @PavelPolyakov thank you for the reply!

I thought about $mount() as well, my concern is if it considers inline templates since I can't just move template from server side to component property.

Will give it a shot and post the result.

bcat-eu commented 9 years ago

So it does not work with $mount() (undefined after I instantiate the component from $options similar to https://github.com/yyx990803/vue/issues/348 ).

In this case I tried to look into framework source to determine what it does when it encounters an element and reproduced it this way:

var $element = $(this.$el).append(html);
this._compile($element.get(0));

_compile compiles the markup, triggers created event, adds element to $ (using v-ref) and $children arrays. Since _compile is marked as private I then changed it to $compile and it seems to work just the same.

var $element = $(this.$el).append(html);
this.$compile($element.get(0));

@yyx990803 please let me know if it's the right way to achieve it (add a component with inline template to DOM dynamically).

Also it might seem confusing that $compile is listed as an instance method, since in this case it does global job, might make sense to move it to global API (or have a reference in both)?

yutto commented 9 years ago

@bcat-eu +1 that works

@yyx990803 please take a look at the solution!

yyx990803 commented 9 years ago

Dynamically compiling additional HTML is not really good practice, since this opens up attack surface for XSS, that's why there's no obvious way to do it. $compile is the lower level api that allows you to achieve it, but use it carefully.

Note $compile returns a cleanup function which you should call when your instance is destroyed:

this.cleanup = this.$compile($element.get(0))

// in beforeDestroy hook
this.cleanup()
yutto commented 9 years ago

@yyx990803 thanks for the answer! Where can I find cleanup / destroy methods for the components created on first compile? Something like vm.$children[0].$destroy(); (vm is the parent Vue instance) does not seem to work.

I have to be able to properly remove the elements, no matter if they were created on the first run or dynamically.

The use case is completely straightforward:

  1. I have some components, initial state is loaded from server, the logic is contained in markup (props, inline templates).
  2. I change the application state by adding a new component (it will first be added to DB and then returned as proper markup), if I refresh the page it will be rendered correctly, but I want to add it to DOM without refreshing (obviously).
  3. I add the same markup to the DOM and have to "make it work" like all the others ($ompile seems to solve that).
  4. I remove element and destroy the related markup, there should be no difference if this element was created on page load or later via AJAX request. I counted on $destroy here and now am a little confused.
  5. After I make 4 work I seem to have to save cleanup() methods for dynamically created element in the same place?
yutto commented 9 years ago

@yyx990803 looked closer, and made it work with remove param vm.$children[0].$destroy(true);

so the only question that remains is the 5: do I have to save cleanup() methods for dynamically created elements? Or does the $destroy call on related $clildren object do the job?

yyx990803 commented 9 years ago

It's not recommended to do this type of low-level management yourself. Think in a data-driven manner! Use v-if and v-repeat to declaratively manage your child components rather than imperatively creating and destroying them.

PavelPolyakov commented 9 years ago

@yyx990803 For some reason, for me, creating and attaching the components on the fly looks more logically as well.

Thanks for the answers!

yutto commented 9 years ago

@yyx990803 I can not use v-if since it's not about adding single element with fixed markup. I have none to "many" child elements each described as a component with its ajax methods, events etc. as well as inline template (I started with normal templates and had to switch to inline ones to handle pro instance template).

My application has state that can change so that it has to react (element added - add and compile component, element deleted - remove component and its markup). I can use v-repeat on $children array, but I'd still have to be able to remove components created by compiler on page load as well as on the fly, I'd also have to be able to add these, right?

Seems to me I have to do this kind of low-level management after all, could you please point me in the right direction on how to destroy them?

$destroy on $children object seems to do everything right aside from removing $ entry, and I can remove v-ref so that it will not be created, would that be enough? Or should I do something else?

PS just to be clear why I need inline templates etc: Each component represents a (backend) model that has type and can have different Templates depending on that type. These are managed by other developers using templating system they are familiar with and generated on the server side, inline templates seem to work fine in this regard, I thought my kind of case was the reason to introduce them.

bcat-eu commented 9 years ago

@yutto @PavelPolyakov +1 I'd like to manage components on the fly as well

yyx990803 commented 9 years ago

I still don't get your use case. But just a note you can use v-repeat like this:

data: {
  elements: [
    { type: 'component-a' },
    { type: 'component-b' }
  ]
}
<component v-repeat="elements" is="{{type}}"></component>

Let the data drive the UI of your application. You will find it much easier to maintain and reason about.

yyx990803 commented 9 years ago

And, you can instantiate components on the fly if you want to - define a component beforehand and use $addChild. Compiling html on the fly, however, is bad practice in general.

yutto commented 9 years ago

At the moment when I add an extra Type in the Database I'd have to create an extra template, the rest will be handled automatically. Also if I do not create that new Template the system will use default one that says "please create a new template named [name] under some/path/[name]".

Each template looks like that (see dynamic stuff generated on the server side):

<custom-element id="content[dynamic id]" element-type="[dynamic type]" get-action="[dynamic api url to ajax contents]" ... inline-template>
    <div>
        ... some markup, v- bindings etc - differs for each type
    </div>
</custom-element>

Now if I use multiple components as in your example, when I add a new type I'd have to also create a 'component-c' in the Vue application with all related bootstrap code (probably not DRY), also the only one who can do that would probably be me.

That is why my approach makes sense to me, hope it does to you as well now.

So what would be a proper way to manage such elements, I can switch them to v-repeat on $children array, but still have to parse new ones and delete the ones user wants to delete?

yutto commented 9 years ago

define a component beforehand and use $addChild

is there a way to do that for components that use inline templates? That is what I started with, but I didn't manage to make it work:

I've tried few tricks incl. using '$addChild' (the only method that does not belong to an existing instance) and then $after to insert it into DOM ("TypeError: Cannot read property '__v_trans' of null" error)

yutto commented 9 years ago

So here is my final solution, might make sense to isolate complicated components for applications with bigger back ends.

Create element:

// add generated component's markup to DOM with JS or jQuery and save element in the $element variable
this.$compile($element.get(0)); // compile the component, events are called, $ and $children populated
this.$emit('element-created'); // trigger whatever has to be done after adding component

Delete element:

var identifier = 'customElement' + elementId; // generate identifier used as id and v-ref
var ref = this.$[identifier]; // get element's ref assuming v-ref used in markup
 // make AJAX call to delete the element and then remove it in vue   
 ref.$destroy(true);
 delete this.$[identifier];
 // ...
 this.$emit('element-deleted');

The whole thing works fine and seems to be clean enough ($destroy() really destroys everything but related $ property).

yutto commented 9 years ago

Replacing delete this.$[identifier]; with this.$.$delete(identifier); might make it a bit cleaner ( http://vuejs.org/guide/best-practices.html#Adding_and_Deleting_Properties ).

Delete method part would then look like this

var identifier = 'customElement' + elementId; // generate identifier used as id and v-ref
var ref = this.$[identifier]; // get element's ref assuming v-ref used in markup
// make AJAX call to delete the element and then remove it in vue   
ref.$destroy(true);
this.$.$delete(identifier);
// ...
this.$emit('element-deleted');
yutto commented 9 years ago

@swift1 in the example above I get component's markup from server and use it as an inline template

Element's data would not be an issue, it is loaded on created no matter if the element appended dynamically or loaded on the first call

anyway I think you'd have to run $compile on anything (uncompiled) that you add to DOM

PavelPolyakov commented 9 years ago

@yutto

Hi,

Have you tried your approach with the components which accept props ?

I've tried to, and met some issues.

When I try to do the:

$(this.$el).append('<div class="modalContainer" verbs="{{ verbs }}" add-verb="{{ addVerb  }}" hide-add-verb-modal="{{ hideAddVerbModal }}"></div>');
this.$compile(this.$el);

this.$addChild({
                    props: ['verbs', 'addVerb', 'hideAddVerbModal'],
                    el: '.modalContainer'
                }, addVerbModal);

I receive the next, in the compiled component:

image

As you see I receive functions like strings, and data like integer, for some reason. How to make them work by real?

The full source code is here: https://github.com/PavelPolyakov/twiliocms/blob/feature/compile/public/app/components/response.vue#L68

@yyx990803 do you think it's possible to use the props method here? Or the data could be send only through the data when we do $addChild ? If I do this: image

I meet this issue once again: https://github.com/vuejs/vueify/issues/15

Regards,

yutto commented 9 years ago

@PavelPolyakov yep all my elements use props, but only to get values from server. In my case I also have different markup for different types of the same component, so I have to use inline templates (otherwise data bindings get mixed up). You seem to have normal templates, so my case might not apply.

Looking at your functions as props I see you do it a little differently, I have complicated logic in my components so I communicate up / down / horizontally exclusively via event system. this means I do not accept any functions from parent scope. Here is a piece of pseudo code:

HTML in child template

<a v-on"click : doSomething()">

child-component.js doSomething Method:

this.$dispatch('some-event', this, someOtherParam);

parent-component.js created Event handler

this.$on('some-event', function () {
    this.reallyDoSomething(child, someOtherParam);
});

and then the logic goes in reallyDoSomething method. This way you'd be able to almost completely decouple your parents and children, you also wouldn't have to pass your methods as props.

As a side notice: if you have "static" component specific templates you don't need to do any of the $compile stuff, you might simplify it by just using the x-template approach.

PavelPolyakov commented 9 years ago

@yutto

yep all my elements use props, but only to get values from server.

so you don't have any bindings two-way between your parent and child components? besides the function, I can not establish the bindings as well, in any manner :(

this issue again: https://github.com/vuejs/vueify/issues/15

yutto commented 9 years ago

I do have two way communication, event system covers everything.

dispatch as in example goes up to root element and broadcast goes another way from parent down to children (same logic as in the example) and emit works in the current instance.

On Sun, Jul 19, 2015, 14:33 Pavel notifications@github.com wrote:

@yutto https://github.com/yutto

yep all my elements use props, but only to get values from server.

so you don't have any bindings two-way between your parent and child components? besides the function, I can not establish the bindings as well, in any manner :(

this issue again: vuejs/vueify#15 https://github.com/vuejs/vueify/issues/15

— Reply to this email directly or view it on GitHub https://github.com/vuejs/Discussion/issues/254#issuecomment-122658299.

PavelPolyakov commented 9 years ago

@yutto

Yes, you do have the communication of course :) But I meant it in the way you pass some value through the prop, then change it in the child component, and then the value is updated in the parent.

But it seems, you do them through the events, which is the different way.

Regards,

groovy9 commented 8 years ago

Just to chime in with a use case that seems to call for dynamically instantiating new components... My app has a data grid component listing rows from a database table, and multiple such grids can be open at once in a tab set. Employees, vendors, jobs, that sort of thing.

Double-clicking a row opens a record edit form in a new tab. Some of this data is nontrivial and you might have the editor open for a few minutes. Meanwhile, someone comes into your office with a question and you have to go to back to the data grid, look it up, and later return to your edit. Furthermore, you need the ability to edit multiple records of possibly multiple tables at once.

So the editor can't prevent access to the grid, such as with a modal or popup, and there can't be just one editor component built into a grid component waiting to be used.

Thus, there's a need to allow for an arbitrary number of open edit widgets, making it difficult to create them all beforehand without imposing some artificial limit, e.g. max 10 editor components.

Which I suppose I could do, but it seems considerably less elegant and more troublesome than just spawning a new one as needed, destroying it when the editor closes.

omanizer commented 8 years ago

The only logical use case for doing this that I have ever seen (coming from a couple years in the Angular world) is for modals, especially when one has modals that can open other modals. Nesting the components like this creates a problem:

`

`

Modal 2 is restricted to the instance of Modal 1 but I may want the modals to work independent of each other.

And thus we need modals to the body programmatically. This is likely one of the main motivations for Angular UI's $uibModal.open method (if using bootstrap) where a template and controller can be passed in and the resulting component is added to the end of the .

Elfayer commented 8 years ago

I still don't get your use case.

@yyx990803 I have a use case in which I have a slider that contains a complete page. This slider can open multiple pages. I don't want to load all the possible pages at first load, but load it when it is required. This way I improve first load. Here is a demo of what it looks like: DEMO

obonyojimmy commented 7 years ago

@yyx990803

There is a typo in your code snipplet

This : <component v-repeat="elements" is="{{type}}"></component> should be : <component v-repeat="elements" :is="{{type}}"></component>

I find this to be the recommended way of using dynamic components

nueverest commented 7 years ago

I found that this worked for me:

<component v-for="element in elements" :id="element.id" :is="element.type"></component>
airtonix commented 7 years ago

@obonyojimmy how would that work? v-bind:is="{{type}}" would be interpreted as object:

<component v-bind:is="{
  {
    "type": type
  }
}"></component>

It would throw invalid token error becuase your inner object is missing a key. Even if it did, then it would still cause an error because component names are strings not objects.

So the syntax provided by @yyx990803 is in fact correct, however if you want to be using the v-bind approach you'd leave out the braces:

<component v-bind:is="type"></component>

But of course, you'd need to be providing the type property on your parent component.

obonyojimmy commented 7 years ago

@airtonix the type object would be your component name that you would want to instantiate .

airtonix commented 7 years ago

@obonyojimmy No. Despite what you desire the variable type to represent is irrelevant. It won't work exactly as you've printed because you're using the v-bind (shorthand or otherwise) syntax for a property, it will be erroneous. So; Like I tried to explain above:

This is correct:

<template v-for"ComponentName in AListOfComponentNamesAsStrings">
  <component :is="ComponentName"></component>
</template>

This will throw an Error (something about an unexpected token):

<template v-for"ComponentName in AListOfComponentNamesAsStrings">
  <component :is="{{ ComponentName }}"></component>
</template>
obonyojimmy commented 7 years ago

@airtonix ah , i get what you mean . The braces are a placeholder , not real data , sorry that i might have confused you !

chrislandeza commented 7 years ago

Is there a way to dynamically compile html on Vue 2? this.$compile($element.get(0)) does not work anymore with new vue version.

Elfayer commented 7 years ago

@chrislandeza It's in the doc: https://vuejs.org/v2/api/#Vue-compile Don't hesitate to ask on Gitter for those kind of things.

chrislandeza commented 7 years ago

@Elfayer - Vue.compile doesn't work the same as this.$compile though.

ghost commented 7 years ago

Hi,

I'm asking myself the same question with Vue2.

Directive can inject Dom in a Component. But how to inject a compiled Dom in a Component, eg a Component itself?

If it's not recommended, what would be the good practice to follow?

Thanks

ghost commented 7 years ago

My point is not to use dynamic components because it anticipate the fact that, in a component, we want to inject an other component, whatever it is.

Directives allow us to insert DOM in a component, and to me there are some use cases where we want to inject components that we've made generic.

Here is an example of code I've come to use to achieve this :

In MyDirBasedOnMyComp.js :

import MyComp from './MyComp.vue'

export default function (_Vue) {
  return {
    name: 'my-dir-base-on-my-comp',
    inserted: function (el, bindings, vnode) {
      let node = document.createElement('div')
      el.appendChild(node)
      const myComp = new _Vue(MyComp).$mount(node)
      _Vue.set(myComp, 'myData', 'myValue')
    }
  }
}

In index.js that bootstraps the application

import MyDirBasedOnMyComp from './MyDirBasedOnMyComp.js'
const myDir = MyDirBasedOnMyComp(Vue)
Vue.directive(myDir.name, myDir)

That we can use as follow in any component's template

<an-other-comp v-my-dir-base-on-my-comp></an-other-comp>

Is that something you would consider a good practice? Is there a way of avoiding to pass Vue as an argument to the directive, like getting it from the parent?

Thanks