I try to create a highly dynamic wizard as a component in Vue. It contains out of three components: the Wizard component itself, a Step component and a single form "MyForm" component. The form can be in edit mode or in read only mode depending on the current step of the wizard.
After some trial and error I finally managed to create such a component and it works. One problem that I struggled to solve was to pass the information if the form is in edit mode or not from the Step component to the child form component.
MyForm.vue
<template>
<form>
<div v-if="inEditMode"><i>Form is in edit mode</i></div>
<div v-else><i>Form is in read only mode</i></div>
</form>
</template>
<script>
import Vue from "vue";
export default Vue.extend({
props: ["inEditMode"]
// mixins: [wizardStepMixin],
});
</script>
Wizard.vue
<Step>
<MyForm/>
</Step>
Step.vue
<slot :isInEditMode="true"/>
Passing/setting a prop to a slot like I did above did not work (prop did not change).
My solution to set the prop isInEdit on the MyForm is to call a function prepareSlot in the Step component before mounting and updating the Step.
prepareSlot() {
this.$slots.default.forEach(element => {
if (!element.data) return
element.componentOptions.propsData = {
...element.componentOptions.propsData,
inEditMode: this.stepNr === this.currentStep
}
})
}
You can find the complete project on https://codesandbox.io/embed/mzr10wzk0j.
Is there a better way to archive that? Is it safe to do it that way?
Related
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>
Component 1:-
<template>
<blur :isData="isData">
<!-- logic/implementation of component 1 -->
<div>
</div>
</blur>
<template>
<script>
import blur from "../shared/Blur";
name: "component-1",
components: {
blur,
},
</script>
Just like this component1.vue, I have multiple components which are using blur component. Is it possible that instead of writing and importing blur in every single component, I can make some base class that can transfer the blur functionality in every single component in the folder. Can something like this be achieved in vue ?
With Vue.component you can create globally registered components:
Vue.component('my-component-name', {
// ... options ...
})
Find out more here
As simplified below, my app has a template with a custom component.
The data is passed from Template A to custom component as props (":list")
Template A:
<template>
...
<custom-component
v-for="list in listGroup"
:key="list.id_list"
:list="list"
/>
</template>
<script>
export default {
data() {
return {
listGroup: []
};
},
components: {
'custom-component':require("...").default
}
</script>
The custom component
<template>
...
</template>
<script>
export default {
props:["list];
...
}
</script>
Problem to solve:
A new item is added to the list sent as props.
I need the list (:list="list") to be dynamically updated so that the props in the custom component automatically reflect that update.
Thanks.
There are two ways to achieve that one way is to use a state management library(Vuex is recommended) the other is to use events.
Here is an example of using events:
create a file event-bus.js with the following content
import Vue from "vue";
export const EventBus = new Vue();
then in your component where you want to update list use this EventBus.$emit('eventName', data);
remember to import event-bus file
the listen to the event in the other component
EventBus.$on('eventName', function (details) {
//update list here
});
I'm using vue-context to modify the default context menu, which I call from a component but when I try to interact with it I get an error, here's my code
<!-- Main -->
<p #contextmenu.prevent="$refs.menu.open">test</p>
<Context reference="menu" />
<script>
import Context from './map/context.vue'
export default { components: { Context } }
</script>
<!-- Component -->
<vue-context :ref="reference" :close-on-click="true" :close-on-scroll="true">
<li>
<a>
Do something
</a>
</li>
<li>
<a>
Do something else
</a>
</li>
</vue-context>
<script>
import VueContext from 'vue-context'
import 'vue-context/dist/css/vue-context.css'
export default {
props: ['reference'],
components: {
VueContext
}
}
</script>
When I right click on the page, I get the error _vm.$refs.menu.open is not a function
Because you wrapped VueContext inside a separate vue instance, the outer Vue instance (the one containing the #contextmenu call) does not have a $refs.menu. You can access the child's $refs by setting a ref on the child itself:
<p #contextmenu.prevent="$refs.wrapper.$refs.menu.open">test</p>
<Context ref="wrapper" reference="menu" />
See it working here.
I'd also argue you shouldn't pass the string 'menu' from parent but specify it directly inside <Context>'s template.
Dynamic props only make sense when you have some benefit from them changing value, which is clearly not the case here. You need that child ref to always be 'menu' so you can access its methods:
<p #contextmenu.prevent="$refs.wrapper.$refs.menu.open">test</p>
<Context ref="wrapper" />
Context.vue:
<template>
<vue-context ref="menu" :close-on-click="true" :close-on-scroll="true">
<li><a>Do something</a></li>
<li><a>Do something else</a></li>
</vue-context>
</template>
<script>
import VueContext from "vue-context";
import "vue-context/dist/css/vue-context.css";
export default { components: { VueContext } };
</script>
To summarize: $refs is a unified mechanism allowing you to access template elements, whether they're DOM elements or Vue instances.
Each component only contains its own $refs. To access the $refs of one of its children, you have to give the child a ref in parent scope and use $refs on that particular reference.
This actually makes a lot of sense in a scenario where you want multiple context menus with different contents for different items in your parent component (although you're probably better off simply passing down the menu items and their actions dynamically to a single context menu instance).
I have just started with Vue and am having an issue where the component isn't rendering for me.
<template>
<div>
<GalleryCollectionBlueBottles />
</div>
</template>
<script>
import GalleryCollectionBlueBottles from '#/components/collections/GalleryCollectionBlueBottles.vue'
export default {
name: 'GalleryCollections'
}
</script>
When I inspect the page in a browser all I see is an element with the component name, not the contents of the component as usual.
The component above is called GalleryCollections and the component I'm importing is called GalleryCollectionBlueBottles.
Hope someone can help, also hoping this is something simple I've overlooked :)
You need to also declare the component inside your script tag within the keyword components{ ... } like;
<script>
import GalleryCollectionBlueBottles from '#/components/collections/GalleryCollectionBlueBottles.vue'
export default {
name: 'GalleryCollections',
components: {
GalleryCollectionBlueBottles
}
}
</script>