'this' context in functional component's child event handler

Viewed 524

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.

1 Answers

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