How do i dynamically generate vue components from wordpress custom fields - javascript

I am building a website using vue with nuxt, loading data from a wordress site through the rest api.
I would like to give the client the ability to modify page templates using custom fields, so I need to dynamically create my vue templates with vue components that are generated depending on the custom fields placed in the wordpress page editor.
This is simplified, but for example, if the clients builds a page with three custom fields:
[custom-field type='hero']
[custom-field type='slider']
[custom-field type='testimonial']
I can get the field information via the rest api in a json object, like this:
page: {
acf: [
{field 1: {
{type: 'hero'},
{content: '...'}
},
{field 2: {
{type:'slider'},
{content: '...'}
},
{field 3: {
{type:'testimonial'},
{content: '...'}
}
}
}
I'll bring this into my vue app, but then i would the template to generate dynamically from a list of possible components mapped to the custom field types. the above would output:
<template>
<Hero ... />
<Slider ... />
<Testimonial ... />
</template>
Would this be done using the v-is directive (https://v2.vuejs.org/v2/guide/components-dynamic-async.html) like:
<component v-for="field in custom-fields" v-is="field.type" :data="field.data"/>?
Is this possible? Any help would be greatly appreciated.

You can dynamically register and display components in Nuxt but there are some caveats.
Method 1 - SSR & SSG Support
This method will register your components dynamically and maintain the integrity of Server Side Rendering/Static Site Generation.
The only trade-off here is that you will need to list the names and file locations of all potential imported components. This shouldn't be too cumbersome for your use-case (I don't imagine you have too many ACF fields) but it could be a lengthy job if you're intending to build an entire component library from it.
<template>
<div>
<div v-for="field in page.acf" :key="field.uniqueId">
<component :is="field.type" :my-prop="field.content" />
</div>
</div>
</template>
<script>
export default {
components: {
hero: () => import('~/components/hero.vue'),
slider: () => import('~/components/slider.vue'),
testimonial: () => import('~/components/testimonial.vue')
},
data() {
return {
page: {
acf: [
{
uniqueId: 1,
type: 'hero',
content: '...'
},
{
uniqueId: 2,
type: 'slider',
content: '...'
},
{
uniqueId: 3,
type: 'testimonial',
content: '...'
}
]
}
}
}
}
</script>
Method 2 - Client Side Only Rendering
This method allows you to programmatically register the component's name and file location. This saves you from writing out each component one by one, but comes at the cost of no SSR or SSG support. However, this may be preferable if you're going the SPA route.
<template>
<div>
<div v-for="field in page.acf" :key="field.uniqueId">
<no-ssr>
<component :is="field.type" :my-prop="field.content" />
</no-ssr>
</div>
</div>
</template>
<script>
import Vue from 'vue'
export default {
data() {
return {
page: {
acf: [
{
uniqueId: 1,
type: 'hero',
content: '...'
},
{
uniqueId: 2,
type: 'slider',
content: '...'
},
{
uniqueId: 3,
type: 'testimonial',
content: '...'
}
]
}
}
},
mounted() {
const sections = this.page.acf
for (let i = 0; i < sections.length; i++) {
Vue.component(sections[i].type, () =>
import(`~/components/${sections[i].type}.vue`)
)
}
}
}
</script>
Be aware that the <no-ssr> tag is being deprecated and if you are using Nuxt above v2.9.0, you should use <client-only> instead.
Notes On Your Question
I understand you've tried to simplify your data architecture but your JSON object would be rather difficult to loop through since the key changes on each object in array. You've also got unnecessary objects in the data structure. I've simplified the structure in the data method so you can understand the concept.
To get the v-for loop working, you need to nest the component inside a HTML tag that has a v-for attribute (as demonstrated above).
You may want to sanitise the data you get from the WordPress API by ensuring that WordPress doesn't supply you with a module that doesn't correspond to a component. If the API supplies a type that doesn't correspond to a component, the whole build will fail.
Hope this helps!
P.S If anyone knows of a method that allows you to programatically set the component name and component file location while maintaining SSG - please let me know!

Related

Is it possible to sanitize a prop without creating a computed or data property

Let's say I have a vue component defined like this:
TestSelect.vue
<template>
<v-select
v-model="selectValue"
:items="items"
item-text="name"
item-value="id"
#change="changed"
>
</v-select>
</template>
<script>
export default {
props: {
link: String,
items: Array,
value: Number,
},
data: (props) => ({
selectValue: props.value,
saneLink: props.link.slice(-1) == "/" ? props.link : `${props.link}/`,
}),
methods: {
changed(value) {
document.location = this.saneLink + value;
},
},
};
</script>
This works fine and can be used either like:
<test-select
link="test"
:value="1"
:items="[
{ id: 1, name: 'dogbert' },
{ id: 2, name: 'pointy' },
]"
></test-select>
or, like
<test-select
link="test/"
:value="1"
:items="[
{ id: 1, name: 'dogbert' },
{ id: 2, name: 'pointy' },
]"
></test-select>
While this is fine, but out of curiosity and a desire to simplify my code I was wondering if there is a way to sanitize a prop without defining a new data or computed property.
Unfortunately now you can only validate props in Vue directly, not sanitise them.
The ability to sanitise props was intentionally removed to reduce Vue bundle size and because the same effect can be achieved using computed properties like you touched on.
That's the preferred approach, unfortunately.
You can however, write a custom plugin that allows you to implement prop sanitising.
From a cursory google search (please don't blindly use this plugin without auditing the code first), I've found this Vue plugin intended to allow you to coerce a prop into a different value.
https://github.com/posva/vue-coerce-props
I can't vouch for the efficacy of that code, or any issues it may have but it is an alternative approach to using the recommended computed properties.

Creating treeview in vue js - unknown depth of the tree - Modifying the DOM

I'm trying to create a vue js application where a treeview is displayed to the user. The elements inside the treeview can contain other elements, that can contain other elements etc ... With no limit, which means that the depth of the treeview is not known. If I'm right, it means I can't simply use the v-for directive (because it implies to know the depth right ?)
So i'm basically looping going through a json array and creating <ul> and <li> tags to append them to some other tag in the DOM, but if I do this, they don't get the styles of their class applied to them.
I suppose it's because Vue doesn't like the fact that we modify the DOM without having vue doing it for us.
Also, We don't want to use components libraries like vuetify, we want to do it with vue only and simple javascript.
Thank you !
This is actually pretty straight forward in Vue.js.
What you have to do is simply create a component that invokes itself but changing the v-for to use the current tree branch's children.
An important step for making this work in Vue is to apply the name key to the component. Otherwise, the component can not invoke itself.
I have provided a simple example below using HTML's neat details element.
// Your recursive branch component "branch.vue"
const branch = {
name: 'branch',
props: {
branch: {
type: Object,
default: () => ({}),
},
},
template: `
<details>
<summary>{{ branch.title }}</summary>
<branch
v-for="branch in branch.children"
:key="branch.title"
:branch="branch"
/>
</details>
`,
}
// Your page view(component) where you want to display the tree
new Vue({
el: '#app',
name: 'tree',
components: {
branch,
},
data() {
return {
tree: [
{
title: 'parent 1',
children: [
{
title: 'child 1',
children: [
{
title: 'child 1-1',
children: [],
},
{
title: 'child 1-2',
children: [],
},
],
},
],
},
],
};
},
})
#app > details {
margin-left: 0;
}
details {
margin-left: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<branch v-for="branch in tree" :key="branch.title" :branch="branch" />
</div>
The solution is to create a recursive component, for example see here and
here

Is it possible to add content to a global Vue component from a single file comp?

I have made a global component that will render the content we want.
This component is very simple
<template>
<section
id="help"
class="collapse"
>
<div class="container-fluid">
<slot />
</div>
</section>
</template>
<script>
export default {
name: 'VHelp',
};
</script>
I use it inside my base template with
<v-help />
I'm trying to add content to this component slot from another single file component using.
<v-help>
<p>esgssthsrthsrt</p>
</v-help>
But this logically create another instance of my comp, with the p tag inside. Not the correct thing I want to do.
So I tried with virtual DOM and rendering function, replacing slot by <v-elements-generator :elements="$store.state.help.helpElements" /> inside my VHelp comp.
The store helpElements is a simple array with objects inside.
{
type: 'a',
config: {
class: 'btn btn-default',
},
nestedElements: [
{
type: 'span',
value: 'example',
},
{
type: 'i',
},
],
},
Then inside my VElementsGenerator comp I have a render function that with render element inside virtual DOM from an object like
<script>
import {
cloneDeep,
isEmpty,
} from 'lodash';
export default {
name: 'VElementsGenerator',
props: {
elements: {
type: Array,
required: true,
},
},
methods: {
iterateThroughObject(object, createElement, isNestedElement = false) {
const generatedElement = [];
for (const entry of object) {
const nestedElements = [];
let elementConfig = {};
if (typeof entry.config !== 'undefined') {
elementConfig = cloneDeep(entry.config);
}
if (entry.nestedElements) {
nestedElements.push(this.iterateThroughObject(entry.nestedElements, createElement, true));
}
generatedElement.push(createElement(
entry.type,
isEmpty(elementConfig) ? entry.value : elementConfig,
nestedElements
));
if (typeof entry.parentValue !== 'undefined') {
generatedElement.push(entry.parentValue);
}
}
if (isNestedElement) {
return generatedElement.length === 1 ? generatedElement[0] : generatedElement;
}
return createElement('div', generatedElement);
},
},
render(createElement) {
if (this.elements) {
return this.iterateThroughObject(this.elements, createElement);
}
return false;
},
};
</script>
This second method is working well but if I want to render complex data, the object used inside the rendering function is very very long and complex to read.
So I'm trying to find another way to add content to a global component used inside a base layout only when I want it on a child component.
I can't use this VHelp component directly inside children comps because the HTML page architecture will be totally wrong.
I'm wondering if this is possible to add content (preferably HTML) to a component slot from a single file comp without re-creating a new instance of the component?
Furthermore I think this is very ugly to save HTML as string inside a Vuex store. So I don't even know if this is possible and if I need to completely change the way I'm trying to do this.
Any ideas ?
In the store, you should only store data and not an HTML structure. The way to go with this problem would be to store the current state of the content of the v-help component in the store. Then, you would have a single v-help component with a slot (like you already proposed). You should pass different contents according to the state in the store. Here is an abstract example:
<v-help>
<content-one v-if="$store.state.content === 'CONTENT_ONE' />
<content-two v-else-if="$store.state.content === 'CONTENT_TWO' />
<content-fallback v-else />
</v-help>
Child element somewhere else:
<div>
<button #click="$store.commit('setContentToOne')">Content 1</button>
</div>
Vuex Store:
state: {
content: null
},
mutations: {
setContentToOne(state) {
state.content = 'CONTENT_ONE';
}
}
Of course it depends on your requirements and especially on how many different scenarios are used if this is the best way to achieve this. If I understood you correctly, you are saving help elements to the store. You could also save an array of currently selected help elements in there and just display them directly in the v-help component.
EDIT:
Of course you can also just save the static component (or its name) in the store. Then, you could dynamically decide in the child components, which content is shown in v-help. Here is an example:
<v-help>
<component :is="$store.state.helpComponent" v-if="$store.state.helpComponent !== null" />
</v-help>
Test Component:
<template>
test component
</template>
<script>
export default {
name: 'test-component'
};
</script>
Child element somewhere else (variant 1, storing the name in Vuex):
<div>
<button #click="$store.commit('setHelpComponent', 'test-component')">Set v-help component to 'test-component'</button>
</div>
Child element somewhere else (variant 2, storing the whole component in Vuex):
<template>
<button #click="$store.commit('setHelpComponent', testComponent)">Set v-help component to testComponent (imported)</button>
</template>
<script>
import TestComponent from '#/components/TestComponent';
export default {
name: 'some-child-component',
computed: {
testComponent() {
return TestComponent;
}
}
};
</script>
Child element somewhere else (variant 3, storing the name, derived from the imported component, in Vuex; I would go with this variant):
<template>
<button #click="$store.commit('setHelpComponent', testComponentName)">Set v-help component to 'test-component'</button>
</template>
<script>
import TestComponent from '#/components/TestComponent';
export default {
name: 'some-child-component',
computed: {
testComponentName() {
return TestComponent.name;
}
}
};
</script>
Vuex Store:
state: {
helpComponent: null
},
mutations: {
setHelpComponent(state, value) {
state.helpComponent = value;
}
}
See also the documentation for dynamic components (<component :is=""> syntax): https://v2.vuejs.org/v2/guide/components.html#Dynamic-Components

Vue JS - Problem with computed property not updating

I am quite new with VueJS and I have been having trouble lately with some computed properties which do not update as I would like. I've done quite some research on Stack Overflow, Vue documentation and other ressources but i haven't found any solution yet.
The "app" is basic. I've got a parent component (Laundry) which has 3 child components (LaundryMachine). The idea is to have for each machine a button which displays its availability and updates the latter when clicked on.
In order to store the availability of all machines, I have a data in the parent component (availabilities) which is an array of booleans. Each element corresponds to a machine's availability.
When I click on the button, I know the array availibities updates correctly thanks to the console.log. However, for each machine, the computed property "available" does not update is I would want it to and I have no clue why.
Here is the code
Parent component:
<div id="machines">
<laundry-machine
name="AA"
v-bind:machineNum="0"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
<laundry-machine
name="BB"
v-bind:machineNum="1"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
<laundry-machine
name="CC"
v-bind:machineNum="2"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
</div>
</div>
</template>
<script>
import LaundryMachine from './LaundryMachine.vue';
export default {
name: 'Laundry',
components: {
'laundry-machine': LaundryMachine
},
data: function() {
return {
availabilities: [true, true, true]
};
},
methods: {
editAvailabilities(index) {
this.availabilities[index] = !this.availabilities[index];
console.log(this.availabilities);
}
}
};
</script>
Child component:
<template>
<div class="about">
<h2>{{ name }}</h2>
<img src="../assets/washing_machine.png" /><br />
<v-btn color="primary" v-on:click="changeAvailability">
{{ this.availability }}</v-btn>
</div>
</template>
<script>
export default {
name: 'LaundryMachine',
props: {
name: String,
machineNum: Number,
availableArray: Array
},
methods: {
changeAvailability: function(event) {
this.$emit('change-avlb', this.machineNum);
console.log(this.availableArray);
console.log('available' + this.available);
}
},
computed: {
available: function() {
return this.availableArray[this.machineNum];
},
availability: function() {
if (this.available) {
return 'disponible';
} else {
return 'indisponible';
}
}
}
};
</script>
Anyway, thanks in advance !
Your problem comes not from the computed properties in the children, rather from the editAvailabilities method in the parent.
The problem is this line in particular:
this.availabilities[index] = !this.availabilities[index];
As you can read here, Vue has problems tracking changes when you modify an array by index.
Instead, you should do:
this.$set(this.availabilities, index, !this.availabilities[index]);
To switch the value at that index and let Vue track that change.

How to access data property of vue component

I am new to Vue.js and I think its amazing. I have been tasked to start implementing some vue components in our non-greenfield web application and I thought I would start by implementing some self-contained "widgets" that have to deal heavily with state in my work's rails app.
Its working great as a self-contained component but I want to load it with a data attribute so the component know what it needs to deal with. My Vue file looks like (I have redacted parts of this due to IP concerns):
<template>
<div class="card">
<div class="card-body">
${{ b.id }}
</div>
<div class="card-footer bg--blue-sky">
${{ b.amount }}
</div>
</div>
</template>
<script>
export default {
data: function () {
return {
errors: [],
b: {
id: null,
amount: null
}
}
},
// Fetches posts when the component is created.
created: function () {
jQuery.ajax({
url: "/api/b/" + '2' + ".json",
method: 'GET',
dataType: "json"
})
.then(response => {
this.b = response.b
})
.catch(e => {
this.errors.push(e)
});
}
}
</script>
<style scoped>
</style>
The component is registered with:
import FiDis from '../components/fi_dis.vue'
Vue.component('fi_dis', FiDis);
document.addEventListener('turbolinks:load', () => {
const fi_dis = new Vue({
el: '#bs',
components: { FiDis }
})
});
And in my html.erb code I create the components with:
<div id="bs" policy="2">
<fi_dis data-b-id="1"></fi_dis>
<fi_dis data-b-id="2"></fi_dis>
</div>
This all works flawlessly, and does exactly what I want it to do except for one thing. I want to access the data-b-id attribute within the created function of the component (i.e. replace the number '2' in the url of the ajax call above with the value form the attribute). In this way, I hope for the component to handle ANY "fi_dis" I choose, merely by specifying the b-id in the data attribute I want it to handle.
How can I achieve this?
You communicate data values passing props from parent component to child components.
So for example you should define which props your component is allowed to receive:
import FiDis from '../components/fi_dis.vue'
Vue.component('fi_dis', FiDis);
document.addEventListener('turbolinks:load', () => {
const fi_dis = new Vue({
el: '#bs',
components: { FiDis },
props['bId'],
created() { // This is a lifecycle method
this.printPropertyValue();
},
methods: {
// Your custom methods goes here separeted by commas
printPropertyValue() {
console.log(this.bId);
}
}
})
});
And the sintax for passing the data from the component implementation is using v-bind:propertyName or :propertyName (short hand).
<div id="bs" policy="2">
<fi_dis :bId="1"></fi_dis>
<fi_dis :bId="2"></fi_dis>
</div>

Categories

Resources