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
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
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++
}
}
}
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...
}
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...
]
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...
};
}
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
}
}
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();
}
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...
}
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...
}
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>
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)
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.
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>
<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>
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.'
}
}
})
Purpose: Reactively fetch and update data based on user input.
- 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
}
}
Purpose: Keep two related data fields in sync.
- 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
}
}
Purpose: Track changes in nested objects, such as form data.
- 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}`)
})
Purpose: Respond to changes in multiple variables simultaneously.
- Immediate Watcher for Initial Fetch Run a watcher immediately on component creation:
watch(
() => someId.value,
(newId) => {
fetchData(newId)
},
{ immediate: true }
)
Purpose: Fetch data on mount and whenever someId changes.
- Watching Props to Update the URL Watch a prop and update the route:
props: ['selectedPet'],
watch: {
selectedPet(newPet) {
this.$router.push({ query: { pet: newPet } })
}
}
Purpose: Sync component state with the URL for deep linking.
- 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
}
}
}
Purpose: Enforce input rules reactively.
- 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 })
Purpose: React to any change in a complex form and trigger side effects.
- 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)
}
}
Purpose: Perform actions when derived data changes.
- 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 }
)
Practical coding example Lifecycle Flow
my file structure
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);
},
})
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
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')
.
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');
});
This means any GET request to /spark or /spark/{type} or /spark/{type}/{id} will hit the BillingPortalController.
- Controller: BillingPortalController In Laravel Spark, this controller typically returns an Inertia response like:
return Inertia::render('BillingPortal', [...props]);
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
BillingPortal.vue
<template>
</template>
<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>
Explanation
Component Registration (components: {})
Purpose: Registers child components for use in the template.
Code Example:
components: {
ErrorMessages,
InfoMessages,
IntervalSelector,
// ...
}
When Called
: During component initialization (before creation).
Role
: Allows reuse of components like in the template.
- Props Declaration (props: []) Purpose: Defines data passed from parent components (e.g., Laravel backend via Inertia.js).
props: [
'balance',
'invoices',
'billableId',
// ...
]
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: { /* ... */ },
// ...
};
}
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 */ }
}
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();
}
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 }
}
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").
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';
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>
child component
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>
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',
],
:plans="monthlyPlans"
:seat-name="seatName"
:current-plan="plan"
<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'"/>
method
@plan-selected="startSubscribingToPlan($event)"
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;
},
Another child component
import SectionHeading from './../Components/SectionHeading';
<section-heading>
{{ __('Failed Subscription Payment') }}
</section-heading>
Top comments (0)