How can i just have one Component instead of different components in the context of Vuejs - vue.js

I am new to Vuejs, I am using 4 different components, where in every component i have called the API. Here 4 different components are all same except their contents.
Now, i want to make my code effective, so i want to have a single component, the reason why i created different components is my another App.vue component has 4 different buttons, so whenever you click any of it, it will open the respective component.
But now i want to have only one component instead of four different components, and whenever the buttons in App.vue component is clicked it should open the exact content in single component(instead of 4 components).
Please do help me with this, by sharing your ideas and if any examples.

In this context, you can use props, which is a way of passing data to "child" components.
https://v2.vuejs.org/v2/guide/components-props.html
Example
App.vue
<template>
<div id="app">
<SingleComponent :button="button" />
</div>
</template>
<script>
import SingleComponent from "#/components/SingleComponent.vue";
// #/ means src/
export default {
name: "App",
components: {
SingleComponent,
},
data: () => ({
button: 2,
}),
};
</script>
SingleComponent.vue
<template>
<div>
<button v-if="button === 0">...</button>
<button v-else-if="button === 1">...</button>
<button v-else-if="button === 2">...</button>
<button v-else-if="button === 3">...</button>
</div>
</template>
<script>
export default {
name: "SingleComponent",
props: {
button: {
type: Number,
required: true,
},
},
};
</script>
You should also take a look at slots, it is very important in Vue.js and that could also solve your problem.
https://v2.vuejs.org/v2/guide/components-slots.html

Related

Vue 3: Each button click, generate new component and append as child in the target element

I have been searching for a simple solution to generate components in a Vue 3 app programmatically. So far, I've used defineComponent to extend the div component and attach it to the main component via createApp and mount:
Main Component
<template>
<button type="button" v-on:click="addDiv"></button>
<div id="app-main">
</div>
</template>
<script>
import {defineComponent, createApp} from 'vue'
import Div from './components/Div.vue'
export default{
name: 'App',
methods: {
addDiv: () => {
let newDiv = defineComponent({extends: Div});
createApp(newDiv).mount("#app-main");
}
}
}
</script>
Div Component:
<template>
<div>This is a div</div>
</template>
<script>
export default {
name: 'Div'
}
</script>
My issue with this is that mount replaces everything in the target element. If you click the button 3 times, only 1 div appears instead of 3. I need a method where the code appends the component as a child in the target element allowing me to create as many div components as I want. Thanks in advance!
Don't use mount as that is designed to mount a new Vue instance. What you want to use is the <component :is="component_name"> feature and possibly have an array or other data structure containing the list of components you wish to have rendered.
For instance, consider the following simple example:
Vue.component('my-component', {
// component config goes here, omitting for simplicity.
});
new Vue({
el: '#app',
data: {
elements: []
},
methods: {
addNewDiv() {
this.elements.push({type: 'my-component'});
}
}
});
<div id="app">
<component v-for="element in elements" :is="element.type"></component>
</div>
This will iterate over the elements array dynamically and will replace each instance of the <component> tag with whatever is defined by the array element.

Calling function inside child component without an event?

