Get imported components programmatically - vuejs2

I would like to be able to access the root components objects in a component. For instance, In the following component, I import and register two child components, now how do I get the components property in the mounted hook?
<template>
<div>
<SomeComponent />
<AnotherComponent />
</div>
</template>
<script>
import SomeComponent from '#/components/home/SomeComponent';
import AnotherComponent from '#/components/home/AnotherComponent';
export default {
components: { SomeComponent, AnotherComponent }, // I need to access this from the mounted hook
mounted() {
console.log('registered components:', Object.keys(componentsObject))
}
}
</script>
I have tried this.components, but that returns undefined, and then I tried just logging this to see what's available, but there's no components property, or anything that resembles what I'm after, so I dunno if there's some other way I could access it?
UPDATE: The reason that I want to do this is that I'm essentially creating a component slideshow, so I want to do Object.keys(components).length to let me programmatically determine the amount of slides, without the need for a amount variable that I have to manually update every time I create another slide.

There is a better approach to this problem. You may insert a property called slidesData in your data() method in your parent component.
data() {
slidesData: [
{ id: 1, title: 'Sample Slide title', description: 'Lorem ipsum', ... },
{ id: 2, title: 'Sample Slide 2 title', description: 'Dolor sit amet', ... },
]
}
and with this sample data, you can create a reusable component called SlideComponent. You can then "v-for" the data and pass it to the SlideComponent and there, you can format the data to your liking.
Going back to your problem, with this approach, you don't need to access the "Slide" component itself, you just have to access the slideData instead. This is a "Vue way" to solve the problem.

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>

Get v-slot's value

I got an component like this:
<template>
<Popover v-slot="{ open }" :ref="`${name}-parent`">
<div>
<PopoverButton :ref="name">
<div>
<slot name="buttoncontent"></slot>
</div>
<ChevronDownIcon/>
</PopoverButton>
</div>
<transition>
<PopoverPanel>
<slot name="popovercontent"></slot>
</PopoverPanel>
</transition>
</Popover>
</template>
<script>
import {Popover, PopoverButton, PopoverPanel} from '#headlessui/vue'
import {ChevronDownIcon} from '#heroicons/vue/solid'
export default {
name: 'PopoverMenu',
props: {
name: {
type: String,
},
title: {
type: String,
default: ''
},
},
components: {
Popover,
PopoverButton,
PopoverPanel,
ChevronDownIcon,
},
setup () {
return {}
},
watch: {
'$route' () {
// this.$refs[this.name] ... do fancy stuff on route change here
},
},
mounted () {
console.log(this.$refs[`${this.name}-parent`])
}
}
</script>
Now I'd like to change the open state depending on the change of the route. Ergo: If the user clicks a link the popover should close.
The Popover, PopoverButton and PopoverPanel are provided by headlessui and only offer the open slot just within the component. My idea was to access the open property and change it manually.
My idea was to access the open property
Accessing the open property is easy, in a way you already have it in your template. If you want to hold on to it (e.g. keep a reference to open so that you can use it some time later), you can convert your slot content into a component and receive open as the props.
and change it manually.
However, this is prohibited. The moment you try to mutate the open you will get a warning saying it is readonly. In general, properties are always readonly, this is enforced by vue. Scoped slots are just anonymous components very similar to lambda functions, and the open variable is just one of the properties for the slot.
Ideally the library you are using should expose not just an open state but also two more methods (open() and close()) to the slot. Unfortunately, not all libraries are built that thoughtful.
You can try to move the focus to some other elements when your route changes and see if that can close the popover. If not, you can manually implement the vue wrapper for popover. This is something I would do btw, for the simple use cases (i.e. pop some panel), it is trivial to implement using popover.

Parent component updates a child component v-for list, the new list is not rendered in the viewport (vue.js)

