1
votes

I am writing VueJS (2.5.22) application using Typescript and trying to add component dynamically at run-time. I ran into two issues with Typescript. Adding child component as named slots and subscribing to emit event from child components. I was able to work around slots by appending child component, however i would like to use named slots. Any input on slot is appreciate.

I am still trying to get second issue resolved of parent component subscribing to child event and updating property in parent component.

The child component is going to emit "update-value". How do i subscribe to this event on parent component dynamically?

Thanks,

Please create tag for: vuejs-component vuejs-dynamic-component vuejs-typescript vuejs-emit

parent component adding dynamically at run-time in created method


<div>
    <p>Dynamic components</p>
    <div ref="controls"></div>
</div>

export default class ParentComponent extends Vue {

public $refs: Vue['$refs'] & {
    controls: HTMLElement
}
public $slots: Vue['$slots'] & {
    TextBox: TextBox
}

vuejs created method
--------------------

const labelControlContainerClass = Vue.extend(LabelControlContainer)
const textBoxClass = Vue.extend(TextBox)

const fields = table.fields
for (const key in fields) {
  if (fields.hasOwnProperty(key)) {
      const element = fields[key]
        if (element.fieldType === 'Char') {
          const textBoxInstance = new textBoxClass({
            // props
            propsData: {
              value: '',
              placeholder: element.translatedCaption,
              name: key
            },
            // how to subscript to "update-value" event???
          })
          textBoxInstance.$mount()

          const instance = new labelControlContainerClass({
            // props
            propsData: {
              caption: element.translatedCaption,
              bold: false
            },
          })

          instance.$mount()
          instance.$el.appendChild(textBoxInstance.$el) // add child component, try adding named slots, but didn't work
          this.$refs.controls.appendChild(instance.$el)
      }
  }
}

}
label component with slot. named slot didn't worked

<template>
  <div class="controlContainer" :class="{vertial: labelTop}">
    <span v-bind:class="{bold: bold}" class=".controlContainer__cisLabel">{{caption}}</span>
    <slot name='control'></slot>
    <slot></slot>
  </div>
</template>

<script lang='ts'>
import Vue from 'vue'
import { Component, Prop } from 'vue-property-decorator'

@Component({
})

export default class LabelControlContainer extends Vue {
  @Prop({ type: String, default: '' }) public caption: string
  @Prop({ type: Boolean, default: true }) public bold: boolean
  @Prop({ type: Boolean, default: false }) public labelTop: boolean
}

</script>

child component that is going to added to slot and emit on value change

export default class TextBox extends Vue {
  @Prop({ type: String, default: 'placeholder text' }) public placeholder: string
  @Prop({ type: Object, default: () => ({}) }) public attributes: any
  @Prop({ type: Boolean, default: false }) public readonly: boolean
  @Prop({ type: String, default: 'text' }) public mode: string
  @Prop({ type: Boolean, default: true }) public isValid: boolean
  @Prop({ type: String, default: '' }) public value: string
  @Prop({ type: String, default: '' }) public name: string
  @Prop({ type: Boolean, default: false }) public setFocus: boolean

  private default = {}
  private unlockable: boolean = this.readonly

  private valueChanged(data: any): void {
    this.$emit('update-value', data.value, this.name)
  }
}
1

1 Answers

0
votes

I'm not using TypeScript but I think this is not about TypeScript so I will answer using plain JavaScript.

To programmatically add slot element you can use $slots property like this:

vm.$slots.default = ['Hello']
vm.$slots. foo = ['Hello'] // for named slots

…But unfortunately the slots must be the array of VNodes, not normal DOM Element or Vue Components. You can use the $createElement method to create them like so:

vm.$slots.default = [vm.$createElement('div', ['Hello'])]
vm.$slots.default = [vm.$createElement('HelloWorld')] // custom component

So your code will look like that:

const TextBox = {
  template: `
    <div>{{ text }}</div>
  `,
  props: ['text']
}

const LabelControl = {
  template: `
    <div>
      <slot name='control'></slot>
    </div>
  `,
  components: {
    TextBox
  }
}

const LabelControlComponent = Vue.extend(LabelControl)

new Vue({
  el: '#app',
  template: `
    <div>
      <div ref='controls'></div>
    </div>
  `,
  mounted () {
    let texts = ['a', 'b', 'c', 'd']
    texts.forEach(text => {
      let labelControl = new LabelControlComponent()
      labelControl.$slots.control = [
        labelControl.$createElement('TextBox', {
          props: { text }
        })
      ]
      labelControl.$mount()
      this.$refs.controls.appendChild(labelControl.$el)
    })
  }
})

Listening to events from a slot child seems impossible, as it's a different scope, but you can still use the $parent property to call a parent method directly or emit an event from the parent.

const TextBox = {
  template: `
    <div @click='$parent.show(text)'>{{ text }}</div>
  `
}

const LabelControl = {
  methods: {
    show (text) {
      this.text = text
    }
  }
}

JSFiddle Example

References: