Published 1/9/2024 · 3 min read
Tags: vue , laravel , fortify , user-profile , forms
Laravel Fortify provides a /user/profile-information endpoint for updating user details. Let’s build the frontend for this.
Laravel Configuration
Ensure the profile information endpoint is in your CORS paths in config/cors.php:
'paths' => [
// ...
'user/profile-information',
],
Fortify’s UpdateUserProfileInformation action handles the validation and update logic. The default implementation is in app/Actions/Fortify/UpdateUserProfileInformation.php.
Profile Form Component
Create src/components/ProfileForm.vue:
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useAuthStore } from "@/stores/auth";
import { authService } from "@/services/auth";
const auth = useAuthStore();
const form = ref({
name: "",
email: "",
});
const errors = ref<Record<string, string[]>>({});
const message = ref<string | null>(null);
const isLoading = ref(false);
onMounted(() => {
if (auth.user) {
form.value.name = auth.user.name;
form.value.email = auth.user.email;
}
});
async function handleSubmit() {
errors.value = {};
message.value = null;
isLoading.value = true;
try {
await authService.updateProfile(form.value);
await auth.fetchUser();
message.value = "Profile updated successfully.";
} catch (e: any) {
if (e.response?.data?.errors) {
errors.value = e.response.data.errors;
}
} finally {
isLoading.value = false;
}
}
</script>
<template>
<form @submit.prevent="handleSubmit" class="space-y-4">
<div v-if="message" class="bg-green-100 text-green-700 p-4 rounded">
{{ message }}
</div>
<div>
<label for="name" class="block text-sm font-medium">Name</label>
<input
id="name"
v-model="form.name"
type="text"
required
class="mt-1 block w-full rounded border-gray-300 shadow-sm"
/>
<p v-if="errors.name" class="text-red-500 text-sm mt-1">
{{ errors.name[0] }}
</p>
</div>
<div>
<label for="email" class="block text-sm font-medium">Email</label>
<input
id="email"
v-model="form.email"
type="email"
required
class="mt-1 block w-full rounded border-gray-300 shadow-sm"
/>
<p v-if="errors.email" class="text-red-500 text-sm mt-1">
{{ errors.email[0] }}
</p>
</div>
<button
type="submit"
:disabled="isLoading"
class="bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700 disabled:opacity-50"
>
{{ isLoading ? "Saving..." : "Update Profile" }}
</button>
</form>
</template>
Password Update Component
Create src/components/PasswordForm.vue:
<script setup lang="ts">
import { ref } from "vue";
import { authService } from "@/services/auth";
const form = ref({
current_password: "",
password: "",
password_confirmation: "",
});
const errors = ref<Record<string, string[]>>({});
const message = ref<string | null>(null);
const isLoading = ref(false);
async function handleSubmit() {
errors.value = {};
message.value = null;
isLoading.value = true;
try {
await authService.updatePassword(form.value);
message.value = "Password updated successfully.";
// Clear form
form.value = {
current_password: "",
password: "",
password_confirmation: "",
};
} catch (e: any) {
if (e.response?.data?.errors) {
errors.value = e.response.data.errors;
}
} finally {
isLoading.value = false;
}
}
</script>
<template>
<form @submit.prevent="handleSubmit" class="space-y-4">
<div v-if="message" class="bg-green-100 text-green-700 p-4 rounded">
{{ message }}
</div>
<div>
<label for="current_password" class="block text-sm font-medium">
Current Password
</label>
<input
id="current_password"
v-model="form.current_password"
type="password"
required
class="mt-1 block w-full rounded border-gray-300 shadow-sm"
/>
<p v-if="errors.current_password" class="text-red-500 text-sm mt-1">
{{ errors.current_password[0] }}
</p>
</div>
<div>
<label for="password" class="block text-sm font-medium">
New Password
</label>
<input
id="password"
v-model="form.password"
type="password"
required
class="mt-1 block w-full rounded border-gray-300 shadow-sm"
/>
<p v-if="errors.password" class="text-red-500 text-sm mt-1">
{{ errors.password[0] }}
</p>
</div>
<div>
<label for="password_confirmation" class="block text-sm font-medium">
Confirm New Password
</label>
<input
id="password_confirmation"
v-model="form.password_confirmation"
type="password"
required
class="mt-1 block w-full rounded border-gray-300 shadow-sm"
/>
</div>
<button
type="submit"
:disabled="isLoading"
class="bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700 disabled:opacity-50"
>
{{ isLoading ? "Updating..." : "Update Password" }}
</button>
</form>
</template>
Settings Page
Create src/views/SettingsView.vue to combine both forms:
<script setup lang="ts">
import ProfileForm from "@/components/ProfileForm.vue";
import PasswordForm from "@/components/PasswordForm.vue";
</script>
<template>
<div class="max-w-2xl mx-auto mt-10 space-y-8">
<div class="bg-white shadow rounded p-6">
<h2 class="text-xl font-semibold mb-4">Profile Information</h2>
<ProfileForm />
</div>
<div class="bg-white shadow rounded p-6">
<h2 class="text-xl font-semibold mb-4">Update Password</h2>
<PasswordForm />
</div>
</div>
</template>
Add Route
Update src/router/index.ts:
{
path: '/settings',
name: 'settings',
component: () => import('@/views/SettingsView.vue'),
meta: { requiresAuth: true },
},
Next up: Setting up basic authorization with admin roles.
Related Articles
- Two-Way Binding
Master Svelte's bind directive for seamless form handling. Learn to bind inputs, selects, checkboxes, and component props.
- Form Actions
Handle form submissions with SvelteKit's actions. Learn progressive enhancement, validation, and how to build forms that work without JavaScript.
- Form Handling: Moving from Vue to Svelte
A practical guide to translating Vue form patterns to Svelte, covering two-way binding, validation, async submission, and what actually works better in each framework.