Closed yutto closed 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,
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.
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)?
@bcat-eu +1 that works
@yyx990803 please take a look at the solution!
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()
@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:
@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?
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.
@yyx990803 For some reason, for me, creating and attaching the components on the fly looks more logically as well.
Thanks for the answers!
@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.
@yutto @PavelPolyakov +1 I'd like to manage components on the fly as well
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.
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.
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?
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)
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).
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');
@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
@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:
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:
I meet this issue once again: https://github.com/vuejs/vueify/issues/15
Regards,
@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.
@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
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.
@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,
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.
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
.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
@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
I found that this worked for me:
<component v-for="element in elements" :id="element.id" :is="element.type"></component>
@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.
@airtonix the type
object would be your component name that you would want to instantiate .
@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:
v-bind
syntax, or v-bind
syntax and keep the braces.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>
@airtonix ah , i get what you mean . The braces are a placeholder , not real data , sorry that i might have confused you !
Is there a way to dynamically compile html on Vue 2? this.$compile($element.get(0)) does not work anymore with new vue version.
@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.
@Elfayer - Vue.compile doesn't work the same as this.$compile though.
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
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
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: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.
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:
And the required element is just a
module.exports
with Vue options object.