Advanced Vue.js Dynamic Functional Component using `:is` syntax and render function - javascript

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...

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

Related

Pass dynamic and responsive props to dynamic components with v-bind! NuxtJS

Long story short(maybe it is not so short after all): on the same page I want to import and load dynamic components based on the selected module.
I have an object defined in the assets which contains the informations about the components that should be loaded for each module, it looks like this:
export const modules = {
module1: {
calls: {...},
components: [
{
url: 'shared/PreviewItem',
properties: [
{
name: 'width',
value: 'leftComponentWidth'
}
]
},
{
url: 'shared/ResizingDivider',
properties: []
},
{
url: 'forms/FormItem',
properties: [
{
name: 'width',
value: 'rightComponentWidth'
},
{
name: 'item',
value: 'item'
}
]
}
]
},
module2: {...}
}
Then I have my index page:
<template>
<div class="item-content">
<component
:is="component"
v-for="(component, i) in dataComponents"
:key="i"
v-bind="component.propertiesToPass"
#emit-action="emitAction($event)"
/>
</div>
</template>
<script>
export default {
data() {
return {
item: null,
rightComponentWidth: 50,
leftComponentWidth: 50,
dataComponents: []
}
},
created() {
this.importComponents()
},
methods: {
importComponents() {
this.dataComponents = []
modules[this.$route.params.module].components.forEach(
(component) => {
import(`~/components/${component.url}`).then((res) => {
res.default.propertiesToPass = []
component.properties.forEach((prop) => {
res.default.propertiesToPass.push({
[prop.name]: this[prop.value]
})
})
this.dataComponents.push(res.default)
})
}
)
},
emitAction(event) {
this[event.function](event.props)
},
changeComponentsWidth(event) {
this.leftComponentWidth -= event
this.rightComponentWidth = 100 - this.leftComponentWidth
}
}
}
}
</script>
As it is probably easy to understand I have to components and one divider between them that can be dragged to the right or to the left for resize the width of the other two components.
The components are getting loaded and imported correctly, and the props are passed right, so the width of both of the components in the start are 50 50.
The issue is that by doing [prop.name]: this[prop.value] I am setting the props to the value of this[prop.value] variable, and not to the variable itself, so, when I try to resize the components by using the divider, the variables get updated but the props get not.
Then the props are not responsive or reactive, are fixed.
The only way to update the props of the components is to add the following lines to the changeComponentsWidth() method:
this.dataComponents[0].propertiesToPass[0].width = this.leftComponentWidth
this.dataComponents[2].propertiesToPass[0].width = this.rightComponentWidth
But this is not a very dynamic way.
So My question is:
Is it possible to bind the props to the variable itself instead of just passing its value?
Or are there other "dynamic" ways to keep my props "responsive and reactive"?

Issue when trying to interact with an API in Vuejs?

datalist.js
import axios from "axios";
export const datalist = () => {
return axios.get("myapiurl/name...").then((response) => response);
};
HelloWorld.vue
<template>
<div>
<div v-for="item in items" :key="item.DttID">
<router-link
:to="{
name: 'UserWithID',
params: { id: item.DepaD },
query: { DepaD: item.DepaID },
}"
>
<div class="bt-color">{{ item.DepaName }}</div>
</router-link>
</div>
<br /><br /><br />
<User />
</div>
</template>
<script>
import User from "./User.vue";
import { datalist } from "./datalist";
export default {
name: "HelloWorld",
components: {
User,
},
data() {
return {
items: datalist,
};
},
mounted() {
datalist().then((r) => {
this.items = r.data;
});
},
};
</script>
User.vue
<template>
<div>
<div v-for="(item, key) in user" :key="key">
{{ item.Accv }}
</div>
</div>
</template>
<script>
import { datalist } from "./datalist";
export default {
name: "User",
data() {
return {
lists: datalist,
};
},
computed: {
user: function () {
return this.lists.filter((item) => {
if (item.DepaD === this.$route.params.id) {
return item;
}
});
},
},
};
</script>
Error with the code is,
[Vue warn]: Error in render: "TypeError: this.lists.filter is not a function"
TypeError: this.lists.filter is not a function
The above error i am getting in User.vue component in the line number '20'
From the api which is in, datalist.js file, i think i am not fetching data correctly. or in the list filter there is problem in User.vue?
Try to change the following
HelloWorld.vue
data() {
return {
items: [],
};
},
mounted() {
datalist().then((r) => {
this.items = r.data;
});
},
User.vue
data() {
return {
lists: []
};
},
mounted() {
datalist().then((r) => {
this.lists = r.data;
});
},
At least this suppress the error, but i cant tell more based on your snippet since there are network issues :)
Since your datalist function returns a Promise, you need to wait for it to complete. To do this, simply modify your component code as follows:
import { datalist } from "./datalist";
export default {
name: "User",
data() {
return {
// empty array on initialization
lists: [],
};
},
computed: {
user: function() {
return this.lists.filter((item) => {
if (item.DeploymentID === this.$route.params.id) {
return item;
}
});
},
},
// asynchronous function - because internally we are waiting for datalist() to complete
async-mounted() {
this.users = await datalist() // or datalist().then(res => this.users = res) - then async is not needed
}
};
now there will be no errors when initializing the component, since initially lists is an empty array but after executing the request it will turn into what you need.
You may define any functions and import them, but they wont affect until you call them, in this case we have datalist function imported in both HelloWorld and User component, but it did not been called in User component. so your code:
data() {
return {
lists: datalist,
};
},
cause lists to be equal to datalist that is a function, no an array! where .filter() should be used after an array, not a function! that is the reason of error.
thus you should call function datalist and put it's response in lists instead of putting datalist itself in lists
Extra:
it is better to call axios inside the component, in mounted, created or ...
it is not good idea to call an axios command twice, can call it in HelloWorl component and pass it to User component via props