My app structure is as follows. The Parent app has an editable form, with a child component list placed at the side. The child component is a list of students in a table.
I'm trying to update a child component list. The child component uses a 'v-for', the list is generated through a web service call using Axios.
In my parent component, I am editing a students name, but the students new name is not reflected in the List that I have on screen.
Example:
Notice on the left the parent form has the updated name now stored in the DB. However, the list (child component) remains unchanged.
I have tried a few things such as using props, ref etc. I am starting to think that my app architecture may be incorrect.
Does anyone know how I might go about solving this issue.
Sections of the code below. You may understand that I am a novice at Vue.
Assistance much appreciated.
// Child component
<component>
..
<tr v-for="student in Students.slice().reverse()" :key="student._id">
..
</component>
export default {
env: '',
// list: this.Students,
props: {
inputData: Boolean,
},
data() {
return {
Students: [],
};
},
created() {
// AXIOS web call...
},
};
// Parent component
import List from "./components/students/listTerms";
export default {
name: "App",
components: {
Header,
Footer,
List,
},
};
// Implementation
<List />
I think that it is better to use vuex for this case and make changes with mutations. Because when you change an object in the data array, it is not overwritten. reactivity doesn't work that way read more about it here
If your list component doesn't make a fresh API call each time the form is submitted, the data won't reflect the changes. However, making a separate request each time doesn't make much sense when the component is a child of the form component.
To utilise Vue's reactivity and prevent overhead, it would be best to use props.
As a simplified example:
// Child component
<template>
...
<tr v-for="student in [...students].reverse()" :key="student._id">
...
</template>
<script>
export default {
props: {
students: Array,
},
};
</script>
// Parent component
<template>
<div>
<form #submit.prevent="submitForm">
<input v-model="studentData.name" />
<input type="submit" value="SUBMIT" />
</form>
<List :students="students" />
</div>
</template>
<script>
import List from "./components/students/listTerms";
export default {
name: "App",
components: {
List,
},
data() {
return {
students: [],
studentData: {
name: ''
}
}
},
methods: {
submitForm() {
this.$axios.post('/endpoint', this.studentData).then(() => {
this.students.push({ ...this.studentData });
}).catch(err => {
console.error(err)
})
}
}
};
</script>
Working example.
This ensures data that isn't stored successfully won't be displayed and data that is stored successfully reflects in the child component.

Pass on the form data from one component to another

