Debug School

rakesh kumar
rakesh kumar

Posted on

Explain Vue.js Component Structure & Lifecycle Flow

Vue.js Component Structure & Lifecycle Flow
Give Component Example
Difference between computed and method
Difference between watch and mounted
How to use props in child component gets from backend
Real coding example of watch
Practical coding example Lifecycle Flow

Vue.js Component Structure & Lifecycle Flow

Lifecycle Flow Chart
Initialization

props resolved

components registered

data() initialized
Enter fullscreen mode Exit fullscreen mode

Creation Phase

beforeCreate → created
(Reactive data available here)
Mounting Phase

beforeMount → template compiled → mounted
(DOM accessible in mounted)
Updating Phase

beforeUpdate → Virtual DOM re-render → updated
(Triggered by data changes)
Destruction Phase

beforeUnmount → cleanup → unmounted
Enter fullscreen mode Exit fullscreen mode

When to Use Each
data: Initial component state

computed: Complex calculations from existing data

methods: User interactions/event handling

watch: API calls on data changes

mounted: DOM manipulation/initial API calls

props: Parent-to-child communication

Example

export default {
  components: {
    UserProfile
  },
  props: {
    initialCount: Number
  },
  data() {
    return {
      count: this.initialCount,
      users: []
    }
  },
  computed: {
    doubledCount() {
      return this.count * 2
    }
  },
  watch: {
    count(newVal) {
      console.log(`Count changed to ${newVal}`)
    }
  },
  mounted() {
    this.fetchUsers()
  },
  methods: {
    fetchUsers() {
      axios.get('/api/users').then(res => {
        this.users = res.data
      })
    },
    increment() {
      this.count++
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

In the BillingPortal.vue component, each section serves a specific purpose in the component's functionality and lifecycle:

Components Section

components: {
    ErrorMessages,
    InfoMessages,
    IntervalSelector,
    // more components...
}
Enter fullscreen mode Exit fullscreen mode

Purpose: Registers child components for use in the template.

When Called: During component initialization, before the component instance is created.

Props Section

props: [
    'balance',
    'invoices',
    'billableId',
    // more props...
]
Enter fullscreen mode Exit fullscreen mode

Purpose: Defines data that can be passed from parent components.

When Called: Values are received when the component is created or when parent updates them.

data() Function

data() {
    return {
        errors: [],
        processing: false,
        // more reactive properties...
    };
}
Enter fullscreen mode Exit fullscreen mode

Purpose: Sets up the component's reactive state variables.

When Called: When the component instance is first created, before mounting.

watch Object

watch: {
    '$page.props.state': {
        // handler for changes
    },
    'subscriptionForm.country'(val) {
        // handler for country changes
    }
}
Enter fullscreen mode Exit fullscreen mode

Purpose: Watches for changes in specific data properties and responds accordingly.

When Called: Whenever a watched property changes value during the component's lifecycle.

mounted() Lifecycle Hook

mounted() {
    this.subscriptionForm.extra = this.$page.props.billable.extra_billing_information;
    // more initialization...
    this.loadLazyData();
}
Enter fullscreen mode Exit fullscreen mode

Purpose: Performs setup that requires the DOM to be rendered.

When Called: After the component has been rendered to the DOM but before it's shown to the user.

computed Object

computed: {
    openInvoices() {
        return this.invoices?.open || []
    },
    // more computed properties...
}
Enter fullscreen mode Exit fullscreen mode

Purpose: Creates reactive properties that automatically update when dependencies change.

When Called: Whenever any of their dependencies change, and during initial rendering.

methods Object

methods: {
    startSubscribingToPlan(plan) {
        // method implementation
    },
    // more methods...
}
Enter fullscreen mode Exit fullscreen mode

Purpose: Contains functions that can be called from templates or other component code.

When Called: Only when explicitly invoked, such as from click events or other methods.

Difference between computed and method

When to Use Each
Use computed for properties that depend on other data and should be cached for performance (e.g., filtering a list, concatenating names).

Use methods for actions, event handlers, or when you need to pass arguments.

Computed properties are best for derived, cacheable values and are only recalculated when their dependencies change.

Methods are functions that run every time they’re called, suitable for actions, event handlers, or calculations with parameters.

Computed = efficient, cached, no parameters.

Method = always runs, can take parameters, use for actions or when caching isn’t needed.

<template>
  <div>
    <h2>Computed vs Method Example</h2>
    <input v-model="firstName" placeholder="First Name" />
    <input v-model="lastName" placeholder="Last Name" />

    <p>Computed Full Name: {{ fullName }}</p>
    <p>Method Full Name: {{ getFullName() }}</p>
    <button @click="sayHello">Say Hello (Method)</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    // CACHED: Only recalculates if firstName or lastName changes
    fullName() {
      console.log('Computed fullName called');
      return this.firstName + ' ' + this.lastName;
    }
  },
  methods: {
    // NOT CACHED: Runs every time it's called
    getFullName() {
      console.log('Method getFullName called');
      return this.firstName + ' ' + this.lastName;
    },
    sayHello() {
      alert(`Hello, ${this.getFullName()}!`);
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Explanation
Computed Property (fullName)

Used as {{ fullName }} in the template.

Vue caches the result and only recalculates if firstName or lastName changes.

Efficient for displaying derived data that’s used in multiple places.

Method (getFullName)
Enter fullscreen mode Exit fullscreen mode

Used as {{ getFullName() }} in the template.

Runs every time the component re-renders, regardless of whether firstName or lastName changed.

Accepts parameters if needed.

Suitable for event handlers or calculations that need to accept arguments.

Image description

Difference between watch and mounted

mounted
What is it?
mounted is a lifecycle hook in Vue.js.

It is called once, after the component’s DOM has been created and inserted into the page.

At this point, you can safely access the DOM elements (e.g., via this.$el or refs).

When is it called?
After the initial render, when the component is added to the DOM tree.

Typical Uses
Focusing an input field when the component loads.

Fetching data that needs the DOM to exist (e.g., measuring element size).

Integrating with third-party DOM libraries.

watch
What is it?
watch is an option that lets you react to changes in specific reactive data, props, or computed properties.

It is called every time the watched property changes (not just once).

When is it called?
After the component is created and whenever the watched property changes.

Typical Uses
Reacting to user input or prop changes.

Performing side effects (like API calls) when data changes.

Validating or transforming data in response to changes.

<template>
  <div>
    <input ref="nameInput" v-model="username" placeholder="Type your name..." />
    <p>Welcome, {{ username }}!</p>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode
<script>
export default {
  data() {
    return {
      username: ''
    }
  },

  // Called once after the component is mounted to the DOM
  mounted() {
    // Focus the input field when the component appears
    this.$refs.nameInput.focus();
    console.log('mounted: Input is focused');
  },

  // Called every time 'username' changes
  watch: {
    username(newVal, oldVal) {
      console.log(`watch: Username changed from "${oldVal}" to "${newVal}"`);
      // Example: Could fetch user data or validate input here
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Image description

Real coding example of watch

Fetching Data When an Input Changes
When a user types a question, fetch an answer from an API:

import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('')

watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.includes('?')) {
    answer.value = 'Thinking...'
    try {
      const res = await fetch('https://yesno.wtf/api')
      answer.value = (await res.json()).answer
    } catch (error) {
      answer.value = 'Error! Could not reach the API.'
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

Purpose: Reactively fetch and update data based on user input.

  1. Syncing Two Form Fields (Kilometers and Meters) Automatically update one field when the other changes:
data() {
  return {
    kilometers: 0,
    meters: 0
  }
},
watch: {
  kilometers(val) {
    this.meters = val * 1000
  },
  meters(val) {
    this.kilometers = val / 1000
  }
}
Enter fullscreen mode Exit fullscreen mode

Purpose: Keep two related data fields in sync.

  1. Watching a Deeply Nested Object React to changes in any property of a nested object:
data() {
  return {
    userInfo: {
      name: '',
      age: 0
    }
  }
},
watch: {
  userInfo: {
    handler(newVal, oldVal) {
      console.log('User info changed:', newVal)
    },
    deep: true
  }
}
Enter fullscreen mode Exit fullscreen mode

Purpose: Track changes in nested objects, such as form data.

  1. Watching Multiple Sources Watch more than one reactive value at once:
import { ref, watch } from 'vue'

const x = ref(0)
const y = ref(0)

watch([x, y], ([newX, newY], [oldX, oldY]) => {
  console.log(`x: ${oldX} → ${newX}, y: ${oldY} → ${newY}`)
})
Enter fullscreen mode Exit fullscreen mode

Purpose: Respond to changes in multiple variables simultaneously.

  1. Immediate Watcher for Initial Fetch Run a watcher immediately on component creation:
watch(
  () => someId.value,
  (newId) => {
    fetchData(newId)
  },
  { immediate: true }
)
Enter fullscreen mode Exit fullscreen mode

Purpose: Fetch data on mount and whenever someId changes.

  1. Watching Props to Update the URL Watch a prop and update the route:
props: ['selectedPet'],
watch: {
  selectedPet(newPet) {
    this.$router.push({ query: { pet: newPet } })
  }
}
Enter fullscreen mode Exit fullscreen mode

Purpose: Sync component state with the URL for deep linking.

  1. Preventing Illegal Input Values Restrict a range input to legal values:
data() {
  return { rangeVal: 70 }
},
watch: {
  rangeVal(val) {
    if (val > 20 && val < 60) {
      this.rangeVal = val < 40 ? 20 : 60
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Purpose: Enforce input rules reactively.

  1. Syncing Form Data with an API (Deep Watch) Sync an entire form object with the server in real time:
import { ref, watch } from 'vue'

const formData = ref({ name: '', email: '' })

watch(formData, (newData) => {
  console.log('Syncing form data with the server:', newData)
}, { deep: true })
Enter fullscreen mode Exit fullscreen mode

Purpose: React to any change in a complex form and trigger side effects.

  1. Watching a Computed Property React to changes in a computed property (e.g., search results):
computed: {
  filteredList() {
    return this.items.filter(item => item.match(this.searchTerm))
  }
},
watch: {
  filteredList(newList) {
    console.log('Filtered list changed:', newList)
  }
}
Enter fullscreen mode Exit fullscreen mode

Purpose: Perform actions when derived data changes.

  1. One-Time Watch (Once) Run a watcher only the first time a value changes:
watch(
  source,
  (newValue, oldValue) => {
    console.log('This will only run once.')
  },
  { once: true }
)
Enter fullscreen mode Exit fullscreen mode

Practical coding example Lifecycle Flow

my file structure

Image description

First page app.js

window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';

import BaseMixin from './Mixins/Base';

createInertiaApp({
    resolve: name => require(`./Pages/${name}`),
    setup({ el, App, props, plugin }) {
        const app = createApp({ render: () => h(App, props) });

        app.mixin(BaseMixin)
            .use(plugin)
            .mount(el);
    },
})
Enter fullscreen mode Exit fullscreen mode

createInertiaApp({
resolve: name => require(./Pages/${name}),
// ...
})
This means the first page that gets called depends on how your Laravel backend renders the Inertia response. The resolve function dynamically loads a Vue component from the Pages directory based on the page name provided by the server.

resources/js/app.js
Enter fullscreen mode Exit fullscreen mode

is always the first file executed on the client side.

The actual "first page" (Vue component) loaded will be determined by the Laravel route/controller that returns the initial Inertia response.

In your Pages folder, for example, BillingPortal.vue could be loaded first if the backend returns it as the initial page.

To know exactly which component is loaded first:

Check your Laravel controller or route that returns

Inertia::render('PageName')
Enter fullscreen mode Exit fullscreen mode

.

The 'PageName' (e.g., 'BillingPortal') will match a .vue file in your Pages directory and will be the first page loaded by the frontend.

Laravel Route:
Your route group includes:

Route::group([
    'middleware' => array_merge(config('spark.middleware', ['web', 'auth']), [HandleInertiaRequests::class]),
    'namespace' => 'Spark\Http\Controllers',
    'prefix' => config('spark.path'),
], function () {
    Route::get('/{type?}/{id?}', 'BillingPortalController')->name('spark.portal');
});
Enter fullscreen mode Exit fullscreen mode

This means any GET request to /spark or /spark/{type} or /spark/{type}/{id} will hit the BillingPortalController.

  1. Controller: BillingPortalController In Laravel Spark, this controller typically returns an Inertia response like:
return Inertia::render('BillingPortal', [...props]);
Enter fullscreen mode Exit fullscreen mode

This tells Inertia.js (and your frontend) to load the BillingPortal.vue component from resources/js/Pages/BillingPortal.vue.

First Page Loaded
After app.js runs, the first Vue component loaded is:

resources/js/Pages/BillingPortal.vue
Enter fullscreen mode Exit fullscreen mode

Image description

Image description

BillingPortal.vue

<template>
</template>
Enter fullscreen mode Exit fullscreen mode
<script>
import ErrorMessages from './../Components/ErrorMessages';
import InfoMessages from './../Components/InfoMessages';
import IntervalSelector from './../Components/IntervalSelector';
import InvoiceList from './../Components/InvoiceList';
import Modal from './../Components/Modal';
import Plan from './../Components/Plan';
import PlanList from './../Components/PlanList';
import PlanSectionHeading from './../Components/PlanSectionHeading';
import SectionHeading from './../Components/SectionHeading';
import SparkButton from './../Components/Button';
import SparkSecondaryButton from './../Components/SecondaryButton';
import SuccessMessage from './../Components/SuccessMessage';
import {router} from '@inertiajs/vue3'

export default {
    components: {
        ErrorMessages,
        InfoMessages,
        IntervalSelector,
        InvoiceList,
        Modal,
        Plan,
        PlanList,
        PlanSectionHeading,
        SectionHeading,
        SparkButton,
        SparkSecondaryButton,
        SuccessMessage,
    },

    props: [
        'balance',
        'invoices',

        'billableId',
        'billableType',
        'billingAddressRequired',
        'collectionMethod',
        'collectsVat',
        'collectsBillingAddress',
        'monthlyPlans',
        'paymentMethod',
        'paymentMethods',
        'plan',
        'seatName',
        'userName',
        'yearlyPlans',
    ],

    data() {
        return {
            errors: [],
            processing: false,

            showingPlansOfInterval: 'monthly',

            subscribing: null,
            addingVatNumber: false,
            subscriptionForm: {
                coupon: null,
                country: '',
                accept: false,
                vat: '',
                postal_code: '',
                address: '',
                address2: '',
                city: '',
                state: '',
                extra: ''
            },

            checkoutTax: 0,
            checkoutAmount: 0,
            rawCheckoutAmount: 0,
            loadingTaxCalculations: true,

            paymentInformationForm: {
                country: '',
                vat: '',
                postal_code: '',
                address: '',
                address2: '',
                city: '',
                state: '',
                extra: ''
            },

            invoiceEmailsForm: {
                invoice_emails: '',
            },

            couponForm: {
                coupon: '',
            },

            selectingNewPlan: false,
            updatingPaymentInformation: false,

            billingInformationForm: {
                extra: ''
            },

            confirmAction: null,
            confirmArguments: [],
            confirmText: '',
            showModal: false,

            reloadDataID: null,
        };
    },

    watch: {
        /**
         * Watch the "$page.props.state" variable to reload data during "pending" state.
         */
        '$page.props.state': {
            immediate: true,
            handler: function (newState, oldState) {
                if (newState == 'pending') {
                    this.startReloadingData();
                }
            }
        },

        "couponForm.coupon"(val) {
            if (val) {
                this.$refs.applyCouponButton.$el.removeAttribute('disabled')
            } else {
                this.$refs.applyCouponButton.$el.setAttribute('disabled', 'disabled')
            }
        },

        "invoiceEmailsForm.invoice_emails"(newValue, oldValue) {
            if (!this.$page.props.sendsInvoicesToCustomAddresses) {
                return;
            }

            if (newValue || oldValue) {
                this.$refs.updateInvoiceEmailsButton.$el.removeAttribute('disabled')
            } else {
                this.$refs.updateInvoiceEmailsButton.$el.setAttribute('disabled', 'disabled')
            }
        },

        subscribing(val) {
            if (!val) {
                window.history.pushState({}, document.title, window.location.pathname);
            } else {
                window.history.pushState({}, document.title, window.location.pathname + '?subscribe=' + val.id);

                this.calculatingTax(this.subscribing, (data) => {
                    this.checkoutTax = data.tax;
                    this.checkoutAmount = data.total;
                    this.rawCheckoutAmount = data.raw_total;
                });
            }

            this.checkoutTax = 0;

            if (!this.$page.props.billable.vat_id) {
                this.addingVatNumber = false;
            }
        },

        'subscriptionForm.country'(val) {
            if (!this.$page.props.billable.vat_id) {
                this.addingVatNumber = false;
            }

            if (this.collectsVat && this.subscribing) {
                this.calculatingTax(this.subscribing, (data) => {
                    this.checkoutTax = data.tax;
                    this.checkoutAmount = data.total;
                    this.rawCheckoutAmount = data.raw_total;
                });
            }
        },

        'subscriptionForm.vat': _.debounce(function () {
            if (this.collectsVat && this.subscribing) {
                this.calculatingTax(this.subscribing, (data) => {
                    this.checkoutTax = data.tax;
                    this.checkoutAmount = data.total;
                    this.rawCheckoutAmount = data.raw_total;
                });
            }
        }, 500)
    },

    /**
     * Initialize the component.
     */
    mounted() {
        this.subscriptionForm.extra = this.$page.props.billable.extra_billing_information;
        this.subscriptionForm.address = this.$page.props.billable.billing_address;
        this.subscriptionForm.address2 = this.$page.props.billable.billing_address_line_2;
        this.subscriptionForm.city = this.$page.props.billable.billing_city;
        this.subscriptionForm.state = this.$page.props.billable.billing_state;
        this.subscriptionForm.postal_code = this.$page.props.billable.billing_postal_code;
        this.subscriptionForm.country = this.$page.props.billable.billing_country || '';
        this.subscriptionForm.vat = this.$page.props.billable.vat_id;

        this.paymentInformationForm.address = this.$page.props.billable.billing_address;
        this.paymentInformationForm.address2 = this.$page.props.billable.billing_address_line_2;
        this.paymentInformationForm.city = this.$page.props.billable.billing_city;
        this.paymentInformationForm.state = this.$page.props.billable.billing_state;
        this.paymentInformationForm.postal_code = this.$page.props.billable.billing_postal_code;
        this.paymentInformationForm.country = this.$page.props.billable.billing_country;
        this.paymentInformationForm.vat = this.$page.props.billable.vat_id;

        if (this.$page.props.billable.vat_id) {
            this.addingVatNumber = true;
        }

        this.billingInformationForm.extra = this.$page.props.billable.extra_billing_information;

        this.invoiceEmailsForm.invoice_emails = this.$page.props.billable.invoice_emails.join(',');

        router.on('invalid', (event) => {
            event.preventDefault();

            if (event.detail.response.request.responseURL) {
                window.location.href = event.detail.response.request.responseURL;
            } else {
                console.error(event);
            }
        });

        if (this.$page.props.plan) {
            this.showingPlansOfInterval = this.$page.props.plan.interval;
        } else if (this.monthlyPlans.length == 0 && this.yearlyPlans.length > 0) {
            this.showingPlansOfInterval = 'yearly';
        } else {
            this.showingPlansOfInterval = this.$page.props.defaultInterval;
        }

        if (this.$page.props.state == 'none' && this.$page.props.subscribingTo) {
            this.startSubscribingToPlan(this.$page.props.subscribingTo);
        }

        this.loadLazyData();
    },

    computed: {
        /**
         * Get all open invoices once the data has been loaded.
         */
        openInvoices() {
            return this.invoices?.open || []
        },

        /**
         * Get all paid invoices once the data has been loaded.
         */
        paidInvoices() {
            return this.invoices?.paid || null
        },

        /**
         * Get the formatted balance once the data has been loaded.
         */
        formattedBalance() {
            return this.balance?.formatted
        },

        /**
         * Get the raw balance once the data has been loaded.
         */
        rawBalance() {
            return this.balance?.raw || 0
        },

        /**
         * Determine if the selected country is a country where VAT is collected.
         */
        vatComplicit() {
            return this.collectsVat ? _.includes([
                'BE', 'BG', 'CZ', 'DK', 'DE',
                'EE', 'IE', 'GR', 'ES', 'FR',
                'HR', 'IT', 'CY', 'LV', 'LT',
                'LU', 'HU', 'MT', 'NL', 'AT',
                'PL', 'PT', 'RO', 'SI', 'SK',
                'FI', 'SE', 'GB',
            ], this.subscriptionForm.country) : false;
        },
    },

    methods: {
        /**
         * Begin the process of subscription to a plan.
         */
        startSubscribingToPlan(plan) {
            if (! this.$page.props.collectsVat && ! this.$page.props.collectsBillingAddress) {
                this.request('POST', '/spark/subscription', {
                    plan: plan.id,
                    direct: true,
                }).then(response => {
                    if (response) {
                        window.location.href = response.data.redirect;
                    } else {
                        this.processing = false;
                    }
                });

                return;
            }


            this.subscribing = plan;
        },

        /**
         * Actually create a new subscription for the billable.
         */
        confirmSubscription() {
            this.processing = true;

            this.request('POST', '/spark/subscription', {
                plan: this.subscribing.id,
                extra_billing_information: this.subscriptionForm.extra,
                billing_address: this.subscriptionForm.address,
                billing_address_line_2: this.subscriptionForm.address2,
                billing_city: this.subscriptionForm.city,
                billing_state: this.subscriptionForm.state,
                billing_postal_code: this.subscriptionForm.postal_code,
                billing_country: this.subscriptionForm.country,
                vat_id: this.subscriptionForm.vat,
            }).then(response => {
                this.billingInformationForm.extra = this.subscriptionForm.extra;

                if (response) {
                    window.location.href = response.data.redirect;
                } else {
                    this.processing = false;
                }
            });
        },

        /**
         * Handle a payment response and optionally redirect to the payment page.
         */
        handlePaymentResponse(response) {
            if (response && response.data.paymentId) {
                window.location = '/' + this.$page.props.cashierPath + '/payment/' + response.data.paymentId + '?redirect=' + window.location.origin + '/' + this.$page.props.sparkPath;
            } else if (response) {
                this.reloadData(['spark', 'lastPayment', 'nextPayment', 'plan', 'openInvoices', 'invoices', 'state', 'trialEndsAt']);
            } else {
                this.processing = false;
            }
        },

        /**
         * Switch to the given plan.
         */
        switchToPlan(plan) {
            this.processing = true;

            this.request('PUT', '/spark/subscription', {
                plan: plan.id,
            }).then(response => {
                this.handlePaymentResponse(response);
            });
        },

        /**
         * Toggle the plan intervals that are being displayed.
         */
        toggleDisplayedPlanIntervals() {
            if (this.showingPlansOfInterval == 'monthly') {
                this.showingPlansOfInterval = 'yearly';
            } else {
                this.showingPlansOfInterval = 'monthly';
            }
        },

        /**
         * Show the VAT number entry field.
         */
        showVatNumber() {
            this.addingVatNumber = true;

            this.$nextTick(() => this.$refs.vat.focus());
        },

        /**
         * Set up a new payment method to use for later.
         */
        addPaymentMethod() {
            this.processing = true;

            this.request('POST', '/spark/subscription/payment-method').then(response => {
                if (response) {
                    window.location.href = response.data.redirect;
                } else {
                    this.processing = false;
                }
            });
        },

        /**
         * Set a payment method as the default.
         */
        defaultPaymentMethod(paymentMethod) {
            this.processing = true;

            this.request('PUT', '/spark/subscription/payment-method/default', {
                payment_method: paymentMethod,
            }).then(response => {
                if (response) {
                    this.reloadData(['paymentMethods']);
                } else {
                    this.processing = false;
                }
            });
        },

        /**
         * Delete a payment method.
         */
        deletePaymentMethod(paymentMethod) {
            this.processing = true;

            this.request('DELETE', '/spark/subscription/payment-method', {
                payment_method: paymentMethod,
            }).then(response => {
                if (response) {
                    this.reloadData(['paymentMethods']);
                } else {
                    this.processing = false;
                }
            });
        },

        /**
         * Update the customer's payment information.
         */
        updatePaymentInformation() {
            this.processing = true;

            this.request('PUT', '/spark/subscription/payment-information', {
                billing_address: this.paymentInformationForm.address,
                billing_address_line_2: this.paymentInformationForm.address2,
                billing_city: this.paymentInformationForm.city,
                billing_state: this.paymentInformationForm.state,
                billing_postal_code: this.paymentInformationForm.postal_code,
                billing_country: this.paymentInformationForm.country,
                vat_id: this.paymentInformationForm.vat,
            }).then(response => {
                if (response) {
                    this.reloadData();
                } else {
                    this.processing = false;
                }
            });
        },

        /**
         * Determine if a billing address is present.
         */
        hasBillingAddress() {
            const billable = this.$page.props.billable;

            return billable.billing_address ||
                billable.billing_address2 ||
                billable.billing_city ||
                billable.billing_state ||
                billable.billing_postal_code ||
                billable.billing_country;
        },

        /**
         * Check if there are any unpaid invoices.
         */
        hasUnpaidInvoices() {
            return this.openInvoices.filter(invoice => invoice.status === 'open').length > 0;
        },

        /**
         * Update the extra billing information for the customer.
         */
        updateBillingInformation() {
            this.processing = true;

            this.request('PUT', '/spark/billing-information', {
                extra_billing_information: this.billingInformationForm.extra,
            }).then(response => {
                this.subscriptionForm.extra = this.billingInformationForm.extra;

                this.reloadData();
            });
        },

        /**
         * Update the email addresses we send invoices to.
         */
        updateInvoiceEmails() {
            this.processing = true;

            this.request('PUT', '/spark/invoice-emails', {
                invoice_emails: this.invoiceEmailsForm.invoice_emails,
            }).then(response => {
                this.reloadData();

                if (! this.invoiceEmailsForm.invoice_emails) {
                    this.$refs.updateInvoiceEmailsButton.$el.setAttribute('disabled', 'disabled')
                }
            });
        },

        /**
         * Apply a coupon to the existing subscription.
         */
        applyCoupon() {
            this.processing = true;

            this.request('PUT', '/spark/coupon', {
                coupon: this.couponForm.coupon,
            }).then(response => {
                this.reloadData();
            });
        },

        /**
         * Cancel the customer's subscription.
         */
        cancelSubscription() {
            this.processing = true;

            this.request('PUT', '/spark/subscription/cancel').then(response => {
                this.reloadData();
            });
        },

        /**
         * Resume a cancelled subscription.
         */
        resumeSubscription() {
            this.processing = true;

            this.request('PUT', '/spark/subscription/resume', {}).then(response => {
                this.reloadData();
            });
        },

        /**
         * Calculate the appropriate TAX for the given total.
         */
        calculatingTax(plan, callback) {
            this.loadingTaxCalculations = true;

            return this.request('POST', '/spark/tax-rate', {
                total: plan.raw_price,
                currency: plan.currency,
                vat_number: this.subscriptionForm.vat,
                country: this.subscriptionForm.country,
                postal_code: this.subscriptionForm.postal_code,
            }).then(response => {
                this.loadingTaxCalculations = false;

                callback(response.data)
            });
        },

        /**
         * Retry a failed payment for a given invoice.
         */
        retryPayment(invoice) {
            this.processing = true;

            this.request('POST', `/spark/${invoice.id}/pay`)
                .then(response => {
                    this.handlePaymentResponse(response);
                });
        },

        /**
         * Start periodically reloading the page's data.
         */
        startReloadingData() {
            this.reloadDataID = setInterval(() => {
                this.reloadData([], () => this.loadLazyData());

                if (this.$page.props.state != 'pending') {
                    clearInterval(this.reloadDataID);
                }
            }, 3000)
        },

        /**
         * Reload the page's data, while maintaining any current state.
         */
        reloadData(only = [], then) {
            return this.$inertia.reload({
                ...(only.length && {only}),
                onSuccess: () => {
                    this.selectingNewPlan = false;
                    this.processing = false;
                    this.subscribing = null;
                    this.updatingPaymentInformation = false;
                },
                onFinish: () => {
                    if (! only.length) {
                        this.loadLazyData();
                    }

                    if (then) then()
                },
            });
        },

        /**
         * Load only the lazy-loaded data.
         */
        loadLazyData() {
            return this.$inertia.reload({
                only: ['balance', 'invoices'],
            })
        },

        /**
         * Make an outgoing request to the Laravel application.
         */
        request(method, url, data = {}) {
            this.errors = [];

            data.billableType = this.billableType;
            data.billableId = this.billableId;

            return axios.request({
                url: url,
                method: method,
                data: data,
            }).then(response => {
                return response;
            }).catch(error => {
                if (error.response.status == 422) {
                    this.errors = _.flatMap(error.response.data.errors)
                } else {
                    this.errors = [this.__('An unexpected error occurred and we have notified our support team. Please try again later.')]
                }

                document.body.scrollTop = 0;
                document.documentElement.scrollTop = 0;
            });
        },

        /**
         * Confirm a modal action.
         */
        confirm() {
            this.$refs.modal?.close();

            this.confirmAction(...this.confirmArguments);

            this.confirmAction = null;
            this.confirmArguments = [];
            this.confirmText = '';
        },

        /**
         * Open a confirm modal.
         */
        open(confirmAction, confirmText, confirmArguments = []) {
            this.confirmAction = confirmAction;
            this.confirmArguments = confirmArguments;
            this.confirmText = confirmText;
            this.showModal = true;
        },

        /**
         * Close a confirm modal.
         */
        close() {
            this.$refs.modal?.close();

            this.confirmAction = null;
            this.confirmArguments = [];
            this.confirmText = '';
        },
    },
}
</script>

Enter fullscreen mode Exit fullscreen mode

Explanation

Component Registration (components: {})
Purpose: Registers child components for use in the template.

Code Example:

components: {
  ErrorMessages,
  InfoMessages,
  IntervalSelector,
  // ...
}
Enter fullscreen mode Exit fullscreen mode

When Called: During component initialization (before creation).

Role: Allows reuse of components like in the template.

  1. Props Declaration (props: []) Purpose: Defines data passed from parent components (e.g., Laravel backend via Inertia.js).
props: [
  'balance',
  'invoices',
  'billableId',
  // ...
]
Enter fullscreen mode Exit fullscreen mode

When Called: Initialized before component creation.

Role: Receives data like balance and invoices from the parent component/backend.

Reactive Data (data() {})
Purpose: Initializes component-specific reactive state.

data() {
  return {
    errors: [],
    processing: false,
    subscriptionForm: { /* ... */ },
    // ...
  };
}
Enter fullscreen mode Exit fullscreen mode

When Called: During component initialization (before created hook).

Role: Manages form state, errors, and UI flags.

Watchers (watch: {})
Purpose: Reacts to specific data changes.

watch: {
  '$page.props.state'(newState) { /* Handle state changes */ },
  'subscriptionForm.country'(val) { /* Update VAT */ }
}
Enter fullscreen mode Exit fullscreen mode

When Called: After observed data changes (e.g., country field updates).

Role: Triggers side effects like VAT recalculation or error clearing.
Lifecycle Hook (mounted() {})
Purpose: Runs after the component is added to the DOM.

mounted() {
  this.subscriptionForm.extra = this.$page.props.billable.extra_billing_information;
  this.loadLazyData();
}
Enter fullscreen mode Exit fullscreen mode

When Called: After initial DOM rendering.

Role: Initializes form data and fetches additional data (e.g., payment methods).

Computed Properties (computed: {})
Purpose: Derives reactive values from existing data.

computed: {
  openInvoices() { return this.invoices?.open || [] },
  hasPaymentMethod() { return !!this.$page.props.state.payment_method }
}
Enter fullscreen mode Exit fullscreen mode

When Called: Automatically when dependencies change.

Role: Provides cached values like filtered invoices or payment method status.

Methods (methods: {})
Purpose: Defines reusable functions for user interactions.

methods: {
  startSubscribingToPlan(plan) { /* Initiate subscription */ },
  confirmSubscription() { /* API call to finalize */ },
  // ...
}
When Called: Explicitly via event handlers (e.g., @click="startSubscribingToPlan").
Enter fullscreen mode Exit fullscreen mode

Role: Handles form submissions, API calls, and UI updates.

Lifecycle Flow
Initialization:

Props are received (e.g., balance, invoices).

data() initializes reactive state (e.g., errors, processing).

Mounting:

Component template renders with initial data.

mounted() hook runs:

Sets up form fields using props/data.

Fetches lazy-loaded data (e.g., payment methods).

Interaction:

User actions (e.g., clicking "Change Plan") call methods.

Methods update data or make API requests (e.g., this.$inertia.post(...)).

Watchers react to data changes (e.g., country → VAT updates).

Updates:

API responses update local state (e.g., this.subscription = response.data).

Computed properties re-calculate (e.g., hasPaymentMethod updates UI).

Vue re-renders affected parts of the template.

Unmounting:

Cleanup tasks (not shown here) would run in beforeUnmount().

Key Integration Points
Inertia.js: Connects Laravel backend to Vue frontend via $page.props.

Reactivity: Watchers and computed properties ensure UI stays in sync with data.

API Interaction: Methods use Axios/Inertia to communicate with Laravel routes (e.g., /spark/subscription).

import PlanList from './../Components/PlanList';
Enter fullscreen mode Exit fullscreen mode

parent componnet BillingPortal.vue

  <Transition name="component-fade" mode="out-in">
                                <!-- Monthly Plans -->
                                <plan-list class="mt-6" key="subscribe-monthly-plans"
                                           :plans="monthlyPlans"
                                           interval="monthly"
                                           :seat-name="seatName"
                                           :current-plan="plan"
                                           @plan-selected="startSubscribingToPlan($event)"
                                           v-if="monthlyPlans.length > 0 && showingPlansOfInterval == 'monthly'"/>
                            </Transition>
Enter fullscreen mode Exit fullscreen mode

child component

Image description

planlist.vue

<template>
    <div class="space-y-6">
        <div class="bg-white sm:rounded-lg shadow-sm overflow-hidden" v-for="plan in plans">
            <plan :plan="plan" :seat-name="seatName" />

            <div class="px-6 py-4 bg-gray-100 bg-opacity-50 border-t border-gray-100 text-right">
                <spark-button @click.native="$emit('plan-selected', plan)" v-if="! currentPlan || (currentPlan && currentPlan.id != plan.id)">
                    {{ __('Subscribe') }}
                </spark-button>
<spark-button @click.native="$emit('plan-selected', plan)" v-if="! currentPlan || (currentPlan && currentPlan.id != plan.id)">
                    {{ __('Paytm') }}
                </spark-button>
                <div class="flex justify-end items-center" v-if="currentPlan && currentPlan.id == plan.id">
                    <div class="ml-1 text-sm text-gray-400">{{ __('Currently Subscribed') }}</div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    import Plan from './../Components/Plan';
    import SparkButton from './../Components/Button';

    export default {
        components: {
            Plan,
            SparkButton,
        },

        props: ['plans', 'interval', 'currentPlan', 'seatName'],
    }
</script>
Enter fullscreen mode Exit fullscreen mode

How to use props in child component gets from backend

 props: [
        'balance',
        'invoices',

        'billableId',
        'billableType',
        'billingAddressRequired',
        'collectionMethod',
        'collectsVat',
        'collectsBillingAddress',
        'monthlyPlans',
        'paymentMethod',
        'paymentMethods',
        'plan',
        'seatName',
        'userName',
        'yearlyPlans',
    ],
Enter fullscreen mode Exit fullscreen mode
 :plans="monthlyPlans"
 :seat-name="seatName"
 :current-plan="plan"
Enter fullscreen mode Exit fullscreen mode
<plan-list class="mt-6" key="subscribe-monthly-plans"
                                           :plans="monthlyPlans"
                                           interval="monthly"
                                           :seat-name="seatName"
                                           :current-plan="plan"
                                           @plan-selected="startSubscribingToPlan($event)"
                                           v-if="monthlyPlans.length > 0 && showingPlansOfInterval == 'monthly'"/>
Enter fullscreen mode Exit fullscreen mode

method

@plan-selected="startSubscribingToPlan($event)"
Enter fullscreen mode Exit fullscreen mode
methods: {
        /**
         * Begin the process of subscription to a plan.
         */
        startSubscribingToPlan(plan) {
            if (! this.$page.props.collectsVat && ! this.$page.props.collectsBillingAddress) {
                this.request('POST', '/spark/subscription', {
                    plan: plan.id,
                    direct: true,
                }).then(response => {
                    if (response) {
                        window.location.href = response.data.redirect;
                    } else {
                        this.processing = false;
                    }
                });

                return;
            }


            this.subscribing = plan;
        },
Enter fullscreen mode Exit fullscreen mode

Another child component
import SectionHeading from './../Components/SectionHeading';

  <section-heading>
                            {{ __('Failed Subscription Payment') }}
 </section-heading>
Enter fullscreen mode Exit fullscreen mode

Top comments (0)