Vue not reacting to a computed props change

I am using the Vue composition API in one of my components and am having some trouble getting a component to show the correct rendered value from a computed prop change. It seems that if I feed the prop directly into the components render it reacts as it should but when I pass it through a computed property it does not.
I am not sure why this is as I would have expected it to be reactive in the computed property too?
Here is my code:
App.vue
<template>
<div id="app">
<Tester :testNumber="testNumber" />
</div>
</template>
<script>
import Tester from "./components/Tester";
export default {
name: "App",
components: {
Tester,
},
data() {
return {
testNumber: 1,
};
},
mounted() {
setTimeout(() => {
this.testNumber = 2;
}, 2000);
},
};
</script>
Tester.vue
<template>
<div>
<p>Here is the number straight from the props: {{ testNumber }}</p>
<p>
Here is the number when it goes through computed (does not update):
{{ testNumberComputed }}
</p>
</div>
</template>
<script>
import { computed } from "#vue/composition-api";
export default {
props: {
testNumber: {
type: Number,
required: true,
},
},
setup({ testNumber }) {
return {
testNumberComputed: computed(() => {
return testNumber;
}),
};
},
};
</script>
Here is a working codesandbox:
https://codesandbox.io/s/vue-composition-api-example-forked-l4xpo?file=/src/components/Tester.vue
I know I could use a watcher but I would like to avoid that if I can as it's cleaner the current way I have it
Don't destruct the prop in order to keep its reactivity setup({ testNumber }) :
setup(props) {
return {
testNumberComputed: computed(() => {
return props.testNumber;
}),
};
}

vue js use v-on within watch method

