1

Background: I've built a standard single file component that takes a name prop and looks in different places my app's directory structure and provides the first matched component with that name. It was created to allow for "child theming" in my Vue.js CMS, called Resto. It's a similar principle to how WordPress looks for template files, first by checking the Child theme location, then reverting to the parent them if not found, etc.

Usage : The component can be used like this:

<!-- Find the PageHeader component
in the current child theme, parent theme,
or base components folder --->
<theme-component name="PageHeader">
    <h1>Maybe I'm a slot for the page title!</h1>
</theme-component> 

My goal : I want to convert to a functional component so it doesn't affect my app's render performance or show up in the Vue devtools. It looks like this:

<template>
  <component
    :is="dynamicComponent"
    v-if="dynamicComponent"
    v-bind="{ ...$attrs, ...$props }"
    v-on="$listeners"
    @hook:mounted="$emit('mounted')"
  >
    <slot />
  </component>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  name: 'ThemeComponent',
  props: {
    name: {
      type: String,
      required: true,
      default: '',
    },
  },
  data() {
    return {
      dynamicComponent: null,
      resolvedPath: '',
    }
  },
  computed: {
    ...mapGetters('site', ['getThemeName']),
    customThemeLoader() {
      if (!this.name.length) {
        return null
      }
      // console.log(`Trying custom theme component for ${this.customThemePath}`)
      return () => import(`@themes/${this.customThemePath}`)
    },
    defaultThemeLoader() {
      if (!this.name.length) {
        return null
      }
      // console.log(`Trying default component for ${this.name}`)
      return () => import(`@restoBaseTheme/${this.componentPath}`)
    },

    baseComponentLoader() {
      if (!this.name.length) {
        return null
      }
      // console.log(`Trying base component for ${this.name}`)
      return () => import(`@components/Base/${this.name}`)
    },

    componentPath() {
      return `components/${this.name}`
    }, // componentPath

    customThemePath() {
      return `${this.getThemeName}/${this.componentPath}`
    }, // customThemePath()
  },
  mounted() {
    this.customThemeLoader()
      .then(() => {
        // If found in the current custom Theme dir, load from there
        this.dynamicComponent = () => this.customThemeLoader()
        this.resolvedPath = `@themes/${this.customThemePath}`
      })
      .catch(() => {
        this.defaultThemeLoader()
          .then(() => {
            // If found in the default Theme dir, load from there
            this.dynamicComponent = () => this.defaultThemeLoader()
            this.resolvedPath = `@restoBaseTheme/${this.defaultThemePath}`
          })
          .catch(() => {
            this.baseComponentLoader()
              .then(() => {
                // Finally, if it can't be found, try the Base folder
                this.dynamicComponent = () => this.baseComponentLoader()
                this.resolvedPath = `@components/Base/${this.name}`
              })
              .catch(() => {
                // If found in the /components dir, load from there
                this.dynamicComponent = () => import(`@components/${this.name}`)
                this.resolvedPath = `@components/${this.name}`
              })
          })
      })
  },
}
</script>

I've tried SO many different approaches but I'm fairly new to functional components and render functions (never got into React).

The roadblock : I can't seem to figure out how to run the chained functions that I call in my original mounted() function. I've tried running it from inside the render function with no success.

Big Question

How can I find and dynamically import the component I'm targeting before I pass that component to the createElement function (or within my single file <template functional><template/>)?

Thanks all you Vue-heads! ✌️

Update: I stumbled across this solution for using the h() render function and randomly loading a component, but I'm not sure how to make it work to accept the name prop...

Community
  • 1
  • 1
slowFooMovement
  • 498
  • 5
  • 14
  • I'm not sure it is possible to translate this to a functional component. Functional component have no state, but this component requires some state because of all the async stuff you're doing in the `mounted` hook. Functional components are meant to be simple synchronous functions that take some input and renders itself immediately. – Decade Moon May 08 '20 at 01:05
  • Another thing: I wouldn't worry about the "performance improvement" a functional component would have here. Unless you're using this component a *lot* and have done some profiling to determine that it is a performance issue, then I'd say just use a normal component. – Decade Moon May 08 '20 at 01:10
  • thanks @DecadeMoon. I use this component *a lot* and want to use it more for easier theming on my platform. Good point about the "performance improvement". I should probably do some perf analysis to be sure. That said, the devtools issue is legit (nesting 4 or 5 of these things is a nightmare) for tooling. I got very close yesterday but the component wasn't actually loading and I don't know why (it would find the correct component file but the `h()` function wasn't rendering). I think there's a way to do `async` stuff that could act like `mounted()` in a functional component, I can FEEL IT! – slowFooMovement May 08 '20 at 17:32
  • See the way this render function works: https://github.com/funkhaus/components/blob/master/src/components/WpContent.vue – Drew Baker May 11 '20 at 17:39

1 Answers1

0

Late to the party, but I was in a similar situation, where I had a component in charge of conditionally render one of 11 different child components:

<template>
  <v-row>
    <v-col>
      <custom-title v-if="type === 'title'" :data="data" />
      <custom-paragraph v-else-if="type === 'paragraph'" :data="data" />
      <custom-text v-else-if="type === 'text'" :data="data" />
      ... 8 more times
    </v-col>
  </v-row>
</template>

<script>
export default {
  name: 'ProjectDynamicFormFieldDetail',
  components: {
    CustomTitle: () => import('@/modules/path/to/CustomTitle'),
    CustomParagraph: () => import('@/modules/path/to/CustomParagraph'),
    CustomText: () => import('@/modules/path/to/CustomText'),
    ... 8 more times
  },
  props: {
    type: {
      type: String,
      required: true,
    },
    data: {
      type: Object,
      default: null,
    }
  },
}
</script>

which of course is not ideal and pretty ugly.

The functional equivalent I came up with is the following

import Vue from 'vue'

export default {
  functional: true,
  props: { type: { type: String, required: true }, data: { type: Object, default: null } },
  render(createElement, { props: { type, data } } ) {
    // prop 'type' === ['Title', 'Paragraph', 'Text', etc]
    const element = `Custom${type}`
    // register the custom component globally
    Vue.component(element, require(`@/modules/path/to/${element}`).default)
    return createElement(element, { props: { data } })
  }
}

Couple of things:

  • lazy imports don't seem to work inside Vue.component, hence require().default is the way to go
  • in this case the prop 'type' needs to be formatted, either in the parent component or right here
Fi Li Ppo
  • 107
  • 11