I am trying to create custom event handlers for child components/elements of the functional component. The problem is that when using a render() function to create the child components, I cannot access their this context.
Suppose we have the following functional component:
const Aggregate = {
functional: true,
props: {
value: Object // to work with v-model
},
render: function(createElement, context){
const template = []
const inputHandler = function(value, prop){
const data = Object.assign({}, context.props.value, { [prop]: value })
console.log(context.props.value)
console.log(data)
this.$emit('input', data)
}
for (const prop of Object.keys(context.props.value)){
const child = createElement('input', {
props: {
value: context.props[prop]
},
on: {
input: function(event){
// 'this' is not binded here - it is undefined,
// hence the inputHandler() function is
// rising an error
inputHandler.apply(this, [event.target.value, prop])
}
}
})
template.push(child)
}
return template
}
}
Is it possible to access this context for a vnode, when creating event handler this way?
P.S. Use case info: I want to implement a component that automatically generates <input> elements for a resource and uses two-way binding through v-model directive. I also want to use it in <table> where wrapping in <td> will be required, thus I made the component functional.
Functional components don't a have a "this", because there is no Vue instance for them. This makes them lightweight.
This also means emiting events from them is kind of harder, since you need to implement Vue's logic yourself.
Lacking an instance doesn't mean you cannot events, instead, you need to manually parse context.listeners and call the event handler manually. In the case of v-model, you need to call the input listener:
const Aggregate = {
functional: true,
props: {
value: Object // to work with v-model
},
render: function(createElement, context){
const template = []
const inputHandler = function(value, prop, handler){
const data = Object.assign({}, context.props.value, { [prop]: value })
console.log(context.props.value)
console.log(data)
// Call handler directly instead of using this.$emit
handler(data)
}
for (const prop of Object.keys(context.props.value)){
console.log(context.props.value, prop)
const child = createElement('input', {
// Small bug fixes in the following section:
domProps: {
value: context.props.value[prop]
},
// End bug fixes
on: {
input: function(event){
// pass `context.listeners.input` instead of binding here
inputHandler(event.target.value, prop, context.listeners.input)
}
}
})
template.push(child)
}
return template
}
}
new Vue({
el: "#app",
components: {
Aggregate
},
data: {
test: {
key1: "val1",
key2: "val2",
}
},
})
<!-- development version, includes helpful console warnings -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<aggregate v-model="test"></aggregate>
<pre>{{ test }}</pre>
<button #click="test = {...test, ping: 'pong'}">Add key</button>
</div>
Related
I have a button component which calls an API, and I want to push the returned response up to the parent, where it will become the 'translatedText' prop, however, I believe I'm using the $emit incorrectly, due to the error: `Uncaught (in promise) TypeError: Cannot read properties of undefined (reading '$emit'). How do I best capture the response data and pass it to my parent prop, and is using $emit the best use in this instance?
TranslationButton.vue
<template>
<b-button type="is-primary" #click="loadTranslations()">Übersetzen</b-button>
</template>
<script>
export default {
name: "TranslationButton",
props: {
translatedText: ''
},
methods: {
loadTranslations() {
fetch('http://localhost:3000/ccenter/cc_apis')
.then(function(response) {
return response.text();
})
.then(function(data) {
console.log(data);
this.$emit('translatedText', this.data);
console.log(data)
})
},
},
};
</script>
Parent Component Props:
props: {
data: Array,
translatedText: '',
showAttachments: {
type: Boolean,
default: false,
}
},
How Child Component is called in Parent Component:
<translation-button #translatedText="loadTranslations()" />
Best practise when passing data from child to parent is emitting events.
this.$root.$emit('translatedText', this.data);
than
this.$root.$on('translatedText', () => { // do stuff })
by emits you pass value to parent component,
#translatedText="loadTranslations()" - its event listner, fireing on your child comp emit
do #translatedText="loadTranslations" instead of #translatedText="loadTranslations()"
and add this loadTranslations as a method to parent comp
BTW
if you dont use arrow funcs, and you use this.data it's pointing to object passed to .then, it will be undefined i guess...
The problem is with the usage of this. It does no longer point to your component inside the promise then() method.
You should create a new variable and initialize it with the value of this and use that variable to emit the event.
E.g.
loadTranslations() {
const _this = this;
fetch().then(response => _this.$emit(response));
}
if you want to pass data from child to parent, you need to use $emit like the below code
child:
<template>
<b-button type="is-primary" #click="loadTranslations">Übersetzen</b-button>
</template>
<script>
export default {
name: "TranslationButton",
props: {
TranslatedText: ''
},
methods: {
loadTranslations() {
const self= this; // change added
fetch('http://localhost:3000/ccenter/cc_apis')
.then(function(response) {
return response.text();
})
.then(function(data) {
console.log(data);
self.$emit('changeTitle', data) // change added
})
}
}
</script>
parent:
<template>
<translation-button #changeTitle="ChangeT" />
</template>
......
methods:{
ChangeT(title)
{
console.log(title)
},
}
I need global variables for errors. But I don't want set input variable for every component.
How I can watch $errors in component ABC without input variable?
(without <abc :errors="$errors"></abc>)
index.js:
Vue.prototype.$errors = {};
new Vue({
el: '#app',
render: h => h(App),
}
App.vue:
...
name: 'App',
components: {
ABC
}
...
methods:{
getContent() {
this.$errors = ...from axis...
}
Component ABC:
<template>
<div>{{ error }}</div>
</template>
...
watch: {
???
}
Here's an example of how it could be done:
const errors = Vue.observable({ errors: {} })
Object.defineProperty(Vue.prototype, '$errors', {
get () {
return errors.errors
},
set (value) {
errors.errors = value
}
})
new Vue({
el: '#app',
methods: {
newErrors () {
// Generate some random errors
const errors = {}
for (const property of ['name', 'type', 'id']) {
if (Math.random() < 0.5) {
errors[property] = 'Invalid value'
}
}
this.$errors = errors
}
}
})
new Vue({
el: '#app2',
watch: {
$errors () {
console.log('$errors has changed')
}
}
});
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<pre>{{ $errors }}</pre>
<button #click="newErrors">New errors</button>
</div>
<div id="app2">
<pre>{{ $errors }}</pre>
</div>
I've created two Vue instances to illustrate that the value really is shared. Clicking the button in the first instance will update the value of $errors and the watch is triggered in the second instance.
There are a few tricks in play here.
Firstly, reactivity can only track the reading and writing of properties of an observable object. So the first thing we do is create a suitable object:
const errors = Vue.observable({ errors: {} })
We then need to wire this up to Vue.prototype.$errors. By defining a get and set for that property we can proxy through to the underlying property within our observable object.
All of this is pretty close to how data properties work behind the scenes. For the data properties the observable object is called $data. Vue then uses defineProperty with get and set to proxy though from the Vue instance to the $data object, just like in my example.
as Estradiaz said:
You can use Vuex and access the value outside of Vue like in this answer: https://stackoverflow.com/a/47575742/10219239
This is an addition to Skirtles answer:
You can access such variables via Vue.prototype.variable.
You can set them directly, or use Vue.set, it works either way.
My code (basically the same as Skirtless):
main.js
const mobile = Vue.observable({ mobile: {} });
Object.defineProperty(Vue.prototype, '$mobile', {
get() { return mobile.mobile; },
set(value) { mobile.mobile = value; }
});
function widthChanged() {
if (window.innerWidth <= 768) {
if (!Vue.prototype.$mobile) Vue.set(Vue.prototype, '$mobile', true);
} else if (Vue.prototype.$mobile) Vue.set(Vue.prototype, '$mobile', false);
}
window.addEventListener("resize", widthChanged);
widthChanged();
Home.vue:
watch: {
'$mobile'(newValue) {
// react to Change in width
}
}
In VueJS, a child component can emit an event, for example:
this.$emit('toggle-button')
In the parent, we can listen to this event as follows:
<my-component v-on:toggle-button="doSomething"></my-component>
This works great. But I have a requirement where the event name (in this case, toggle-button) is stored in a variable or Vuex store. So, I don't have the exact event name but a variable or store with the name of the event.
In such a case what would be the syntax for referring that that event name in the on-click directive?
For example, let say we have:
let eventName = 'toggle-button'
How can I use this variable (eventName) instead of the exact event name (toggle-button) in the following:
<my-component v-on:toggle-button="doSomething"></my-component>
You could use $on(EVENT_NAME, CALLBACK) in this case:
// <my-component ref="foo" />
this.$refs.foo.$on(eventName, doSomething)
Vue.component('my-component', {
template: `<button #click="$emit('click', $event)">Click</button>`
});
new Vue({
el: '#app',
mounted() {
const eventName = 'click';
this.$refs.foo.$on(eventName, this.doSomething);
},
methods: {
doSomething() {
alert('clicked');
}
}
})
<script src="https://unpkg.com/vue#2.5.17"></script>
<div id="app">
<my-component ref="foo" />
</div>
If you render your component using separate render function, this is easy:
export default {
props: {
eventName: {
type: String,
},
},
render: function (createElement) {
return createElement('my-component', {
on: {
[this.eventName]: (event) => {
console.log('Received event!')
}
}
})
}
}
Render functions also come with ability to dynamically specify a name for the component, if that is also required for your app
In the Vue.js documentation, there is an example of a custom input component. I'm trying to figure out how I can write a unit test for a component like that. Usage of the component would look like this
<currency-input v-model="price"></currency-input>
The full implementation can be found at https://v2.vuejs.org/v2/guide/components.html#Form-Input-Components-using-Custom-Events
The documentation says
So for a component to work with v-model, it should (these can be configured in 2.2.0+):
accept a value prop
emit an input event with the new value
How do I write a unit test that ensures that I've written this component such that it will work with v-model? Ideally, I don't want to specifically test for those two conditions, I want to test the behavior that when the value changes within the component, it also changes in the model.
You can do it:
Using Vue Test Utils, and
Mounting a parent element that uses <currency-input>
Fake an input event to the inner text field of <currency-input> with a value that it transforms (13.467 is transformed by <currency-input> to 13.46)
Verify if, in the parent, the price property (bound to v-model) has changed.
Example code (using Mocha):
import { mount } from '#vue/test-utils'
import CurrencyInput from '#/components/CurrencyInput.vue'
describe('CurrencyInput.vue', () => {
it("changing the element's value, updates the v-model", () => {
var parent = mount({
data: { price: null },
template: '<div> <currency-input v-model="price"></currency-input> </div>',
components: { 'currency-input': CurrencyInput }
})
var currencyInputInnerTextField = parent.find('input');
currencyInputInnerTextField.element.value = 13.467;
currencyInputInnerTextField.trigger('input');
expect(parent.vm.price).toBe(13.46);
});
});
In-browser runnable demo using Jasmine:
var CurrencyInput = Vue.component('currency-input', {
template: '\
<span>\
$\
<input\
ref="input"\
v-bind:value="value"\
v-on:input="updateValue($event.target.value)">\
</span>\
',
props: ['value'],
methods: {
// Instead of updating the value directly, this
// method is used to format and place constraints
// on the input's value
updateValue: function(value) {
var formattedValue = value
// Remove whitespace on either side
.trim()
// Shorten to 2 decimal places
.slice(0, value.indexOf('.') === -1 ? value.length : value.indexOf('.') + 3)
// If the value was not already normalized,
// manually override it to conform
if (formattedValue !== value) {
this.$refs.input.value = formattedValue
}
// Emit the number value through the input event
this.$emit('input', Number(formattedValue))
}
}
});
// specs code ///////////////////////////////////////////////////////////
var mount = vueTestUtils.mount;
describe('CurrencyInput', () => {
it("changing the element's value, updates the v-model", () => {
var parent = mount({
data() { return { price: null } },
template: '<div> <currency-input v-model="price"></currency-input> </div>',
components: { 'currency-input': CurrencyInput }
});
var currencyInputInnerTextField = parent.find('input');
currencyInputInnerTextField.element.value = 13.467;
currencyInputInnerTextField.trigger('input');
expect(parent.vm.price).toBe(13.46);
});
});
// load jasmine htmlReporter
(function() {
var env = jasmine.getEnv()
env.addReporter(new jasmine.HtmlReporter())
env.execute()
}())
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine.css">
<script src="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine.js"></script>
<script src="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine-html.js"></script>
<script src="https://npmcdn.com/vue#2.5.15/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-template-compiler#2.5.15/browser.js"></script>
<script src="https://rawgit.com/vuejs/vue-test-utils/2b078c68293a41d68a0a98393f497d0b0031f41a/dist/vue-test-utils.iife.js"></script>
Note: The code above works fine (as you can see), but there can be improvements to tests involving v-model soon. Follow this issue for up-to-date info.
I would also mount a parent element that uses the component. Below a newer example with Jest and Vue Test Utils. Check the Vue documentation for more information.
import { mount } from "#vue/test-utils";
import Input from "Input.vue";
describe('Input.vue', () => {
test('changing the input element value updates the v-model', async () => {
const wrapper = mount({
data() {
return { name: '' };
},
template: '<Input v-model="name" />',
components: { Input },
});
const name = 'Brendan Eich';
await wrapper.find('input').setValue(name);
expect(wrapper.vm.$data.name).toBe(name);
});
test('changing the v-model updates the input element value', async () => {
const wrapper = mount({
data() {
return { name: '' };
},
template: '<Input v-model="name" />',
components: { Input },
});
const name = 'Bjarne Stroustrup';
await wrapper.setData({ name });
const inputElement = wrapper.find('input').element;
expect(inputElement.value).toBe(name);
});
});
Input.vue component:
<template>
<input :value="$attrs.value" #input="$emit('input', $event.target.value)" />
</template>
I'm using <component v-for="..."> tags in Vue.js 2.3 to dynamically render a list of components.
The template looks like this:
<some-component v-for="{name, props}, index in modules" :key="index">
<component :is="name" v-bind="props"></component>
</some-component>
The modules array is in my component data() here:
modules: [
{
name: 'some-thing',
props: {
color: '#0f0',
text: 'some text',
},
},
{
name: 'some-thing',
props: {
color: '#f3f',
text: 'some other text',
},
},
],
I'm using the v-bind={...} object syntax to dynamically bind props and this works perfectly. I also want to bind event listeners with v-on (and use .sync'd props) with this approach, but I don't know if it's possible without creating custom directives.
I tried adding to my props objects like this, but it didn't work:
props: {
color: '#f3f',
text: 'some other text',
'v-on:loaded': 'handleLoaded', // no luck
'volume.sync': 'someValue', // no luck
},
My goal is to let users re-order widgets in a sidebar with vuedraggable, and persist their layout preference to a database, but some of the widgets have #events and .synced props. Is this possible? I welcome any suggestions!
I don't know of a way you could accomplish this using a dynamic component. You could, however, do it with a render function.
Consider this data structure, which is a modification of yours.
modules: [
{
name: 'some-thing',
props: {
color: '#0f0',
text: 'some text',
},
sync:{
"volume": "volume"
},
on:{
loaded: "handleLoaded"
}
},
{
name: 'other-thing',
on:{
clicked: "onClicked"
}
},
],
Here I am defining two other properties: sync and on. The sync property is an object that contains a list of all the properties you would want to sync. For example, above the sync property for one of the components contains volume: "volume". That represents a property you would want to typically add as :volume.sync="volume". There's no way (that I know of) that you can add that to your dynamic component dynamically, but in a render function, you could break it down into it's de-sugared parts and add a property and a handler for updated:volume.
Similarly with the on property, in a render function we can add a handler for an event identified by the key that calls a method identified in the value. Here is a possible implementation for that render function.
render(h){
let components = []
let modules = Object.assign({}, this.modules)
for (let template of this.modules) {
let def = {on:{}, props:{}}
// add props
if (template.props){
def.props = template.props
}
// add sync props
if (template.sync){
for (let sync of Object.keys(template.sync)){
// sync properties are just sugar for a prop and a handler
// for `updated:prop`. So here we add the prop and the handler.
def.on[`update:${sync}`] = val => this[sync] = val
def.props[sync] = this[template.sync[sync]]
}
}
// add handers
if (template.on){
// for current purposes, the handler is a string containing the
// name of the method to call
for (let handler of Object.keys(template.on)){
def.on[handler] = this[template.on[handler]]
}
}
components.push(h(template.name, def))
}
return h('div', components)
}
Basically, the render method looks through all the properties in your template in modules to decide how to render the component. In the case of properties, it just passes them along. For sync properties it breaks it down into the property and event handler, and for on handlers it adds the appropriate event handler.
Here is an example of this working.
console.clear()
Vue.component("some-thing", {
props: ["volume","text","color"],
template: `
<div>
<span :style="{color}">{{text}}</span>
<input :value="volume" #input="$emit('update:volume', $event.target.value)" />
<button #click="$emit('loaded')">Click me</button>
</div>
`
})
Vue.component("other-thing", {
template: `
<div>
<button #click="$emit('clicked')">Click me</button>
</div>
`
})
new Vue({
el: "#app",
data: {
modules: [{
name: 'some-thing',
props: {
color: '#0f0',
text: 'some text',
},
sync: {
"volume": "volume"
},
on: {
loaded: "handleLoaded"
}
},
{
name: 'other-thing',
on: {
clicked: "onClicked"
}
},
],
volume: "stuff"
},
methods: {
handleLoaded() {
alert('loaded')
},
onClicked() {
alert("clicked")
}
},
render(h) {
let components = []
let modules = Object.assign({}, this.modules)
for (let template of this.modules) {
let def = {
on: {},
props: {}
}
// add props
if (template.props) {
def.props = template.props
}
// add sync props
if (template.sync) {
for (let sync of Object.keys(template.sync)) {
// sync properties are just sugar for a prop and a handler
// for `updated:prop`. So here we add the prop and the handler.
def.on[`update:${sync}`] = val => this[sync] = val
def.props[sync] = this[template.sync[sync]]
}
}
// add handers
if (template.on) {
// for current purposes, the handler is a string containing the
// name of the method to call
for (let handler of Object.keys(template.on)) {
def.on[handler] = this[template.on[handler]]
}
}
components.push(h(template.name, def))
}
return h('div', components)
},
})
<script src="https://unpkg.com/vue#2.2.6/dist/vue.js"></script>
<div id="app"></div>