Dynamically instantiating a component in Vue.js - vue.js

Following this tutorial, I'm trying to programmatically create instances of a component on my page.
The main snippet is this:
import Button from 'Button.vue'
import Vue from 'vue'
var ComponentClass = Vue.extend(Button)
var instance = new ComponentClass()
instance.$mount()
this.$refs.container.appendChild(instance.$el)
However I get two errors:
The component I'm trying to instantiate contains references to the store, and these don't work: "TypeError: Cannot read property 'state' of undefined".
For the last line of the snippet (this.$refs.container.appendChild(instance.$el)) I get this error: "Uncaught TypeError: Cannot read property 'container' of undefined"
I'm really not sure how to troubleshoot this, if anyone strong in Vue.js could give me some hint as to why I'm getting these errors and to solve them that would be terrific.

1) Since you're manually instantiating that component and it doesn't belong to your main app's component tree, the store won't be automatically injected into it from your root component. You'll have to manually provide the store to the constructor when you instantiate the component ..
import ProjectRow from "./ProjectRow.vue";
import Vue from "vue";
import store from "../store";
let ProjectRowClass = Vue.extend(ProjectRow);
let ProjectRowInstance = new ProjectRowClass({ store });
2) In a Vue Single File Component (SFC), outside of the default export this doesn't refer to the Vue instance, so you don't have access to $refs or any other Vue instance property/method. To gain access to the Vue instance you'll need to move this line this.$refs.container.appendChild(instance.$el) somewhere inside the default export, for example in the mounted hook or inside one of your methods.
See this CodeSandbox for an example of how you may go about this.

This is another way to instantiate a component in Vue.js, you can use two different root elements.
// Instantiate you main app
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
//
// Then instantiate your component dynamically
//
// Create a component or import it.
const Hello = {
props: ['text'],
template: '<div class="hello">{{ text }}</div>',
};
// Create a componentClass by Vue.
const HelloCtor = Vue.extend(Hello);
// Use componentClass to instantiate your component.
const vm = new HelloCtor({
propsData: {
text: 'HI :)'
}
})
// then mount it to an element.
.$mount('#mount');

It works by assigning "this" to the property "parent". By setting the parent you also have access to the $store in the new instance. (Provided that "this" is another Vue instance/Component and already has access to the store, of course)
new (Vue.extend(YourNewComponent))({
parent: this,
propsData: {
whatever: 'some value',
},
}).$mount(el.querySelector('.some-id'))
If you don't need the reference to the parent, you can just leave "parent: this," out.
Important note: When mounting many (like 500+) items on the page this way you will get a huge performance hit. It is better to only give the new Component the necessary stuff via props instead of giving it the entire "this" object.

I went down this path, following all the examples above, and even this one: https://css-tricks.com/creating-vue-js-component-instances-programmatically/
While I got far, and it works (I made a lot of components this way), at least for my case, it came with drawbacks. For example I'm using Vuetify at the same time, and the dynamically added components didn't belong to the outer form, which meant that while local (per component) validation worked, the form didn't receive the overall status. Another thing that did not work was to disable the form. With more work, passing the form as parent property, some of that got working, but what about removing components. That didn't go well. While they were invisible, they were not really removed (memory leak).
So I changed to use render functions. It is actually much easier, well documented (both Vue 2 and Vue 3), and everything just works. I also had good help from this project: https://koumoul-dev.github.io/vuetify-jsonschema-form/latest/
Basically, to add a function dynamically, just implement the render() function instead of using a template. Works a bit like React. You can implement any logic in here to choose the tag, the options, everything. Just return that, and Vue will build the shadow-DOM and keep the real DOM up to date.
The methods in here seems to manipulate the DOM directly, which I'm glad I no longer have to do.

Related

Vue3 equivalent to create component instance via new Vue()

