Search code examples
javascriptvue.jsvue-componentstateless

How to access native HTML element in template of functional component in Vue.js?


I'm trying to create a custom checkbox component with the Vue.js 2.6. I want to make it stateless, so it takes value as a prop, and creates event to the parent component on the user input, and doesn't hold any data itself.

This is my single-file component (simplified):

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  functional: true,
  props: {
    value: {
      type: Boolean,
      default: false,
    },
  },
});
</script>
<template functional lang="pug">
label
  input(type="checkbox" :checked="value" ref="input"
    //-@input="(listeners['input'] || (() => {}))($refs.input.checked)")
    @input="(listeners['input'] || (() => {}))($event.target.checked)")
</template>

Which generates this render function:

var render = function(_h, _vm) {
  var _c = _vm._c
  return _c("label", [
    _c("input", {
      ref: "input",
      attrs: { type: "checkbox" },
      domProps: { checked: _vm.value },
      on: {
        input: (_vm.listeners["input"] || function() {})(
          //_vm.$refs.input.checked
          _vm.$event.target.checked // Got error on this line
        )
      }
    })
  ])
}
var staticRenderFns = []
render._withStripped = true

Parent component:

<template lang="pug">
form
  checkbox(:value="value" @input="value = $event")
  //-checkbox(v-model="value") - also should work
</template>
<script lang="ts">
import Vue from 'vue';
import { default as Checkbox } from './checkbox.vue';

export default Vue.extend({
  components: {
    checkbox: Checkbox,
  },
  data() {
    return { value: false };
  },
});
</script>

When native input element creates an input event, I want to pass the checked property of the input to the parent component listener (if it is exists).

I have tried to use $event.target.checked and $refs.input.checked in the event handler to get the checked property, but realized that I haven't direct access to them in a functional template:

TypeError: "_vm.$refs is undefined"

So, is there a way to access a native HTML element, that creates an event, in attached to it event handler, in a functional component template? I have searched documentation for this topic, but didn't find much.

Or should I just use a stateful component in this case?


Solution

  • You can, but not in an expected way. The thing is with functional components that they pass their refs up the tree. So your ref 'input' will appear in the component that includes your functional checkbox. If you try to attach a ref using template on your functional component, it will simply fail.

    So the following wouldn't work (in your parent component), ref 'check' will not be there. But ref 'input' will be, at least the last one, if you put multiple checkboxes.

    <form>
        <functional-checkbox ref="check" :value="myValue" @input="myValue = $event"/>
    </form>
    

    But the following will

    <form>
        <functional-checkbox :value="myValue" @input="myValue = $event"/>
    </form>
    
    // later in code (assuming functional-checkbox has ref="input" defined inside)
    this.$refs.input // will have a value, when the component is _mounted_
    

    Regarding your problem with a functional checkbox component - there is a much simpler fix. You just need to define your handler as a function call as it is understood by Vue template compiler. It's not perfect, which is why despite you having created a function call, it is not being interpreted as such.

    So you would need to simplify your function call a bit like this

    <template functional>
        <label>
            <input type="checkbox" :checked="props.value"
                   @input="listeners.input && listeners.input($event.target.checked)">
        </label>
    </template>
    

    Which would be interpreted as desired

    var render = function(_h, _vm) {
      var _c = _vm._c
      return _c("label", [
        _c("input", {
          attrs: { type: "checkbox" },
          domProps: { checked: _vm.props.value },
          on: {
            input: function($event) {
              _vm.listeners.input && _vm.listeners.input($event.target.checked)
            }
          }
        })
      ])
    }
    

    You can take a more detailed look here, if you're interested about how exactly this render function comes to be https://github.com/vuejs/vue/blob/dev/packages/vue-template-compiler/browser.js#L4169