Search code examples
vuejs3v-modelslotsvue-render-function

Vue3 - slot with v-model html rendered with render function


In vue3 I have this:

<template>
  <ChildComponent>
    <div>
      <input type="text" v-model="myValue"/>
    </div>
  </ChildComponent>
</template>

Here, in the child component the slot is rendered with input field, and it is binded to the myValue from the parent component.

How could I make it working with render function instead od declarative template like above?

In my app the slot inner html comes from the api rest service, but inserting it to the slot via v-html obviously does not work.


Solution

  • Vue SFC Playground

    You can compile your inner html to a render function.

    Here's a trick: we should provide a render context to the compiled render function with myValue. The problem that we cannot provide myValue as it is since the compiled function looks like that:

    (function anonymous(Vue
    ) {
    const _Vue = Vue
    const { createElementVNode: _createElementVNode } = _Vue
    
    const _hoisted_1 = ["onUpdate:modelValue"]
    
    return function render(_ctx, _cache) {
      with (_ctx) {
        const { vModelText: _vModelText, createElementVNode: _createElementVNode, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
    
        return (_openBlock(), _createElementBlock("div", null, [
          _withDirectives(_createElementVNode("input", {
            type: "text",
            "onUpdate:modelValue": $event => ((myValue) = $event)
          }, null, 8 /* PROPS */, _hoisted_1), [
            [_vModelText, myValue]
          ])
        ]))
      }
    }
    })
    

    As you see the problem in

    "onUpdate:modelValue": $event => ((myValue) = $event)
    

    So basically the ref is overwritten, not changed (myValue.value = $event). So we need to wrap our context into a reactive, that way when myValue in the context is changed, our linked myValue ref is changed too:

    <script setup>
    
    import ChildComponent from './ChildComponent.vue';
    import {compile, ref, reactive} from 'vue';
    
    const myValue = ref('test');
    
    const compiled = compile(`<div>
          <input type="text" v-model="myValue"/>
        </div>`);
    
    const content = () => compiled(reactive({myValue}));
    
    </script>
    
    <template>
      <div>{{ myValue }}</div>
      <ChildComponent>
        <content/>
      </ChildComponent>
    </template>
    

    To support the compile you should change Vue's runtime:

    vite.config.js:

    import { fileURLToPath, URL } from 'node:url'
    
    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    
    // https://vitejs.dev/config/
    export default defineConfig({
        plugins: [
            vue(),
        ],
        resolve: {
            alias: {
                '@': fileURLToPath(new URL('./src', import.meta.url)),
                vue: 'vue/dist/vue.esm-bundler.js',
            }
        }
    })