Vue.js dynamic add and remove - javascript

I'm learning Vue.js for my game and I was wondering if there is a way to dynamically add and remove components in Vue.js ?
Here's my current code
var vue = new Vue({
el: "#fui",
template: ``
})
const HelloCtor = Vue.extend({
props: ['text'],
template: '<div class="hello">{{ text }}</div>',
});
const vm = new HelloCtor({
data: {
text: 'HI :)'
}
});
/*
How can I do something like this?
vue.add(vm);
vue.remove(vm);
*/
The code basically speaks for himself
So, is it possible (and how?) to dynamically add and remove Vue.js components to a Vue?

You need a place to put vm in the template. Then you can $mount the component manually to an element with vm.$mount('el'). You can also delete the element with vm.$destroy(true). Destroy won't delete the element from the DOM. You'll need to do that manually with something like (vm.$el).remove()
I'm not 100% this is what you're looking for, and when you find yourself manually calling $destroy() you are probably not doing things right…but it does let you take control of the creating and destruction of components.
Something like this will let you create then destroy your component (note in this case once you destroy vm it's gone):
<div id="fui">
<button #click="make">Make</button>
<button #click="bye">destroy</button>
<div id="mountme"></div>
</div>
<script>
const HelloCtor = Vue.extend({
props: ['text'],
template: '<div class="hello">This has been {{ text }}</div>',
})
const vm = new HelloCtor ({
data: {
text: "Mounted"
}
})
var vue = new Vue({
el: "#fui",
template: ``,
methods: {
make: () => {
vm.$mount('#mountme')
},
bye: () => {
vm.$destroy(true);
(vm.$el).remove();}
}
})
</script>

Related

Elegantly inject pre-rendered element into Vue template

I'm trying to render an existing HTMLElement in a Vue component, like follows:
const someElement = document.createElement('p'); // for instance
Vue.component('my-component', {
data: () => ({ someElement }),
template: '<div>{{ someElement }}</div>',
// The above doesn't work; {{ moustache }} tags turn the element into a string.
});
There are several partial solutions, but none are perfect:
I could use v-html, which will work for some elements, but will fail to preserve e.g. event listeners.
Vue.component('my-component', {
data: () => ({ someElement }),
template: '<div v-html="someElement.outerHTML"></div>',
});
I could manually inject the element on the mounted() hook, which will work, but is somewhat ugly and feels unidiomatic.
Vue.component('my-component', {
data: () => ({ someElement }),
template: '<div></div>',
mounted() {
this.$el.appendChild(someElement);
},
});
If I could somehow wrap the element in a component, I could render that wrapper component. But it's unclear how to do this. I could use a manual render() function, but render() seems to have to return a VNode instance, and I see no way to turn an HTMLElement object into a VNode instance. A way shouldn't be expected to exist, given that VNode represents a virtual DOM whereas HTMLElement is part of the actual DOM.
Vue.component('element-wrapper', {
render() {
return someElement; // can't, not a VNode
},
});
Vue.component('my-component', {
template: '<div><element-wrapper></element-wrapper></div>',
});
I could use :is, but :is expects a component options object, meaning this option has all the same difficulties as #3.
Vue.component('my-component', {
template: '<div><component :is="elementAsComponent"></component></div>',
data: () => ({
elementAsComponent: /* again need to wrap element in component */,
}),
});
Is there an elegant way to do this?
This is possible by wrapping the custom element in a very thin Vue component which just replaces its root element with the custom element on mount.
Like so (fiddle):
const myCustomElement = document.createElement('p');
myCustomElement.innerText = 'click me';
myCustomElement.addEventListener('click', () => {
myCustomElement.style.color = 'red';
})
/* Takes an HTMLElement and returns a wrapping Vue component options object */
function liftToVue(element) {
return {
template: '<div></div>',
mounted() {
this.$el.replaceWith(element);
},
// If element is or may be a Promise, you can do:
// async mounted() { this.$el.replaceWith(await element); }
};
}
Vue.component('my-component', {
data: () => ({ myCustomElementAsComponent: liftToVue(myCustomElement) }),
template: '<div><component :is="myCustomElementAsComponent" /></div>',
})
There is a high chance, however, that this is an XY problem. See if you can use Vue components in the first place instead of generating HTMLElement instances at all.

vue js - can't find child component template

After reading the vue.js docs I just jumped into components.
I want to create a custom (local) input component that emits an event to the parent on keyup, but I have two problems. (see code example at the end of the post)
[solved] 1. I already get an error when I register the child component that says
[Vue warn]: Failed to mount component: template or render function not defined.
found in
---> <InputTest>
<Root>
I guess it's a complete no-brainer, but I just don't get it.
[solved] 2. The child event doesn't even fire
Before abstracting and simplyfing the code for this question I tried to create the same behaviour with single-file (.vue) components. With SFCs the template compiles / mounts successfully, but the child component events doesn't fire. Obviously I can not tell for sure if this problem will occur in my provided example as well, but I'd guess so.
EDIT 1: Solved problem 1
My child-component should be an object instead of a vue instance. I updated the code for that. I also changed the onChange method from lambda to function, as this doesn't point to the vue instance in a lambda.
EDIT 2: Solved problem 2
There may be times when you want to listen for a native event on the root element of a component.
Apparently the native modifier can only be used on components and not on native elements. Removing the modifier fixed the problem. I changed the code accordingly.
CODE
const inputText = {
data () {
return {
model: ''
}
},
template: '<input type="text" v-model="model" #keyup="onChange">',
methods: {
onChange: function () {
this.$emit('update', this.model);
}
}
};
const app = new Vue({
el: '#app',
data () {
return {
txt: ''
}
},
methods: {
onUpdate: function(txt) {
this.txt = txt;
}
},
components: {
'input-text': inputText
}
});
<script src="https://unpkg.com/vue#2.5.13/dist/vue.js"></script>
<div id="app">
<input-text #update="onUpdate"></input-text><br>
{{ txt }}
</div>
You don't need two vue instances. You can create a component as a simple object and use it in your vue instance
const inputText = {
template: '<div> <input type="text" #keyup.native="onChange"> </div>',
methods: {
onChange: () => {
console.log('onChange');
this.$emit('update')
}
}
}
const app = new Vue({
el: '#app',
template: '<input-test #keyup.native="onKeyup" #update="onUpdate"></input-test>',
methods: {
onUpdate: () => console.log('onUpdate'),
onKeyup: () => console.log('onKeyup')
},
components: {
'input-test': inputText
}
});
<script src="https://unpkg.com/vue#2.5.13/dist/vue.js"></script>
<div id="app"></div>

Vue components / elements in v-html

I have a post.text data that contains the text of a blog post submitted by the user Much like in Twitter, users can mention other users with the sintax I am tagging #user1 in this post. When rendering the post, I want to replace all the #username instances with links to the page of the mentioned user.
With a regex match / replace I can easily transform the mentioned #username into something like (I'm using vue-router):
I am tagging <router-link :to="{name: 'user', params: {userId: post.userId}}">{{ dPost.user_name }}</router-link> in this post
But when I use it like this:
<p v-html="preparedText"></p>
vue doesn't reprocess the html to bind its own tags.
How to solve this problem? Thanks
What you want to do sortof breaks the normal Vue paradigm, but it can be done using Vue.compile. You'll need to use Vue.compile to generate the render functions and then manually create a new Vue instance
once your component has been mounted.
Here's an example:
Vue.component('post', {
template: `<div></div>`,
props: { text: String },
mounted() {
let regex = /\B\#([\w\-]+)/gim;
let username = this.text.match(regex)[0];
let linkText = this.text.replace(regex, `${username}`);
let res = Vue.compile(`<p>${linkText}</p>`);
let { render, staticRenderFns } = res;
new Vue({ el: this.$el, render, staticRenderFns })
}
})
new Vue({
el: '#app',
data() {
return { text: `Hi #user1, how are you?` }
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.4/vue.min.js"></script>
<div id="app">
<post :text="text"></post>
</div>
You don't need to use v-html.
To dynamically render your components, simply use :is="your component name".

Vue instance inside Vue instance

Sup people!
I got this HTML code here:
// index.html
<div data-init="component-one">
<...>
<div data-init="component-two">
<button #click="doSomething($event)">
</div>
</div>
This basically references a Vue instance inside another Vue instance if I understood everything correctly. The respective JS code is split up in two files and looks like this:
// componentOne.js
new Vue(
el: '[data-init="component-one"]',
data: {...},
methods: {...}
);
// componentTwo.js
new Vue(
el: '[data-init="component-two"]'
data: {...}
methods: {
doSomething: function(event) {...}
}
);
Now, the problem with this is, that doSomething from componentTwo never gets called.
But when I do some inline stuff, like {{ 3 + 3 }}, it gets computed like it should. So Vue knows there is something. And it also removes the #click element on page load.
I tried fiddling around with inline-template as well, but it doesn't really work as I'd expect it to in this situation. And I figured it isn't meant for this case anyway, so I dropped it again.
What would the correct approach be here? And how can I make this work the easiest way possible with how it's set up right now?
The Vue version we use is 2.1.8.
Cheers!
The problem is that you have two vue instances nested to each other.
If the elements are nested, then you should use the same instance or try components
https://jsfiddle.net/p16y2g16/1/
// componentTwo.js
var item = Vue.component('item',({
name:'item',
template:'<button #click="doSomething($event)">{{ message2 }</button>',
data: function(){
return{
message2: 'ddddddddddd!'
}},
methods: {
doSomething: function(event) {alert('s')}
}
}));
var app = new Vue({
el: '[data-init="component-one"]',
data: {
message: 'Hello Vue!'
}
});
<div data-init="component-one">
<button >{{ message }}</button>
<item></item>
</div>
Separate instances work if they are independant of each other.
as follows:
https://jsfiddle.net/p16y2g16/
var app = new Vue({
el: '[data-init="component-one"]',
data: {
message: 'Hello Vue!'
}
});
// componentTwo.js
var ddd = new Vue({
el: '[data-init="component-two"]',
data: {
message: 'ddddddddddd!'
},
methods: {
doSomething: function(event) {alert('s')}
}
});
But when I do some inline stuff, like {{ 3 + 3 }}, it gets computed like it should. So Vue knows there is something.
Because you have parent instance 'componentOne'. It activated Vue for this template. If you need to set another instance inside, you have to separate part of template. Example (it can lag in snippet!) .
Alternative
https://jsfiddle.net/qh8a8ebg/2/
// componentOne.js
new Vue({
el: '[data-init="component-one"]',
data: {
text: 'first'
},
methods: {}
});
// componentTwo.js
new Vue({
el: '[data-init="component-two"]',
data: {
text: 'second'
},
template: `<button #click="doSomething($event)">{{text}}</button>`,
methods: {
doSomething: function(event) {
console.log(event);
}
}
});
<script src="https://vuejs.org/js/vue.min.js"></script>
<div data-init="component-one">
{{text}}
</div>
<div data-init="component-two">
</div>
The button element inside component-two is referenced as a slot in Vue.
The evaluation of the #click directive value happens in the parent component (component-one, which host component-two). Therefor, you need to declare the click handler over there (over component-one).
If you want the handler to be handled inside component-two, you should declare a click directive for the slot element in it's (component-two) template, and pass the handler function, for instance, as a pop.
good luck.
You're doing everything right except you've nested the 2nd Vue instance inside the 1st. Just put it to the side and it will work as expected.
Vue ignores binding more than once to the same element to avoid infinite loops, which is the only reason it doesn't work nested.
Use vue-cli to create a webpack starter app. vue init app --webpack
Then, try to structure your components this way. Read more: https://v2.vuejs.org/v2/guide/components.html#What-are-Components
This is main.js
import Vue from 'vue'
import ComponentOne from './ComponentOne.vue'
import ComponentTwo from './ComponentTwo.vue'
new Vue({
el: '#app',
template: '<App/>',
components: {
ComponentOne,
ComponentTwo
}
})
This is ComponentOne.vue
<template>
<div class="user">
<div v-for="user in users">
<p>Username: {{ user.username }}</p>
</div>
</div>
</template>
<script>
export default {
data () {
return {
users: [
{username: 'Bryan'},
{username: 'Gwen'},
{username: 'Gabriel'}
]
}
}
}
</script>
This is ComponentTwo.vue
<template>
<div class="two">
Hello World
</div>
</template>
<script>
export default {
}
</script>
<div th:if="${msg.replyFloor}">
<div class="msg-lists-item-left">
<span class="msg-left-edit"
th:classappend=" ${msg.unreadCount == 0} ? 'msg-all-read' ">您在</span>
<span th:text="${msg.topic.title}"
class="msg-left-edit-res"
th:classappend=" ${msg.unreadCount == 0} ? 'msg-all-read' ">问题回答</span>
<span th:text="${msg.type.name}"
class="msg-left-edit "
th:classappend=" ${msg.unreadCount == 0} ? 'msg-all-read' ">帖子相关</span>
<span class="msg-left-edit-number" >
产生了<span th:text="${msg.unreadCount} ? : ${msg.unreadCount} + '条新' : ${msg.unreadCount} + '条' "
th:class="${msg.unreadCount} ? : 'number-inner':''">2132条</span>回复
</span>
</div>
<div class="msg-lists-item-right">
<span th:text="${msg.lastShowTime}">2017-8-10</span>
</div>
</div>

Render component template outside it with vue.js

I have a template:
var playersList = Vue.extend({
props: ['players'],
template: '#players-template'
});
Vue.component('players', playersList);
new Vue({
el: 'body',
methods: {
someMethod: function() {
//JSON data from request comes here
//Want to render template of players component with that data
}
}
});
I'm new with Vue.js and don't know how could I make it possible. How can I render the template with data from AJAX request? Someone posted a solution with v-view but documentation for it is gone at official website.
You have to specify the data in your Vue instance where the response is going to be stored
var playersList = Vue.extend({
template: '#players-template',
props: ['players']
}
});
Vue.component('players', playersList);
new Vue({
el: 'body',
data: {
players: ''
},
methods: {
someMethod: function() {
//JSON data from request comes here
//Want to render template of players component with that data
this.$set('players', data);
}
}
});
in your html
<body>
<ul>
<li v-for="player in players">
<players :players="players"></players>
</li>
</ul>
<template id="players-template">
<p>{{player.name}}</p>
</template>
</body>
Once you create the Vue app on the body element, you can use its component anywhere within that element. Since you named the component players, you'd render it like this:
<body>
<players :players="players"></players>
</body>
Then in your Vue
new Vue({
el: 'body',
data: function(){
return { players:[] }
},
methods: {
someMethod: function() {
//var result = ajax result
this.players = result;
}
}
});
Since players is a prop on your list component, you pass it in with :players="players". Then if players updates on the parent app, the list component would automatically update according to the new list.

Categories