vuejs - Async AND Dynamic component - vuejs2

What I want to achive is to combine vuejs's "Async Components" and its "Dynamic Components" (https://v2.vuejs.org/v2/guide/components-dynamic-async.html) to get a flexible way of using dynamic components.
Let's say, I have the following component:
<template>
<div>
some stuff..
<component :is="type"></component>
some stuff...
</div>
</template>
<script>
export default {
name: "mycomponent",
props: {
type: {}
}
}
</script>
I would use it like so:
<mycomponent type="myinput"></mycomponent>
To make it work, I would need to load the myinput component in the mycomponent and I will do it aync:
components: {
MyInput: () => import("./myinput")
}
That works!
The problem now is, that I want to make it dynamic and use it like so:
<mycomponent type="myinput"></mycomponent>
<mycomponent type="myselect"></mycomponent>
<mycomponent type="mytextarea"></mycomponent>
...
To get this up and running, I would need to import all of this components in the mycomponent, which is obviously not an good idea. Global import is as well, not the way to go.
This was just an example, but the real use case for this is getting an array of type's from an api and dynamically render a form with it.

So I have came up with I idea what I could not try yet.
Lets say you have a component for different types of inputs for the sake of simplicity you name them all with an 'Input' prefix.
As long as everyone follows the naming rules you can register all of these component globally.
For example in a BaseInputs.js
const components = require.context(path, subfolder, regexForInputComponentFileNames)
https://webpack.js.org/guides/dependency-management/#require-context
components.keys().forEach(element => {
const componentName = element.replace(/*Everything that is not the component name like .vue*/)
Vue.component(componentName, () => import(path + componentName)
})
Then you just import 'path/to/BaseInputs' at the beginning of your application.

Related

Call a function from another component using composition API

Below is a code for a header and a body (different components). How do you call the continue function of the component 2 and pass a parameter when you are inside component 1, using composition API way...
Component 2:
export default {
setup() {
const continue = (parameter1) => {
// do stuff here
}
return {
continue
}
}
}
One way to solve this is to use events for parent-to-child communication, combined with template refs, from which the child method can be directly called.
In ComponentB.vue, emit an event (e.g., named continue-event) that the parent can listen to. We use a button-click to trigger the event for this example:
<!-- ComponentB.vue -->
<script>
export default {
emits: ['continue-event'],
}
</script>
<template>
<h2>Component B</h2>
<button #click="$emit('continue-event', 'hi')">Trigger continue event</button>
</template>
In the parent, use a template ref on ComponentA.vue to get a reference to it in JavaScript, and create a function (e.g., named myCompContinue) to call the child component's continueFn directly.
<!-- Parent.vue -->
<script>
import { ref } from 'vue'
export default {
⋮
setup() {
const myComp = ref()
const myCompContinue = () => myComp.value.continueFn('hello from B')
return {
myComp,
myCompContinue,
}
},
}
</script>
<template>
<ComponentA ref="myComp" />
⋮
</template>
To link the two components in the parent, use the v-on directive (or # shorthand) to set myCompContinue as the event handler for ComponentB.vue's continue-event, emitted in step 1:
<template>
⋮
<ComponentB #continue-event="myCompContinue" />
</template>
demo
Note: Components written with the Options API (as you are using in the question) by default have their methods and props exposed via template refs, but this is not true for components written with <script setup>. In that case, defineExpose would be needed to expose the desired methods.
It seems like composition API makes everything a lot harder to do with basically no or little benefit. I've recently been porting my app to composition API and it required complete re-architecture, loads of new code and complexity. I really don't get it, seems just like a massive waste of time. Does anyone really think this direction is good ?
Here is how I solved it with script setup syntax:
Parent:
<script setup>
import { ref } from 'vue';
const childComponent = ref(null);
const onSave = () => {
childComponent.value.saveThing();
};
</script>
<template>
<div>
<ChildComponent ref="childComponent" />
<SomeOtherComponent
#save-thing="onSave"
/>
</div>
</template>
ChildComponent:
<script setup>
const saveThing = () => {
// do stuff
};
defineExpose({
saveThing,
});
</script>
It doesn't work without defineExpose. Besides that, the only trick is to create a ref on the component in which you are trying to call a function.
In the above code, it doesn't work to do #save-thing="childComponent.saveThing", and it appears the reason is that the ref is null when the component initially mounts.

Vue Test Utils: how to pass Vuelidate validation rules to child components?

while trying to write a component test by using vue test utils, testing interaction between child components and stuff, I am stuck due to usage of Vuelidate from child components. Below is an example simplified:
// parent component code
<template>
<div>
<childA />
</div>
</template>
//childA code
<template>
<input v-model="value" />
</template>
<script>
...
validations: {
value: {
required
}
}
...
</script>
// parent component test
...
const wrapper = mount(MyParentComponent, {
...,
components: {
childA,
},
validations: {
value: required
},
...
})
I have tried to find a solution out there that I could mount (note here that I WANT to mount also the child components, so shallow-mount is not what I look for) the child component, with it's respective Vuelidate validation rules, but I still haven't found any solution.
Instead, my test gives me errors like:
Cannot read property `value` of undefined
which makes sense, since the test cannot access the child component's $v instance.
Has anyone achieved it so far?
For answering your question and after i've did some test i believe you missed the data part inside your mount
mount: render child components
shallowMount: doesn't render child components
MyParentComponent need to have in the options the structure of you're child component so this is why he is returning the error
And i saw that you're passing the import of your component directly but don't forget that your test folder is outside of your src folder
import ChildA from "#/components/ChildA";
will not work instead i propose to use absolute path directly to import your child component or use a configuration to resolve them
const wrapper = mount(MyParentComponent, {
data() {
return {
value: null
}
},
components: {
ChildA: () => import('../../src/components/ChildA'),
},
validations: {
value: required
},
})

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>

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 add a component with JS

Can I create/mount a VUE component calling a JS function in a fullyloaded page?
As a async call? Something like:
function getComponent(obj){
return <component parameters="obj"></component >;
}
Can I create/mount a VUE component calling a JS function in a fullyloaded page?
I think you want to load/mount the component in a certain condition and not on page initialization.
If that the case then you can take advantage of lazy-loading and dynamic components:
<template>
//...
<button #click="activateComponent">Activate component</button>
<component :is="dynamicComponent" />
//...
</template>
<script>
export default {
components: {
MyCmp: () => import('./MyCmp.vue') //lazy loading
},
data: () => ({
dynamicComponent: null
}),
methods: {
activateComponent () {
this.dynamicComponent = 'MyCmp'
}
}
}
</script>
I think what you are looking for is Vue dynamic components. What that allows you is to choose which component to load after the original component has loaded. You can do that whenever and however you want to, aka in an async way. You can read more in the Vue guides. All the code you need is there so won't repost it here again.