I have a component that has a data property called rows, which are used to populate a dynamic table (I'm using Datatables).
I have a computed property called clientLeads that loads store data.
I have a watch function that watches the clientLeads computed property and updates the rows data property.
I would like to use the router-link component to create a Vue JS Router link to a profile page.
Here is the Datatables component (src: https://codepen.io/stwilson/pen/oBRePd):
<script>
import 'datatables.net-bs4'
import jQuery from 'jquery'
export default {
template: '<table class="table table-striped dt-responsive nowrap" style="width:100%"></table>',
props: {
headers: {
default () {
return {}
}
},
rows: {
default () {
return []
}
}
},
data () {
return {
dTHandle: null
}
},
watch: {
rows (val, oldVal) {
let vm = this
vm.dtHandle.clear()
vm.dtHandle.rows.add(vm.rows)
vm.dtHandle.draw()
}
},
mounted () {
let vm = this
vm.dtHandle = jQuery(this.$el).DataTable({
classes: {
sWrapper: 'dataTables_wrapper dt-bootstrap4'
},
columns: vm.headers,
data: vm.rows,
searching: false,
paging: false,
info: true,
responsive: {
details: {
type: 'column'
}
}
})
}
}
</script>
And here is my view component:
<template>
<div>
<data-tables :headers="headers" :rows="rows" />
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
data () {
return {
headers: [
{ title: 'Name' },
{ title: ' ' }
],
rows: []
}
},
computed: mapGetters({
clientLeads: 'clientLeads'
}),
watch: {
clientLeads (val, oldVal) {
let vm = this
let rows = []
val.forEach(function (item) {
let row = []
row.push(item.name)
row.push('<router-link to="/lead_details/' + item.id + '"><a><i class="fa fa-eye"></i></a></router-link>')
rows.push(row)
})
vm.rows = rows
}
},
created () {
// get leads
this.$store.dispatch('getClientLeads', {
status: this.status
})
}
}
</script>
However, the resulting html is:
<router-link to="/lead_details/123"><a><i class="fa fa-eye"></i></a></router-link>
...instead of:
<i class="fa fa-eye"></i>
I understand why but cannot figure out a workaround to resolve my issue.
The problem here is that datatables manipulates the DOM outside of Vue.js. As Vue doesn't handle the elements created by the lib, the router link component is not 'interpreted'.
If you want to keep going with this datables library maybe you can try to add a standard <a> tag with a # prefixed href :
<i class="fa fa-eye"></i>
As vue-router seems to prefix url with a # when you are in the default mode (hash-mode) : https://router.vuejs.org/guide/essentials/history-mode.html#example-server-configurations. Or you can just change the mode to history :
const router = new VueRouter({
mode: 'history',
routes: [...]
})
And get rid of the #.
Of course it supposes you have the matching route declared previously. It's a little tweaky as you will bypass vue-router like this and reload the page, but it works.
Working with vue, I strongly recommend, as much as possible, to prefered libraries that doesn't manipulate directly the DOM and let this task to Vue. Vue components give more options to be tweaked.
There are many components libraries that seems to do the job, for example :
https://vuetifyjs.com/en/components/data-tables
or http://element.eleme.io/#/en-US/component/table

Passing props dynamically to dynamic component in VueJS

I've a dynamic view:
<div id="myview">
<div :is="currentComponent"></div>
</div>
with an associated Vue instance:
new Vue ({
data: function () {
return {
currentComponent: 'myComponent',
}
},
}).$mount('#myview');
This allows me to change my component dynamically.
In my case, I have three different components: myComponent, myComponent1, and myComponent2. And I switch between them like this:
Vue.component('myComponent', {
template: "<button #click=\"$parent.currentComponent = 'myComponent1'\"></button>"
}
Now, I'd like to pass props to myComponent1.
How can I pass these props when I change the component type to myComponent1?
To pass props dynamically, you can add the v-bind directive to your dynamic component and pass an object containing your prop names and values:
So your dynamic component would look like this:
<component :is="currentComponent" v-bind="currentProperties"></component>
And in your Vue instance, currentProperties can change based on the current component:
data: function () {
return {
currentComponent: 'myComponent',
}
},
computed: {
currentProperties: function() {
if (this.currentComponent === 'myComponent') {
return { foo: 'bar' }
}
}
}
So now, when the currentComponent is myComponent, it will have a foo property equal to 'bar'. And when it isn't, no properties will be passed.
You can also do without computed property and inline the object.
<div v-bind="{ id: someProp, 'other-attr': otherProp }"></div>
Shown in the docs on V-Bind - https://v2.vuejs.org/v2/api/#v-bind
You could build it like...
comp: { component: 'ComponentName', props: { square: true, outlined: true, dense: true }, model: 'form.bar' }
<component :is="comp.component" v-bind="{...comp.props}" v-model="comp.model"/>
I have the same challenge, fixed by the following:
<component :is="currentComponent" v-bind="resetProps">
{{ title }}
</component>
and the script is
export default {
…
props:['title'],
data() {
return {
currentComponent: 'component-name',
}
},
computed: {
resetProps() {
return { ...this.$attrs };
},
}
<div
:color="'error'"
:onClick="handleOnclick"
:title="'Title'"
/>
I'm came from reactjs and I found this solve my issue
If you have imported you code through require
var patientDetailsEdit = require('../patient-profile/patient-profile-personal-details-edit')
and initalize the data instance as below
data: function () {
return {
currentView: patientDetailsEdit,
}
you can also reference the component through the name property if you r component has it assigned
currentProperties: function() {
if (this.currentView.name === 'Personal-Details-Edit') {
return { mode: 'create' }
}
}
When you use the <component> inside a v-for you can change the answer of thanksd as follow:
methods: {
getCurrentProperties(component) {
if (component === 'myComponent') {
return {foo: baz};
}
}
},
usage
<div v-for="object in object.items" :key="object._your_id">
<component :is="object.component" v-bind="getCurrentProperties(object.component)" />
</div>
Let me know if there is an easier way.

Categories

Resources