I'm trying to get information from a form via button which is not part of the component where the form is present.
Basically the current structure looks like this:
->Component A (button is here)
--> Inside component A I import another component (B) where the form is located
So I am trying to pass the information from component B, using button which is located in component A.
The reason why component B is not just part of component A (would make things relatively easier) is because it's used in several other places.
In component A I have:
`<ComponentB></ComponentB>
<div class="padding">
<button #onSubmit="onSubmit" class="add-button btn btn-md float-
right"
type="submit">Add items from component B</button>
</div>`
This is my data located in export default in component A
import ComponentB from './ComponentB';
import {mapActions} from 'vuex';
export default {
name: "ComponentA",
components:{
ComponentB
},
data(){
return{
firstname: '',
surname: '',
dateOfBirth: '',
isPolicyHolder: '',
drivingLicence:
licenceNumber: '',
countryLicenced: '',
coverEffectiveFrom: '',
coverEffectiveTo: '',
}
}
I also have vuex state where I'm trying to pass the information to on button click.
In Component A this is the method:
`methods:{
...mapActions(['addDriver']),
onSubmit(e){
e.preventDefault();
this.addDriver( this.firstname,
this.surname,
this.dateOfBirth,
this.isPolicyHolder,
this.licenceNumber,
this.countryLicenced,
this.coverEffectiveFrom,
this.coverEffectiveTo)
}
}`
Component B just hold few text boxes and date pickers.
So to summarise, on button click it should take whatever info is typed in component B to component A data.
You have to create a Vuex state to manage the variables. And then, create the actions and mutates to modify the state. After that, you can just get what you want in component B.
const store = new Vuex.Store({
state: {
// your states
},
mutations: {
// change state here sync
},
actions: {
// call mutations here, but you can do this async as axios methods etc..
},
getters: {
// need this only if state still needs some calculation.
}
}
So after you create all this. You'll be able to call the actions like above
this.$store.dispatch('ACTION')
and get the state in component B
this.$store.state.stateYouWant
or with getters
this.$store.getters.getterYouCreated

Setting props of child component in vue

I'm following the example here of using a Vue template as a Kendo UI template in their components:
https://www.telerik.com/kendo-vue-ui/components/framework/vue-templates/
The example isn't very clear on how to supply properties to components that are rendered with this method (as opposed to rendering right in the template). I need to supply a single value determined in the parent to all instances of this child component, and I also need to subscribe to emitted events from the child component. My assumption is that there's an overload to Vue.component() that lets me access this functionality?
Edit:
Specifically what I am looking for is a way to have a header template for each column created from a Vue component. I need each column's template to receive data from the parent so I know how to construct it, and I also need each column's template to report an event back to the parent.
I think the key point is Step 3 in the link you attached (Kendo Vue Template Usage). (Never touch Kendo Before, if anything wrong, correct me, thanks.)
First, please open this Vue kendo Sandbox, you will find one dropdownlist then each option is one button plus one text. If you click the button, it will call one method in MyTemplate.vue and another Method at DropDownStyle.vue, then its background of each option is blue which passed from DropDownStyle.vue.
Kendo will bind this function of Step 3 to its attribute=template, then fisrt parameter (and only one) is each element of the data-source.
Then this function need to return one object including template and templateArgs, then Kendo construct it.
So my solution is add your function/callback/styles into templateArgs, then do what you need at MyTemplate.vue.
Below is the codes extended from Step 3.
methods: {
getMyTemplate: function (e) {
// parameter=e: it is the value of each element of the dropdown
e.callback = this.eventCallback
e.styles="background-color:blue"
return {
template: MyTemplate,
templateArgs: e
}
},
eventCallback: function (data) {
console.log(this.dropdowns)
}
}
Below is MyTemplate.vue.
<template>
<span :style="templateArgs.styles">
<button #click="buttonClick();templateArgs.callback()">{{templateArgs.value}}</button>
{{templateArgs.text}}
</span>
</template>
<script>
export default {
name: 'template1',
methods: {
buttonClick: function (e) {
console.log('props',this.templateArgs.styles)
}
},
data () {
return {
templateArgs: {
callback:function(){
console.log('Test')
},
styles:''
}
}
}
}
</script>
Very odd design choice in terms of passing the template in like they do. Avoiding the KendoUI and focusing on VueJS methods - could you use provide/inject? Providing the value in the parent and injecting in any of the children?
Also a plugin could be created to keep track of events or values you want available to all components in the application. In essence the plugin would be a service. A singleton object that is only instantiated once.
The documentation is indeed lacking. I agree with you on that. I took a different approach with templating for Kendo UI component and got this working: https://codesandbox.io/s/github/ariellephan/vue-kendoui-template
To start, I have this dropdown component that utilizes Kendo dropdown list component:
<template>
<div>
<p>Style with template {{template}}</p>
<kendo-dropdownlist
:template="template"
:headerTemplate="headerTemplate"
:data-source="dataSourceArray"
:data-text-field="'text'"
:data-value-field="'value'"
:filter="'contains'">
</kendo-dropdownlist>
</div>
</template>
<script>
export default {
name: "Dropdown",
props: ["dataSourceArray", "template", "headerTemplate"],
data() {
return {
value: "Click Me",
text: "I'm in Template template"
};
}
};
</script>
To render different styles/templates, I parsed in props from the parent component. In this case, DropdownStyles
<template>
<div id="DropdownStyles">
<h1>KendoUI dropdown instances with different templates</h1>
<Dropdown
v-for="dropdown in dropdowns"
v-bind:key="dropdown.id"
v-bind:title="dropdown.title"
v-bind:data-source-array="dropdown.dataSourceArray"
v-bind:template="dropdown.template"
v-bind:headerTemplate="dropdown.headerTemplate"
></Dropdown>
</div>
</template>
<script>
import Dropdown from "./Dropdown";
import DropdownTemplate from "./DropdownTemplate";
export default {
name: "DropdownStyles",
components: { Dropdown },
data() {
return {
dropdowns: [
{
id: 1,
title: "x style",
dataSourceArray: [
"Football",
"Tennis",
"Basketball",
"Baseball",
"Cricket",
"Field Hockey",
"Volleyball"
],
template: `<strong class="custom-dropdown">x #:data#</strong>`,
headerTemplate: DropdownTemplate.template
},
{
id: 2,
title: "+ style",
dataSourceArray: [
"Football",
"Tennis",
"Basketball",
"Baseball",
"Cricket",
"Field Hockey",
"Volleyball"
],
template: `<strong class="custom-dropdown">+ #:data#</strong>`,
headerTemplate: `<div><h3 style="padding-left:10px;">Sports 2</h3></div>`
}
]
};
}
};
</script>
You can move the template into its own file or function. For example, the first drop down is using DropdownTemplate for its headerTemplate:
DropdownTemplate.vue
<script>
export default {
name: "DropdownTemplate",
props: ["header"],
template: `<div>
<div><h3>Sports 1</h3></div>
</div>`,
data() {
return {};
}
};
</script>
<style scoped>
h3 {
padding-left: 10px;
}
</style>