Bernardo Benini Fantin
Bernardo Benini Fantin

Reputation: 103

Proper way to decouple components

I am working on the communication of two components (using Vue 2), where one is a button, which can have states such as initial, loading, and concluded (successfully or not), and for each state of the button, I might have a different text displayed, different icon (loading: spinning icon, conlcuded with sucess: check mark, concluded with error: an x), and I also have a form, which will be using the button component. My doubt is how to change states on the button based on current state of the form submission. Check 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>

And 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 am making my button change according to the state of the form: my button is emitting out two function when clicked (startLoading, concludedSuccessfully) and then I am using these two functions inside sendItemToCart.

This seem like coupling the two components a bit too much, since I have to keep pasing the functions as parameters to the parent's methods. Also, I have another ideia on how I could do it, which would be by giving each button a ref and then calling its methods on the parent using the ref. This ideia sounds a bit like the "Composition rather than inheritance" from OOP, where I'd just ask the object/component to do something, but, in this case, without the functions as parameters.

Well, both cases above seem better than keep creating variables on the data for each button I might have, but they seem like they could get improved. So that's what I am looking for: how to better decouple my components?

Upvotes: 0

Views: 256

Answers (1)

Alexander Nenashev
Alexander Nenashev

Reputation: 23493

You are probably looking for provide/inject if we are talking about Vue 3 (not sure about Vue2, you didn't specify Vue's version):

https://vuejs.org/guide/components/provide-inject.html

So if you have a parent component and a bunch of child components that could appear ONLY AS DESCENDANTS of the parent component (a form and inputs is a good example) you could provide the form's state:

The OP commented that the button could appear in other places too, so we should use both props and provide. In absense of the form we could use the 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>

Adapt the code to the Options API.

UPDATING WITH VUE 2

The OP corrected the answer that Vue 2 is used. So...

Fortunately Vue 2 supports provide/inject also! The only problem how to make the provide reactive, which I guess is solved here:

How do I make Vue 2 Provide / Inject API reactive?

Upvotes: 2

Related Questions