Есть ли эквивалент vue.js для ngTemplateOutlet?

Имеет ли vue.js эквивалент директивы Angular *ngTemplateOutlet? Допустим, у меня есть некоторые компоненты, определенные следующим образом:

    <template>
        <div id="independentComponent">
            Hello, {{firstName}}!
        </div>
    </template>
    <script>
        export default {
            name: "independentComponent",
            props: ['firstName']
        }
    </script>

    ...

    <template>
        <div id="someChildComponent">
            <slot></slot>
            <span>Let get started.</span>
        </div>
    </template>
    <script>
        export default {
            name: "someChildComponent"
        }
    </script>

Я хочу иметь возможность сделать что-то вроде этого:

<template>
    <div id="parentComponent">
        <template #indepdentInstance>
            <independentComponent :firstName="firstName" />
        </template>
        <someChildComponent>
            <template #indepdentInstance></template>
        </someChildComponent>
    </div>
</template>
<script>
    export default {
        name: "parentComponent",
        components: {
            someChildComponent,
            independentComponent
        },
        data() {
            return {
                firstName: "Bob"
            }
        }
    }
</script>

В Angular я мог бы сделать это с помощью

<div id="parentComponent">
    <someChildComponent>
        <ng-container *ngTemplateOutlet="independentInstance"></ng-container>
    </someChildComponent>

    <ng-template #independentInstance>
        <independentComponent [firstName]="firstName"></independentComponent>
    </ng-template>
</div>

Но похоже, что Vue требует, чтобы элемент был записан в DOM именно там, где он находится в шаблоне. Есть ли способ ссылаться на встроенный элемент и использовать его для передачи другому компоненту в качестве слота?

Ответ 1

Вы не можете повторно использовать шаблоны, такие как ngTemplateOutlet, но можете объединить идею $refs, v-pre и компиляции шаблона времени выполнения с v-runtime-template для достижения этой цели.

Сначала создайте шаблон многократного использования (<ng-template #independentInstance>):

<div ref="independentInstance" v-show="false">
    <template v-pre> <!-- v-pre disable compiling content of template -->
        <div> <!-- We need this div, because only one root element allowed in templates -->
            <h2>Reusable template</h2>
            <input type="text" v-model="testContext.readWriteVar">
            <input type="text" v-model="readOnlyVar">
            <progress-bar></progress-bar>
        </div>
    </template>
</div>

Теперь вы можете повторно использовать шаблон independentInstance:

<v-runtime-template
    :template="$refs.independentInstance.innerHTML"
    v-if="$refs.independentInstance">
</v-runtime-template>

Но имейте в виду, что вы не можете изменить readOnlyVar из шаблона independentInstance - vue предупредит вас с помощью:

[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop value. Prop being mutated: "readOnlyVar"

Но вы можете обернуть его в object, и он будет работать:

@Component({
    components: {
        VRuntimeTemplate
    }
})
export default class ObjectList extends Vue {
    reusableContext = {
        readWriteVar: '...'
    };

    readOnlyVar = '...';
}

Ответ 2

Вы можете попробовать Portal vue, написанный LinusBorg основным участником команды Vue.

PortalVue представляет собой набор из двух компонентов, которые позволяют вам визуализировать шаблон компонента (или его часть) в любом месте документа - даже вне той части, которая контролируется вашим приложением Vue!

Пример кода:

<template>
  <div id="parentComponent">
    <portal to="independentInstance">
      <!-- This slot content will be rendered wherever the <portal-target>
           with name 'independentInstance' is located. -->
      <independent-component :first-name="firstName" />
    </portal>

    <some-child-component>
        <portal-target name="independentInstance">
          <!--
          This component can be located anywhere in your App.
          The slot content of the above portal component will be rendered here.
          -->
        </portal-target>
    </some-child-component>
  </div>
</template>

Существует также vue-simple-portal, написанный тем же автором, который меньше по размеру, но монтирует компонент в конец элемента body.

Ответ 3

X-шаблоны

Используйте x-template. Определите тег сценария внутри файла index.html.

На x-template можно ссылаться в нескольких компонентах в определении шаблона как #my-template.

Запустите фрагмент для примера.

Смотрите документ Vue.js для получения дополнительной информации о x-шаблонах.

Vue.component('my-firstname', {
  template: '#my-template',
  data() {
    return {
      label: 'First name'
    }
  }
});

Vue.component('my-lastname', {
  template: '#my-template',
  data() {
    return {
      label: 'Last name'
    }
  }
});

new Vue({
  el: '#app'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <my-firstname></my-firstname>
  <my-lastname></my-lastname>
</div>

<script type="text/x-template" id="my-template">
  <div>
    <label>{{ label }}</label>
    <input />
  </div>
</script>

Ответ 4

Не совсем уверен, что я понимаю вашу проблему здесь, но я постараюсь дать вам кое-что, что я решу сделать, если я хочу добавить два компонента в один шаблон.

HeaderSection.vue

    <template>
        <div id="header_id" :style="'background:'+my_color">
            welcome to my blog
        </div>
    </template>
    <script>
        export default {

            props: ['my_color']
        }
    </script>

BodySection.vue

    <template>
        <div id="body_id">
            body section here
        </div>
    </template>
    <script>
        export default {

        }
    </script>

home.vue

<template>

    <div id="parentComponent">

        <header-section :color="my_color" />

        <body-section />
    </div>
</template>
<script>
    import HeaderSection from "./components/HeaderSection.vue"
    import BodySection from "./components/BodySection.vue"
    export default {

        name: "home",

        components: {
            HeaderSection,
            BodySection
        },

        data() {
            return {
                my_color: "red"
            }
        }
    }
</script>