I have a data-modelling library that uses underlying Vue2 component instances, eg:
const Person = Vue.extend({
name: 'Person',
//- ... data, computed, watch, methods, events, etc...
})
const person = new Person({ ... })
I can't find docs to determine if there is an equivalent method of creating a new component instance detached from the DOM and OUTSIDE of the main App's context.
Certainly createApp() isn't what I'm looking for - from the Vue3 API Docs
I guess defineComponent() seems to be the way to define the component (d'uh!) ... but since there is no constructor, there seems to be no way to instantiate it outside of the Apps context (ie mounting it)
Is there such a thing? Have I perhaps been versioned-out of my sweet sweet data modelling library?!? Tell me it's not so!

How does the next() guard function in vue-router work?

I'm trying to understand the solution in this SO post. The solution allows the user to keep track of the previous route in the current route.
Below is the snippet of Vue code that I'm trying to understand. If I understand correctly, next accepts a callback function that receives the current component's vue instance. We then set the prevRoute data property of this vue instance to from. Is this interpretation correct? If not, what is actually happening?
If someone could also add a brief explanation as to what the Vue API is doing behind the scenes that would also be very helpful for me to actually understand the snippet of code.
...
data() {
return {
...
prevRoute: null
}
},
beforeRouteEnter(to, from, next) {
next(vm => {
vm.prevRoute = from
})
},
...
As per the documentation...
The beforeRouteEnter guard does NOT have access to this, because the guard is called before the navigation is confirmed, thus the new entering component has not even been created yet.
However, you can access the instance by passing a callback to next. The callback will be called when the navigation is confirmed, and the component instance will be passed to the callback as the argument
So vm is the component instance assigned to the destination route.
From your question...
We then set the prevRoute data property of this vue instance to from. Is this interpretation correct?
Almost. All you're doing is setting a direct object property on the Vue component which is after all, just a JavaScript object at heart. For example
const vm = { name: 'I am totally a Vue component' }
vm.prevRoute = from
This property will not be reactive but you can certainly access it within your component via this, just as you can other non-data properties like $el, $refs, etc.

Vue component state freezing when moved between router-views

I have a custom map component, which wraps a Openlayers 4 instance. This component which I am forced to use, is used multiple places across my SPA. The initialization process is quite long, so I would like to keep one instance of the map available, and move it between views when I need to. Problem is that the state doesen´t update within the component when it has moved.
I´ve boiled the problem down to this fiddle: https://jsfiddle.net/j16d4yto/
When moved on the same router-view the state updates fine (click the ‘Change text’ button). But when the router-view changes, and the component is moved with appendChild to the new div, the state freezes, and you can´t update the text variable anymore.
This is how I move the component from one element to another:
this.$root.$on('showMoveableComponent', function(element) {
element.appendChild(thisElement);
this.text = 'Changed text2';
});
I bet I am doing something wrong here, and probably also approaching this problem in the wrong way?
Thanks!
It's not working because of when router-view changed your MoveableComponent has been destroyed only its DOM element still referenced by you. You can test by print something in destroyed lifecycle callback function.
So this mean you can solve this by using built-in keep-alive component:
<keep-alive>
<router-view></router-view>
</keep-alive>
Example
The keep-alive component will cache everything which may not good in some other cases.
In my opinion the better way to solve this is create another Vue instance and move it.
const MoveableComponent = new Vue({
el: '#some-id',
template: `...`,
data: { ... },
methods: {
changeText() {
...
},
moveTo(element) {
element.appendChild(this.$el)
}
}
})
Example

Vue multiples components

I'm doing a project with ElementUI Tabs (just HTML and JS files, no .vue files) and I want to open a new Tab, and add html inside, like I've always used to do in Jquery and SemanticUI, for example, the user clicks the menu called "Person" and the Person View (a Vue component) opens in the tab (id = "tab1") to add a new person register, and if the user clicks again the "Person" menu, another tab opens (id = "tab2") with the Person View.
First Question: Because the Vue Component has no "el:" selector, how can I tell to component (Person View) to open inside the "tab1", and another click to open inside the "tab2" ? There is any selector like "el" in Vue.component()?
Second Question: Using Vue instance ( new Vue ({options}) ), it works, because is possible to use the selector "el", but I've read before in some blogs, that is not good practice, because the app must have only one instance of Vue. Is correct add more than one Vue instance ( new Vue () ) as used to be done adding many Vue.component ({}) in the project?
Third Question: I've read before that Vue.component() is a Vue instance, and so would be correct to say that Vue.component() and Vue() is the same thing, but with different sintax ?
Question 1:
Actually, a component does have an el. Your template determines what el is.
For example, I created an inline template for my select2 that look like this:
<select2>
<select></select>
</select2>
Vue.componet("select2", {blah blah blah});
in this case el is the select box directly.
If I did:
<select2>
<div>
<select></select>
</div>
</select2>
the component el would be the div.
Question 2: what you heard from those blogs is nonsense, at least as far as Vue 2 is concerned (never worked with ver 1)
You, as a coder, determine what el is in your code so it is safe to use as a selector. I do it all of the time.
Vues cannot overlap but you can have as many on a page as makes sense. On one set of my tabs, each tab is completely different from each other and independent of each other so each has its own Vue instance. On another, each tab is the same so a made a single component and generated it inside each tab as part of the parent Vue instance.
question 3:
Think of Components as parts and the Vue instance as the whole containing the parts. I personally use components to reduce and compartmentalize code. For example, I have a DataTables component, a select2 component and a tab component, in all cases I have a number of each on each page. Then all I need to do is include them in my Vue instance definition.
After almost two weeks trying, I got it !
First i created an object that has a component structure in a JS file
(personview.js) that i load with requireJS, and pass as a parameter to a
method of Vue Instance called appVue:
appVue.addComponent(componentName,{name:"personview",template:"<div>html tags...</div>",methods:...});
In appVue i added the method:
var appVue=new Vue({
el:'#app',
data() {
return {
components: {},
instances: {}
}
},
methods: {
addComponent(componentName,componentBody){
this.$data.components[componentName]=Vue.component(componentName,Vue.extend(componentBody));
}
}
}
When the user clicks on menu, the method openViewByClickOnMenu is called
and executes:
methods: {
openViewByClickOnMenu(){
//id to identify the components and scripts to load
var componentName="personView"; //for this example i forced the name
//call a method that adds the new tab, and inside the tab adds
//<div id="divX"></div> and return the counter ever increased.
//X in id attribute is the number genereate by the counter
var ctTab=body.addTab({label:menuItem.label});
// will be used to identify an instance of compoment
var componentId=componentName+ctTab; //will be personView1, personView2, etc..
// will be used to identify the div where i want to show component
var divTabId="div"+ctTab;
//load the personview.js with component body
requirejs([componentName],function(){
//creates a new instance of component
app.$data.instances[componentId]=new app.$data.componentes[componentName];
//mounts the component in the div that i want
app.$data.instances[componentId].$mount("#"+divTabId);
});
}
I think the Vue team could add a method in Vue instance to add
components dinamically more easily, sometimes there's no need to
load all html and js files because the user has no acess/permissions
to see some views. And i miss a way to load html native, like
Angular does, because sometimes we need generate html from template engine
inside a SpringBoot for example.

Passing in two-way binding prop to a slot

I'm somewhat new to Vue, and I'm having particular difficulty in passing in formData to my individual child nodes. Ideally each child node simply updates the parent formData object allowing me to submit the form data later on as a whole.
I've setup a JSFiddle to illustrate: https://jsfiddle.net/3nm1mrLo/
My current thinking is that I should v-bind:field="formData.name" but that throws an error. It seems as though formData doesn't exist in the slot based HTML.
Any pointers would be gratefully received. Thanks!
As you rightly said, you need to use v-bind:field="formData.name" or :field="formData.name".
It is not working because you have defined the main app template directly in your html, and used "content distribution" to include <example-form> and <example-input>.
As defined in the docs, this content-distribution (or "transclusion" if you are familiar with Angular 1.x) works fine, but the scope belongs to the main app (the root instance, because the template belongs to it).
Ref: https://v2.vuejs.org/v2/guide/components.html#Compilation-Scope
Quote:
A simple rule of thumb for component scope is:
Everything in the parent template is compiled in parent scope; everything in the child template is compiled in child scope.
If you are curious, try changing your main app (root instance) as follows:
new Vue({
el: '*[my-app]',
data: function() {
return {
formData: { name: 'abc', location: 'xyz'}
};
}
});
Now you will see that formData is not undefined anymore.
But a more proper method is to include <example-input> as part of the template of example-form component as follows:
Vue.component('example-form', {
template: `
<div class="my-example-form">
<form><pre>{{formData}}</pre><slot></slot></form>
<example-input :field="formData.name"></example-input>
<example-input :field="formData.location"></example-input>
</div>
`,
data: function() {
return {
formData: { name: '', location: ''}
};
}
});
Now it will bind to the right formData as you would expect.
But this will still not work for you because props is a one-way binding only. The child component (example-input) will get the value, but will not pass the data changes back to parent component (example-form)
For child to pass data back to parent, the right way is to use $emit as explained in this question: Updating parent data via child component?
If you want to have an <example-input> component to work as form elements, here is a related answer which works like what you expect: https://stackoverflow.com/a/40337942/654825 - there is a working jsFiddle also.