I'm dealing with communication between two components (using Vue 2), one of which is a button, which can have states like initial, loading and completion (success or failure), for each state of the button I may display Different text, different icons (loading: spin icon, successful completion: tick, error completion: x), I also have a form that will use a button component. I'm not sure how to change the button's state based on the current state of the form submission. Please see the code below.
My button component:
<template>
<button
class="ui-button"
@click="clicked"
:data-status-type="status_type"
:disabled="is_disabled"
:type="type"
>
<i :class="icon" v-if="is_disabled || concluded"></i>
{{ title }}
</button>
</template>
<script>
export default {
props: {
title: {
type: String,
},
type: {
default: "button",
type: String,
},
},
data() {
return {
concluded: false,
icon: "fa fa-spin ",
is_disabled: false,
status_type: "success",
};
},
methods: {
clicked() {
if (!this.is_disabled) {
this.$emit(
"clicked",
() => {
this.is_disabled = true;
this.icon = "fa fa-spin fas fa-spinner";
},
(succeeded) => {
this.is_disabled = false;
this.concluded = true;
this.icon = succeeded ? "fas fa-check" : "fas fa-xmark";
this.status_type = succeeded ? "success" : "error";
setTimeout(() => {
this.concluded = false;
this.icon = "";
this.status_type = "";
}, 1500);
}
);
}
},
},
};
</script>
My form component:
<template>
<div>
<ThePages :parents="accompaniments">
<!-- ... some reactive stuff -->
<template #extra_button>
<TheButton @clicked="sendItemToCart" :title="button_text" :disabled="button_disabled" />
</template>
</ThePages>
</div>
</template>
<script>
import axios from 'axios'
import FormatHelper from '../helpers/FormatHelper'
import SwalHelper from '../helpers/SwalHelper'
import TheButton from './TheButton.vue'
import ThePages from './ThePages.vue'
import TheQuantityPicker from './TheQuantityPicker.vue'
export default {
props: ['product'],
components: {
TheButton,
ThePages,
TheQuantityPicker,
},
data() {
return {
accompaniments: this.product.accompaniment_categories,
button_text: '',
button_disabled: false,
format_helper: FormatHelper.toBRCurrency,
observation: '',
quantity: 1,
success: false,
}
},
created() {
this.addQuantityPropToAccompaniments()
this.availability()
},
methods: {
// ... some other methods
async sendItemToCart(startLoading, concludedSuccessfully) {
startLoading() // This will change the button state
this.button_text = 'Adicionando...'
await axios
.post(route('cart.add'), {
accompaniments: this.buildAccompanimentsArray(),
id: this.product.id,
quantity: this.quantity,
observation: this.observation,
})
.then(() => {
concludedSuccessfully(true) // This will change the button state
this.button_text = 'Adicionado'
SwalHelper.productAddedSuccessfully()
})
.catch((error) => {
concludedSuccessfully(false) // This will change the button state
if (
error?.response?.data?.message ==
'Este produto atingiu a quantidade máxima para este pedido.'
) {
SwalHelper.genericError(error?.response?.data?.message)
} else {
SwalHelper.genericError()
}
this.button_text = 'Adicionar ao carrinho'
})
},
},
}
</script>
In the code above, you can see how I change the state of the button based on the state of the form: My button emits two functions when clicked (startLoading, includedSuccessfully), and then I use these two in sendItemToCart function.
This seems to couple the two components a little too much, since I have to pass these functions as parameters to the parent component's methods. Also, I have another idea on how to do this, which is to give each button a ref and then call its method using the ref in the parent component. The idea sounds a bit like "composition instead of inheritance" in object-oriented programming, where I just ask the object/component to do something, but in this case, without taking the function as a parameter.
Well, the two cases above seem better than creating a variable for every button I might have, but they seem like they could be improved. So what I'm looking for is: How can I decouple my components better?
If we're talking about Vue 3 (you didn't specify the version of Vue, so not sure it's Vue 2), you're probably looking for
provide/inject:https://vuejs.org/guide/components/provide-inject.html
So, if you have a parent component and a bunch of child components that can only appear as descendants of the parent component (such as forms and input boxes), you can
providethe state of the form:The OP commented that buttons may appear elsewhere as well, so we should use both props and provide. In the absence of a form, we can use props as the default injected value.
In the form component:
<script setup> import {reactive, provide} from 'vue'; const form = reactive({button_disabled: false, button_text: '', sendItemToCart}); provide('form', form); function sendItemToCart(){ // your logic here } </script> <template> <div> <ThePages :parents="accompaniments"> <!-- ... some reactive stuff --> <template #extra_button> <TheButton /> </template> </ThePages> </div> </template>In the button component:
<script setup> import {inject} from 'vue'; const props = defineProps('button_disabled button_text'.split(' ')); const form = inject('form', props); // use either provide of props </setup> <template> <button class="ui-button" @click="() => form.sendItemToCart ? form.sendItemToCart() : $emit('clicked')" :data-status-type="status_type" :disabled="form.button_disabled" :type="type" > <i :class="icon" v-if="form.button_disabled || concluded"></i> {{ form.button_text }} </button> </template>Adjust the code to Options API.
Update using Vue 2
OP corrected the answer, using Vue 2. so...
Fortunately, Vue 2 also supports
provide/inject! The only problem was how to make the provide responsive, which I guess is solved here:How do I make Vue 2 Provide / Inject API reactive?