Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

BernardoBF4's avatar

Proper way to decouple components

I am working on the communication of two components, 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?

0 likes
1 reply
martinbean's avatar

@bernardobf4 Your submit button should be decoupled. Pass its state as a prop:

props: {
  state: {
    default: 'ready',
    required: false,
    type: String,
    validator: (state) => ['ready', 'processing', 'successful', 'errored'].includes(state),
  },
},
<SubmitButton v-bind:state="formState" />

You should also add type="submit" to the button in your component so that it submits whatever parent form it is used in. Now, your button will submit the parent form when clicked, and you can put the submission handling logic on your form:

<form v-on:submit.prevent="onSubmit">
    <!-- From fields -->
    <SubmitButton v-bind:state="formState" />
</form>
data: () => {
    formState: 'ready',
},
methods: {
    onSubmit() {
        this.formState = 'processing';

        // Do submission logic
        // Set formState to successful or errored, depending on result
    },
},
1 like

Please or to participate in this conversation.