Vue dynamic component event bindings - vue.js

I have a dynamic component that is resolved and bound on runtime using the documented dynamic component syntax;
<div class="field">
<component v-if="component" :is="component" #valueUpdated="onUpdate($event)"></component>
</div>
Decided using a prop assigned to on mounting.
For whatever reason, when the child component, rendered dynamically, emits an event this.$emit('eventName', /*someData*/) the parent does not seem to be able to hear it. Is the approach used in standard components applicable to those rendered dynamically? Props seem to work, so maybe I am not doing something correctly?

yeah you can use props with dynamic components it's just you need to use lowercase hyphenated (kebab-case) names for the html attribute, i.e.
<component v-if="component" :is="component" #value-updated="onUpdate"></component>
Vue.component('foo', {
template: `
<div>
<button #click="$emit('value-updated', { bar: 'baz' })">pass data</button
</div>
`
});
new Vue({
el: '#app',
data: {
currentComponent: 'foo',
output: {},
},
methods: {
onUpdate (payload) {
this.output = payload
}
}
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<component v-if="currentComponent" :is="currentComponent" #value-updated="onUpdate"></component>
<pre>{{ output }}</pre>
</div>

Related

Passing v-model into a checkbox inside a Component in Vue 3?

I want to embed a checkbox inside a Vue3 Component and have the v-model binding passed down to the checkbox.
Inside the Component:
<!-- Tile.vue -->
<template>
<div>
<input type=checkbox v-model="$attrs">
</div>
</template>
<script>
export default {inheritAttrs: false}
</script>
Then in an outside file:
<template>
<Tile value="carrot" v-model="foods" />
<Tile value="tomatoes" v-model="foods" />
</template>
<script setup>
var foods = ref([]);
</script>
How do I achieve this?
The documentation says that v-model is just a shorthand for :modelValue and #update:modelValue but this is not universal as Vue obviously behaves differently for form elements such as smartly listening to onchange instead of oninput and modifying the property checked instead of value depending on the node.
If I use v-model on the outer component, how do I forward it to the checkbox and get the same smart behavior that Vue has?
I have found tons of controversial information. Some recommend using #input event (Vue 3 custom checkbox component with v-model and array of items). Some recommend emitting modelValue:update instead of update:modelValue (https://github.com/vuejs/core/issues/2667#issuecomment-732886315). Etc.. Following worked for me after hour of trial and error on latest Vuejs3
Child
<template>
<div class="form-check noselect">
<input class="form-check-input" type="checkbox" :id="id" :checked="modelValue" #change="$emit('update:modelValue', $event.target.checked)" />
<label class="form-check-label" :for="id"><slot /></label>
</div>
</template>
<script>
import { v4 as uuidv4 } from "uuid";
export default {
inheritAttrs: false,
emits: ["update:modelValue"],
props: {
modelValue: {
type: Boolean,
required: true,
},
},
setup() {
return {
id: uuidv4(),
};
},
};
</script>
Parent:
<Checkbox v-model="someVariable">Is true?</Checkbox>
you can verify that it works but doing this in parent:
var someVariable= ref(false);
watch(someVariable, () => {
console.log(someVariable.value);
});
p.s. The other solution above does not work for me. Author recommends using value property. But in example he passes v-model attribute. So I don't know exactly how it's supposed to work.
You can achieve the behavior by using emits to keep data in sync and behave as default v-model behavior. Checkbox component:
<template>
<div>
<input
type="checkbox"
:checked="value"
#change="$emit('input', $event.target.checked)"
/>
{{ text }}
</div>
</template>
<script>
export default {
name: "inputcheckbox",
props: ["value", "text"],
};
</script>
And in the parent component you can have as many checkboxes you want.
<template>
<div id="app">
<maincontent :showContent="showContent" />
<inputcheckbox text="one" v-model="checkedOne" />
<inputcheckbox text="two" v-model="checkedTwo" />
</div>
</template>
Here is a vue 2 example but is applicable to vue 3 as well. Hope this was helpful. Sandbox with this behavior:
https://codesandbox.io/embed/confident-buck-kith5?fontsize=14&hidenavigation=1&theme=dark

Adding a component by clicking a button

Vue.component('component-a', {
template: '<h3>Hello world!</h3>'
})
new Vue({
el: "#app",
data: {
arr: []
},
methods: {
add(){
this.arr.push('component-a');
console.dir(this.arr)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<component-a></component-a>
<hr>
<button #click="add">Add a component</button>
<ul>
<li v-for="component in arr"> {{ component }} </li>
</ul>
</div>
I want to insert a component a lot of times to the page by clicking a butoon, but instead of this only a component`s name is inserted. How to add a component itself?
In your code the double curly braces do not reference the component itself but just the string you added with this.arr.push('component-a'); hence just the string being displayed.
If you would like this string to call the actual component you could use dynamic components.
Replacing {{ component }} with <component :is="component"/> would achieve the effect I think you're looking for.
However if you're only going to be adding one type of component I would consider adding the v-for to the component tag itself like so:
<component-a v-for="component in arr/>
Use the component element to render your component dynamically.
The usage is very simple: <component :is="yourComponentName"></component>
The ":is" property is required, it takes a string (or a component definition).
Vue will then take that provided string and tries to render that component. Of course the provided component needs to be registered first.
All you have to do is to add the component tag as a child element of your list tag:
<li v-for="component in arr">
<component :is="component"></component>
</li>
Vue.component('component-a', {
template: '<h3>Hello world!</h3>'
})
new Vue({
el: "#app",
data: {
arr: []
},
methods: {
add() {
this.arr.push('component-a');
console.dir(this.arr)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<component-a></component-a>
<hr>
<button #click="add">Add a component</button>
<ul>
<li v-for="component in arr">
<component :is="component"></component>
</li>
</ul>
</div>

Nesting a slot in a slot for vue

Update: Here's a simplified version of what I'm trying to achieve here (from the threaded conversation below):
Accept Component A - Accept Component B - Accept a condition - if
condition is true : wrap Component B with Component A [and render]- else only
render component B.
I'm interested in creating a component that renders a wrapper conditionally. I figured a theoretical approach like this would probably be best**:**
<template>
<div>
<slot v-if="wrapIf" name="wrapper">
<slot name="content"></slot>
</slot>
<slot v-else name="content"></slot>
</div>
</template>
<script>
export default {
props: {
wrapIf: Boolean,
}
}
</script>
Then when we implement, it would look something like this:
...
<wrapper-if :wrap-if="!!link">
<a :href="link" slot="wrapper"><slot></slot></a>
<template slot="content">
content
</template>
</wrapper-if>
The idea being that, in this case, if there is a link, then let's wrap the content with the wrapper slot (which can be any component/element). If there isn't, then let's just render the content without the wrapped link. Pretty simple logic, but it seems that I'm misunderstanding some basic vue functionality because this particular example does not work.
What is wrong with my code or is there some kind of native api that already achieves this or perhaps a dependency that does this sort of thing already?
The output should look like this:
wrapIf === true
<a href="some.link">
content
</a>
wrapIf === false
content
Just focus on the content itself, and let the component worry about whether or not to wrap the default or named content slot.
If you need the wrapper to be dynamic, a dynamic component should solve that. I've updated my solution accordingly. So if you need the wrapper to be a label element, just set the tag property to it, and so on and so forth.
const WrapperIf = Vue.extend({
template: `
<div>
<component :is="tag" v-if="wrapIf" class="wrapper">
<slot name="content"></slot>
</component>
<slot v-else name="content"></slot>
</div>
`,
props: ['wrapIf', 'tag']
});
new Vue({
el: '#app',
data() {
return {
link: 'https://stackoverflow.com/company',
tagList: ['p', 'label'],
tag: 'p',
wrap: true
}
},
components: {
WrapperIf
}
})
.wrapper {
display: block;
padding: 10px;
}
p.wrapper {
background-color: lightgray;
}
label.wrapper {
background-color: lavender;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<wrapper-if :wrap-if="wrap" :tag="tag">
<a :href="link" slot="content">
content
</a>
</wrapper-if>
<div>
Change wrapper type:
<select v-model="tag">
<option v-for="tag in tagList">{{tag}}</option>
</select>
</div>
<button #click="wrap = !wrap">Toggle wrapper</button>
</div>

How to point to wrapping custom element with v-model?

I currently have a custom vue-form component:
In my common HTML it kind of looks something like this:
<vue-form>
<input type="text" v-model="test">
<input type="password" v-model="test2">
</vue-form>
Note that this is not a Vue template.
My v-model keeps pointing to my root component when in this case I would like my v-model to point the the actual component that is wrapping my content.
For the vue-form component I am simply using a slot like so:
<template>
<div>
<slot></slot>
</div>
</template>
Is there a way to get the v-model binding to point towards the wrapping component instead of the root element?
You can use Scoped Slots documentation.
Edit:
Here is working example:
<div id="app">
<vue-form v-bind:model="x">
<template scope="props">
<input type="text" v-model="props.model.test">
<input type="password" v-model="props.model.test2">
</template>
</vue-form>
</div>
new Vue({
el: '#app',
data: function() {
return {
x: {
test: 'John',
test2: 'Smith'
}
}
},
components: {
'vue-form': {
template: `<div><slot :model="model"></slot></div>`,
props: ['model']
}
}});
jsfiddle
Just notice that this solution requires from you to propagate property from component to slot by :model="model" part in slot declaration.

Vue.js - Render issue

I'm trying to create a component, that can show/hide on click, similar to an accordion.
I have the following error and I don't know why:
[Vue warn]: Property or method "is_open" is not defined on the
instance but referenced during render. Make sure to declare reactive
data properties in the data option. (found in root instance)
<div id="app">
<div is="m-panel" v-show="is_open"></div>
<div is="m-panel" v-show="is_open"></div>
</div>
</body>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="comp_a.js" ></script>
<!--<script src="app.js" ></script>-->
</html>
Vue.component('m-panel', {
data: function() {
return {
is_open: true
}
},
template: '<p>Lorem Ipsum</p>'
})
new Vue({
el:'#app',
})
Your code seems a little confused, your is_open is in your component but you are trying to access it in the parent. You just need to make sure this logic is contained inside your component. The easiest way is to simply place an event on the relevant element in your component template:
<template>
<div>
<!-- Toggle when button is clicked-->
<button #click="is_open=!is_open">
Open Me!
</button>
<span v-show="is_open">
I'm Open!
</span>
</div>
</template>
Here's the JSFiddle: https://jsfiddle.net/ytw22k3w/
Because u used is_open property in '#app instance' but u didnt declare in it,u decladed in 'm-panel component' which has no relation with it.Try something like this can avoid it.
new Vue({
el:'#app',
data:{
is_open:''
}
})