Currently trying to use a method belonging to the parent
<p class="message-date text-center">
{{ $emit('format_date_day_month_year_time', message.date) }}
</p>
However I am getting the error.
Converting circular structure to JSON
--> starting at object with constructor 'Object'
How can I call a function inside a child component that does not rely on an event? I apologize for asking such a simple question but everything I was able to find on google is using $emit and using an event.
$emit was designed to only trigger an event on the current instance of vue. Therefore, it is not possible to receive data from another component this way.
For your case, I would suggest to use Mixins especially if you need to use certain functions among multiple vue components.
Alternately, let the child component call the the parent through $emit then receive the result from the parent through a prop.
Your code could be something as follows:
Child component
<template>
<p class="message-date text-center">
{{ date }}
</p>
</template>
<script>
export default {
name: 'Child',
props: {
date: String,
},
mounted() {
this.$emit("format-date", message.date);
},
}
</script>
Parent component
<template>
<Child :date="childDate" #format-date="formatChildDate" />
</template>
<script>
import Child from '#/components/Child';
export default {
components: {
Child,
},
data: () => ({
childDate: '',
}),
methods: {
formatChildDate(date) {
this.childDate = this.formatDateDayMonthYearTime(date)
},
formatDateDayMonthYearTime(date) {
//return the formatted date
},
},
}
</script>
with $emit you call a function where the Parent can listento.
where you are using it i would suggest a computed prop of the function.
But back to your Question here is a example of emiting and listen.
//Parent
<template>
<MyComponent #childFunction="doSomethingInParent($event)"/>
</template>
//Child
<template>
<button #click="emitStuff">
</template>
.....
methods:{
emitStuff(){
this.$emit(childFunction, somedata)
}
with the event you can give Data informations to a Parentcomponent.

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.

Extending Vue.js SFC components

I haven't found a good resource on extending Vue.js components. In every project I've worked on, regardless of the UI component library that's used, there are application Base components which extend the UI library components to enforce company/application defaults and standards.
I'm trying to extend Vue-Multiselect: https://vue-multiselect.js.org/ which has about 30 props and 12 slots. The component I'm extending doesn't matter -- I only mention it because ideally I don't want to have to repeat 30 props and 12 slots in my implementation.
I simply want to make two changes to the behavior of the component:
Make disabled prop a bit smarter
The Vue-Multiselect component has a standard disabled prop which works as expected:
<Multiselect :disabled="isDisabled" ...>
In our application, we have global state in Vuex which determines if the application is read-only. What I want to avoid is requiring developers to pass this state to every form field:
<Multiselect :disabled="readOnly || isDisabled" ...>
<OtherComponent :disabled="readOnly || someOtherCondition" ...>
...
So the user of my base component should only need to be concerned about their local UI state which affect the disabled status:
<BaseCombo :disabled="!emailValid" ...>
This would handle the 90% case of form fields that are locked down when the application is read-only and I can use an additional prop for cases where we want to ignore the global read-only status.
<BaseCombo :disabled="!emailValid" :ignoreReadOnly="true" ...>
Provide defaults
Secondly, I simply want to override some of the default prop values. This post addresses the question of supplying defaults:
https://stackoverflow.com/a/52592047/695318
And this works perfectly until I tried to modify the behavior of the disabled prop I mentioned previously.
My attempt to solve this was to either wrap or extend the component. I'd really want to avoid redeclaring all of the props if possible.
<template>
<Multiselect
:disabled="myCustomDisabled"
:value="value"
#input="$emit('input', $event)"
:options="options"
:label="label"
:track-by="trackBy"
:placeholder="placeholder"
... repeat for all 30 options
<script>
import Multiselect from 'vue-multiselect'
export default {
name: "BaseCombo",
extends: Multiselect, // extend or simply wrap?
computed: {
myCustomDisabled() {
this.props.disabled || ... use disabled from Vuex state
}
},
props: {
disabled: Boolean,
placeholder: {
type: String,
default: 'My Default Value',
},
... repeat for all props
The problem I ran into is I don't know how to handle the slots. The user of this BaseCombo should still be able to use all 12 slots in the VueMultiselect component.
Is there a better solution for extending components?
You can use this.$props to access props defined in the props attribute. Similarly you can access attributes (things you haven't defined as props) with this.$attrs. Finally you can bind props with v-bind="someVariable".
If you combine this you can do something like this:
<!-- App.vue -->
<template>
<component-a msg="Hello world" :fancy="{ test: 1 }" />
</template>
<!-- ComponentA.vue -->
<template>
<component-b v-bind="$attrs" />
</template>
<script>
export default {
name: 'componentA'
}
</script>
<!-- ComponentB.vue -->
<template>
<div>
{{ msg }}
{{ fancy }}
</div>
</template>
<script>
export default {
props: {
msg: String,
fancy: Object
},
mounted () {
console.log(this.$props);
}
}
</script>
In this example, component B would be the component you try to extend.
Here's a complete example based on Sumurai8's answer and motia's comments.
<template>
<Multiselect v-bind="childProps" v-on="$listeners">
<slot v-for="(_, name) in $slots" :name="name" :slot="name" />
<template v-for="(_, name) in $scopedSlots" :slot="name" slot-scope="slotData">
<slot :name="name" v-bind="slotData" />
</template>
</Multiselect>
</template>
<script>
import Multiselect from 'vue-multiselect'
export default {
name: "BaseCombo",
props: {
placeholder: {
type: String,
default: 'This is my default',
},
disabled: {
type: Boolean,
default: false,
},
},
components: {
Multiselect,
},
computed: {
childProps() {
return { ...this.$props, ...this.$attrs, disabled: this.isDisabled };
},
appReadOnly() {
return this.$store.state.appReadOnly;
},
isDisabled() {
return this.disabled || this.appReadOnly;
}
},
}
</script>

Component Declaration And Communication

I have added a component declaration to the default main.js file which is generated during the Webpack project creation process as
import Modal from '#/components/Modal'
Vue.component('modal', Modal)
And in the App.vue, I have
<modal v-show="showModal"></modal>
<button id="show-modal" v-on:click="showModal = true">Click to have a modal</button>
And they work fine. Now, I need to setup a "props down, events up" communication channel between the parent and a child. To do so, I need to add a property, called 'isActive', the Modal component so that the root component can send a message to the child component, that is
<modal isActive="showModal"></modal>
<button id="show-modal" v-on:click="showModal = true">Click to have a modal</button>
I guess the component declaration should be something like:
Vue.component('modal', {
props: ['isActive'],
Modal
})
It doesn't work, however, due to
Failed to mount component: template or render function not defined.
I have tried different variants without a luck.
My second question is that how a child event changes its parent data. For example, in the child component
<button class="modal-close is-large" v-on:click="closeModal"></button>
the closeModal event is handled in the following javacript code in the child component.
export default {
method: {
closeModal: function(event) {
...
}
}
}
How can I set its parent data showModal to false?
Update:
The code segment of Modal:
<template>
<div class="signin">
<div class="modal" v-bind:class="{ 'is-active': isActive }">
...
</div>
<button class="modal-close is-large" v-on:click="isActive = false"></button>
</div>
</div>
</template>
<script>
import axios from 'axios'
import _ from 'lodash'
import Notification from '#/components/Notification'
import { required, email } from 'vuelidate/lib/validators'
export default {
name: 'signin',
components: {
Notification
},
data: () => ({
isActive: true,
email: '',
...
}),
...
}
</script>
Bulma is used for styling. And the isActive is defined in the Modal. I think it needs to be changed to achieve "props down".
As it looks, your file /components/Modal contains a full definition of a component: the template, and the script parts for it. So you can just bind the component to the tag-name you want to use in your markup:
import Modal from '#/components/Modal'
Vue.component('modal', Modal)
This is basically what you had in the beginning. To pass properties to this component, add the props-line directly to your component, that is into /components/Modal:
...
export default {
name: 'signin',
components: {
Notification
},
props: ['isActive'],
data: () => ({
...
As for the second question, how to communicate back to the parent, have a look at Vue's Custom Events. Basically, your Modal component could issue a "close"-event like this:
methods: {
closeModal: function(event) {
this.$emit('modalClose')
}
}
and when you use the component, you could listen to it like this:
<modal v-bind:isActive="showModal" v-on:modalClose="showModal = false"></modal>
Note that you should use v-bind for providing the value to isActive. If you don't use v-bind, the value is just passed once when the component is created. This means, the component would never see a change to this prop when it is changed by the parent. By using v-bind, changes by the parent to this attribute are pushed down to the child-component, so the Modal actually sees the updated value and can react to it.