v-runtime-template and vuex cause infinite update loop - vuejs2

I came across an infinite loop that really confused me. I used v-runtime-template to load dynamic forms, everything works fine when I use static data, but switches to an infinite loop after getting data from vuex.
I have written two examples with CodeSandbox, but note that clicking on Demo2 may cause the browser to die.
The loading of data needs to be done through vuex. How to solve the problem of infinite loop, I look forward to your help.

I have solved this problem. defining a sub-component to load v-runtime-template, get data in the parent component and passing it to sub-components via props.
Code is like this:
<template>
<form data-vv-scope="custom-form" v-if="html">
<form-content :html="html" :data="data" :permission="permission" />
</form>
</template>
<script>
import FormContent from "#/components/FormContent.vue";
import { mapState } from "vuex";
export default {
name: "demo2",
computed: mapState({
html: state => state.html,
data: state => state.data,
permission: state => state.permission
}),
components: {
FormContent
},
created() {
this.$store.dispatch("loadForm");
}
};
</script>

Related

What's the most idomatic Vue way of handling this higher-order component?

I have a VueJS organization and architecture question. I have a bunch of pages that serve as CRUD pages for individual objects in my system. They share a lot of code . I've abstracted this away in a shared component but I don't love what I did and I'm looking for advice on the idiomatic Vue way to do this.
I'm trying to create a higher order component that accepts two arguments:
An edit component that is the editable view of each object. So you can think of it as a stand in for a text input with a v-model accept that it has tons of different inputs.
A list component which displays a list of all the objects for that type in the system. It controls navigation to edit that object.
Normally this would be simply something where I use slots and invoke this component in the view page for each CRUD object. So basically I'd have something like this:
<!-- this file is src/views/DogsCRUDPage.vue -->
<template>
<general-crud-component
name="dogs"
backendurl="/dogs/"
>
<template slot="list">
<dogs-list-component />
</template>
<template slot="edit">
<dogs-edit-field v-model="... oops .." />
</template>
</general-crud-component>
</template>
<script>
export default {
name: "DogCRUDPage",
components: {
GeneralCrudComponent,
DogListComponent,
DogEditField,
},
}
</script>
This is nice because it matches the general syntax of all of my other VueJS pages and how I pass props and things to shared code. However, the problem is that GeneralCRUDComponent handles all of the mechanisms for checking if an object is edited, and therefor hiding or unhiding the save button, etc. Therefor it has the editable object in its data which will become the v-model for DogsEditField or any other that's passed to it. So it needs to pass this component a prop. So what I've done this:
// This file is src/utils/crud.js
import Vue from "vue"
const crudView = (listComponent, editComponent) => {
return Vue.component('CrudView', {
template: `
<v-row>
<list-component />
<v-form>
<edit-component v-model="obj" />
</v-form>
</v-row>
`,
components: {
ListComponent: listComponent,
EditComponent: editComponent,
},
data() {
return {
obj: {},
}
},
})
}
export default crudView
This file has a ton of shared code not shown that is doing the nuts and bolts of editing, undo, saving, etc.
And then in my src/router/index.js
//import DogCRUDPage from "#/views/libs/DogCRUDPage"
import crudView from "#/utils/crud"
import DogListComponent from "#/components/DogListComponent"
import DogEditField from "#/components/design/DogEditField"
const DogCRUDPage = crudView(DesignBasisList, DesignBasis)
Vue.use(VueRouter);
export default new VueRouter({
routes: [
{
path: "/dog",
name: "dog",
component: DogCRUDPage,
},
})
This is working, but there are issues I don't love about it. For one, I needed to enable runtimecompiler for my project which increases the size of the payload to the browser. I need to import the list and edit components to my router instead of just the page for every single object I have a page for. The syntax for this new shared component is totally different from the template syntax all the other pages use. It puts all of my page creation into the router/index.js file instead of just layed out as files in src/views which I am used to in Vue.
What is the idiomatic way to accomplish this? Am I on the right track here? I'm happy to do this, it's working, if this really is how we do this in Vue. But I would love to explore alternatives if the Vue community does something differently. I guess I'm mostly looking for the idiomatic Vue way to accomplish this. Thanks a bunch.
How about this:
DogsPage.vue
<template>
<CrudView
:editComponent="DogsEdit"
:listComponent="DogsList"
></CrudView>
</template>
<script>
import DogsEdit from '#/components/DogsEdit.vue'
import DogsList from '#/components/DogsList.vue'
import CrudView from '#/components/CrudView.vue'
export default {
components: { CrudView },
data() {
return { DogsEdit, DogsList }
}
}
</script>
CrudView.vue
<template>
<div>
<component :is="listComponent"></component>
<component :is="editComponent" v-model="obj"></component>
</div>
</template>
<script>
export default {
props: {
editComponent: Object,
listComponent: Object
},
data() {
return {
obj: {}
}
}
}
</script>

Unique EventBus for multiple instances of the same Vue application

I have a Vue application (which is basically a video player) that uses EventBus to communicate across components which normally cannot communicate easily. This worked perfectly when I was developing the video player, but now I have bundled it using Rollup, and when I try to put multiple instances of the video player on the same page, any Event one instance sends will also be picked up by all the other instances of the application.
Now in hindsight I understand why people don't seem to like the EventBus, but I can't find a great solution to either improve or replace it. I can't name the EventBus instances dynamically, because then the rest of my application won't be informed about the new name. I can't even use something like a videoId in my EventBus listeners to control the uniqueness, because then I will encounter the same problem if a video is on the same page more than once.
Some posts suggest VueX, but my app doesn't need to be stateful and it doesn't seem like a replacement for Event and listeners (though I could be wrong on that.) It seems like a lot of overhead for more functionality than I need. Again, I could be wrong.
I tried to remove as much irrelevant code as possible, but to give an idea of how I implemented my EventBus:
event-bus.js:
import Vue from 'vue';
const EventBus = new Vue();
export default EventBus;
MediaPlayer.vue:
<template>
<div>
<div>
<div id='media-player'>
<end-screen v-if="videoIsFinished"/>
<tap-video
ref="tapVideoRef"
:source='sourceUrl'
:videoId='videoId'
#videoEndHandler='videoEndHandler'
>
</tap-video>
<div id="control-bar-container">
<transition name="slide-fade">
<div v-show='(showControls || !playing)' >
<media-controls
:playing="playing"
:videoLength="videoLength"
/>
</div>
</transition>
</div>
</div>
</div>
</div>
</template>
<script>
import TapVideo from './TapVideo.vue';
import EventBus from './event-bus';
export default {
data (){
return{
playing: false,
showControls: false,
videoLength: 0,
tapVideoRef: null
}
},
mounted() {
this.tapVideoRef = this.$refs.tapVideoRef;
EventBus.$on('videoLoaded', videoLength => {
this.videoLength = videoLength;
});
EventBus.$on('playStateChange', playing => {
this.onPlayStateChange(playing);
});
},
beforeDestroy() {
EventBus.$off(['playStateChange','closeDrawer']);
},
props: ['sourceUrl', 'platformType', 'videoId'],
}
</script>
In case anyone comes across this problem, I found a solution that works well for me. Thanks to #ChunbinLi for pointing me in the right direction - their solution did work, but passing props everywhere is a bit clunky.
Instead, I used the Provide/Inject pattern supported by Vue: https://v3.vuejs.org/guide/component-provide-inject.html
Some minimal relevant code:
The highest level Grandparent will provide the EventBus,
Grandparent.vue
export default {
provide() {
return {
eventBus: new Vue()
}
}
}
Then any descendant has the ability to Inject that bus,
Parent.vue
export default {
inject: ['eventBus'],
created() {
this.eventBus.$emit('neededEvent')
}
}
Child.vue
export default {
inject: ['eventBus'],
created(){
this.eventBus.$on('neededEvent', ()=>{console.log('Event triggered!')});
}
}
This is still a GLOBAL EventBus, so directionality of events and parental relationship is easy, as long as all components communicating are descendants of the component which "Provided" the bus.

Lazy loading a specific component in Vue.js

I just make it quick:
In normal loading of a component (for example "Picker" component from emoji-mart-vue package) this syntax should be used:
import {Picker} from "./emoji-mart-vue";
Vue.component("picker", Picker);
And it works just fine.
But when I try to lazy load this component I'm not sure exactly what code to write. Note that the following syntax which is written in the documentation doesn't work in this case as expected:
let Picker = ()=>import("./emoji-mart-vue");
The problem, I'm assuming, is that you're using
let Picker = ()=>import("./emoji-mart-vue");
Vue.component("picker", Picker);
to be clear, you're defining the component directly before the promise is resolved, so the component is assigned a promise, rather than a resolved component.
The solution is not clear and depends on "what are you trying to accomplish"
One possible solution:
import("./emoji-mart-vue")
.then(Picker=> {
Vue.component("picker", Picker);
// other vue stuff
});
This will (block) wait until the component is loaded before loading rest of the application. IMHO, this defeats the purpose of code-spliting, since the application overall load time is likely worse.
Another option
is to load it on the component that needs it.
so you could put this into the .vue sfc that uses it:
export default {
components: {
Picker: () => import("./emoji-mart-vue")
}
};
But this would make it so that all components that use it need to have this added, however, this may have benefits in code-splitting, since it will load only when needed the 1st time, so if user lands on a route that doesn't require it, the load time will be faster.
A witty way to solve it
can be done by using a placeholder component while the other one loads
const Picker= () => ({
component: import("./emoji-mart-vue"),
loading: SomeLoadingComponent
});
Vue.component("picker", Picker);
or if you don't want to load another component (SomeLoadingComponent), you can pass a template like this
const Picker= () => ({
component: import("./emoji-mart-vue"),
loading: {template:`<h1>LOADING</h1>`},
});
Vue.component("picker", Picker);
In PluginPicker.vue you do this:
<template>
<picker />
</template>
<script>
import { Picker } from "./emoji-mart-vue";
export default {
components: { Picker }
}
</script>
And in comp where you like to lazy load do this:
The component will not be loaded until it is required in the DOM, which is as soon as the v-if value changes to true.
<template>
<div>
<plugin-picker v-if="compLoaded" />
</div>
</template>
<script>
const PluginPicker = () => import('./PluginPicker.vue')
export default {
data() = { return { compLoaded: false }}
components: { PluginPicker }
}
// Another syntax
export default {
components: {
PluginPicker: () => import('./PluginPicker.vue')
}
}
</script>

Vue dynamic components - watch for mounted

I'm using Webpack dynamic imports and Vue dynamic components to lazy-load a rather large Vue markdown-parsing component.
Now, I want to add syntax highlighting with Prism.js. I'm currently using the mounted() lifecycle hook of the parent component to install syntax highlighting, but this is only working some of the time, since the syntax highlighting depends on the Markdown component to be loaded first (when I manually execute Prism.highlightAll() from the console after page load, it works every time).
Relevant source code:
<template>
<vue-markdown>
# Hello
```javascript
import { hello } from "world"
```
</vue-markdown>
</template>
<script>
export default {
components: {
"vue-markdown": () => import("vue-markdown/src/VueMarkdown"),
},
mounted() {
import("prismjs/themes/prism-tomorrow.css")
.then(() => import("prismjs").then(p => Prism.highlightAll()))
}
}
</script>
So how do I wait for a dynamic component to load? I almost want something like this:
<vue-markdown v-on:mounted="syntaxHighlighting()"></vue-markdown>
I solved the problem by creating my own component which extends the VueMarkdown component, but with a mounted() hook that activates syntax highlighting. It looks like this:
<script>
import VueMarkdown from "vue-markdown/src/VueMarkdown"
import "prismjs/themes/prism-tomorrow.css"
import Prism from "prismjs"
export default {
extends: VueMarkdown,
mounted() {
Prism.highlightAll()
}
}
</script>
Then, I dynamically import this component into the parent component.
Not sure if this is the best solution, though...

Observe Vue.js store boolean change inside component

I have a vue.js store that holds a boolean value and I need to detect when this changes inside a component. When userEnabled changes I need to call a method inside the component. I don't need to compute a new value.
My question is should I be using 'watch' for this or is there a more efficient way? If so, should I be configuring the watch inside 'mounted' and what about unwatch?
store.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export const store = new Vuex.Store({
state : {
userEnabled: false,
}
});
user.vue
<template>
<div>
<div id="user"></div>
</div>
</template>
<script>
export default {
watch: {
'this.$store.userEnabled': function() {
console.log('test');
}
},
mounted: function() {
this.$store.watch( state => state.userEnabled, (newValue, oldValue) => {
// call function!
});
}
}
</script>
If the functionality is to be executed only inside the component, watch is the way to go. You might map userEnabled to a computed prop of the component to improve readability, but that's optional.
Manually calling $store.watch in mounted mainly gives you the option to store the returned unwatch function and stop watching the property at an arbitrary point. However, if you want to watch the property as long as the component exists anyway, this adds no benefits.
Finally, if the desired functionality should be run whenever userEnabled is changed, regardless of specific components handling the change, a better approach might be to move that code into the Vuex mutation function which changes the value of userEnabled in the store.