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

muuucho's avatar
Level 11

Passing error to Livewire view

How do I display theerror when a user updates to a new password that isn't minimum level 3 in Zxcvbn? I am using livewire + Breeze and have added a strength indicator for a new password in the profile page. The indicator works. Then I implemented a rule that checks the strengthlevel and validates that it is >3. I can fetch the password error from that rule in the 'update-password-form.blade.php' but it is not passed to the livewire component's view 'update-passwords.blade.php'

The Breeze controller where I have added the PasswordRule

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Rules\PasswordRule;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
use Jantinnerezo\LivewireAlert\LivewireAlert;

class PasswordController extends Controller
{
    use LivewireAlert;
    /**
     * Update the user's password.
     */
    public function update(Request $request): RedirectResponse
    {
        $validated = $request->validateWithBag('updatePassword', [
            'current_password' => ['required', 'current_password'],
            'password' => ['required', Password::defaults(), 'confirmed', new PasswordRule],
        ]);

        $request->user()->update([
            'password' => Hash::make($validated['password']),
        ]);

        $this->flash('success', 'Password successfully updated', [
            'timer' => 3000,
            'toast' => true,
        ]);

        return back()->with('status', 'password-updated');
    }
}

The Rule

<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use ZxcvbnPhp\Zxcvbn;


class PasswordRule implements ValidationRule
{
    /**
     * Run the validation rule.
     *
     * @param  \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString  $fail
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if($this->getStrengthScore($value) < 3){
            $fail('The :attribute must be at least "Good". ');
        }
    }

    public function getStrengthScore($password): mixed
    {
        return (new Zxcvbn())->passwordStrength($password)['score'];
    }
}

The view

<section>
        {{--{{ dd($errors) }}works--}}
    <header>
        <h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
            {{ __('Update Password') }}
        </h2>

        <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
            {{ __('Ensure your account is using a long, random password to stay secure.') }}
        </p>
    </header>

    <form method="post" action="{{ route('password.update') }}" class="mt-6 space-y-6">
        @csrf
        @method('put')

        <div>
            <x-input-label for="current_password" :value="__('Current Password')" />
            <x-text-input id="current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" />
            <x-input-error :messages="$errors->updatePassword->get('current_password')" class="mt-2" />
        </div>
        <!-- shall I pass the error here? How? -->
        <livewire:update-passwords/>

        <div class="flex items-center gap-4">
            <x-primary-button>{{ __('Save') }}</x-primary-button>

            @if (session('status') === 'password-updated')
                <p
                    x-data="{ show: true }"
                    x-show="show"
                    x-transition
                    x-init="setTimeout(() => show = false, 2000)"
                    class="text-sm text-gray-600 dark:text-gray-400"
                >{{ __('Saved.') }}</p>
            @endif
        </div>
    </form>
</section>

The UpdatePasswords component

<?php

namespace App\Livewire;

use ZxcvbnPhp\Zxcvbn;
use Livewire\Component;
use Illuminate\Support\Str;
use Illuminate\Contracts\View\View;

class UpdatePasswords extends Component
{
    public string $password = '';
    public string $passwordConfirmation = '';
    public int $strengthScore = 0;
    public array $strengthLevels = [
        1 => 'Weak',
        2 => 'Fair',
        3 => 'Good',
        4 => 'Strong',
    ];
    public array $strengthColors = [
        0 => 'bg-red-600',
        1 => 'bg-yellow-600',
        2 => 'bg-orange-600',
        3 => 'bg-lime-600',
        4 => 'bg-green-600',
    ];

    public function updatePassword(): void
    {
        $this->updatedPassword($this->password);
    }

    public function updatedPassword($value): void
    {
        $this->strengthScore = (new Zxcvbn())->passwordStrength($value)['score'];
    }

    public function generatePassword(): void
    {
        $password = Str::password(12);
        $this->setPasswords($password);
    }

    protected function setPasswords($value): void
    {
        $this->password = $value;
        $this->passwordConfirmation = $value;
        $this->updatedPassword($value);
    }

    public function render(): View
    {
        return view('livewire.update-passwords');
    }
}


It's view

<div>
    <!-- Password -->
    <div class="mt-4">
        <x-input-label for="password" :value="__('Password. Must be strong')" />
        <div class="flex mt-1 mb-2">
            <div class="relative flex-1 col-span-4" x-data="{ show: true }">
                <input class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
                       id="password"
                       :type="show ? 'password' : 'text'"
                       name="password"
                       wire:model="password"
                       wire:keyup="updatePassword"
                       required autocomplete="new-password" />
                <button type="button" class="flex absolute inset-y-0 right-0 items-center pr-3" @click="show = !show" :class="{'hidden': !show, 'block': show }">
                    <!-- Heroicon name: eye -->
                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
                        <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
                    </svg>
                </button>
                <button type="button" class="flex absolute inset-y-0 right-0 items-center pr-3" @click="show = !show" :class="{'block': !show, 'hidden': show }">
                    <!-- Heroicon name: eye-slash -->
                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
                    </svg>
                </button>
            </div>
            <div class="flex items-center place-content-end ml-1">
                <x-primary-button wire:click="generatePassword" type="button">Generate</x-primary-button>
            </div>
        </div>
        <span class="text-sm">
            <span class="font-semibold">Password strength:</span> {{ $strengthLevels[$strengthScore] ?? 'Weak' }}
        </span>
        <div>
            <div class="mt-6" aria-hidden="true">
                <div class="overflow-hidden rounded-full bg-gray-200">
                    <div class="h-2 rounded-full {{ $strengthColors[$strengthScore] }}" style="width: {{ $strengthScore === 0 ? 10 : $strengthScore * 25 }}%"></div>
                </div>
            </div>
        </div>
        {{--  THE PORBLEM: error does'nt show   --}}
        {{--<x-input-error :messages="$errors->get('password')" class="mt-2" />--}}
        <x-input-error :messages="$errors->updatePassword->get('password')" class="mt-2" />
    </div>
    <!-- Confirm Password -->
    <div class="mt-4">
        <x-input-label for="password_confirmation" :value="__('Confirm Password')" />
        <x-text-input id="password_confirmation" class="block mt-1 w-full"
                      type="password"
                      wire:model="passwordConfirmation"
                      name="password_confirmation" required />
        <x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
    </div>
</div>

0 likes
2 replies
LaryAI's avatar
Level 58

To display the error when a user updates to a new password that doesn't meet the minimum strength level in your Livewire component, you need to ensure that the validation errors are properly passed to the Livewire component. Here’s a step-by-step solution to achieve this:

  1. Update the Livewire Component to Handle Validation Errors:

    Modify your UpdatePasswords Livewire component to handle validation errors. You can use the @error directive to display validation errors in the Livewire view.

    namespace App\Livewire;
    
    use Livewire\Component;
    use Illuminate\Support\Str;
    use Illuminate\Contracts\View\View;
    use ZxcvbnPhp\Zxcvbn;
    
    class UpdatePasswords extends Component
    {
        public string $password = '';
        public string $passwordConfirmation = '';
        public int $strengthScore = 0;
        public array $strengthLevels = [
            1 => 'Weak',
            2 => 'Fair',
            3 => 'Good',
            4 => 'Strong',
        ];
        public array $strengthColors = [
            0 => 'bg-red-600',
            1 => 'bg-yellow-600',
            2 => 'bg-orange-600',
            3 => 'bg-lime-600',
            4 => 'bg-green-600',
        ];
    
        public function updatePassword(): void
        {
            $this->validate([
                'password' => ['required', 'confirmed', new PasswordRule],
            ]);
    
            $this->updatedPassword($this->password);
        }
    
        public function updatedPassword($value): void
        {
            $this->strengthScore = (new Zxcvbn())->passwordStrength($value)['score'];
        }
    
        public function generatePassword(): void
        {
            $password = Str::password(12);
            $this->setPasswords($password);
        }
    
        protected function setPasswords($value): void
        {
            $this->password = $value;
            $this->passwordConfirmation = $value;
            $this->updatedPassword($value);
        }
    
        public function render(): View
        {
            return view('livewire.update-passwords');
        }
    }
    
  2. Update the Livewire View to Display Validation Errors:

    Modify the Livewire view to display validation errors using the @error directive.

    <div>
        <!-- Password -->
        <div class="mt-4">
            <x-input-label for="password" :value="__('Password. Must be strong')" />
            <div class="flex mt-1 mb-2">
                <div class="relative flex-1 col-span-4" x-data="{ show: true }">
                    <input class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
                           id="password"
                           :type="show ? 'password' : 'text'"
                           name="password"
                           wire:model="password"
                           wire:keyup="updatePassword"
                           required autocomplete="new-password" />
                    <button type="button" class="flex absolute inset-y-0 right-0 items-center pr-3" @click="show = !show" :class="{'hidden': !show, 'block': show }">
                        <!-- Heroicon name: eye -->
                        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
                            <path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
                            <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
                        </svg>
                    </button>
                    <button type="button" class="flex absolute inset-y-0 right-0 items-center pr-3" @click="show = !show" :class="{'block': !show, 'hidden': show }">
                        <!-- Heroicon name: eye-slash -->
                        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
                            <path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
                        </svg>
                    </button>
                </div>
                <div class="flex items-center place-content-end ml-1">
                    <x-primary-button wire:click="generatePassword" type="button">Generate</x-primary-button>
                </div>
            </div>
            <span class="text-sm">
                <span class="font-semibold">Password strength:</span> {{ $strengthLevels[$strengthScore] ?? 'Weak' }}
            </span>
            <div>
                <div class="mt-6" aria-hidden="true">
                    <div class="overflow-hidden rounded-full bg-gray-200">
                        <div class="h-2 rounded-full {{ $strengthColors[$strengthScore] }}" style="width: {{ $strengthScore === 0 ? 10 : $strengthScore * 25 }}%"></div>
                    </div>
                </div>
            </div>
            @error('password')
                <span class="text-red-600 text-sm">{{ $message }}</span>
            @enderror
        </div>
        <!-- Confirm Password -->
        <div class="mt-4">
            <x-input-label for="password_confirmation" :value="__('Confirm Password')" />
            <x-text-input id="password_confirmation" class="block mt-1 w-full"
                          type="password"
                          wire:model="passwordConfirmation"
                          name="password_confirmation" required />
            @error('password_confirmation')
                <span class="text-red-600 text-sm">{{ $message }}</span>
            @enderror
        </div>
    </div>
    
  3. Update the Form to Use Livewire:

    Ensure that your form uses Livewire for handling the password update. This means you should call the Livewire method instead of submitting the form traditionally.

    <section>
        <header>
            <h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
                {{ __('Update Password') }}
            </h2>
    
            <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
                {{ __('Ensure your account is using a long, random password to stay secure.') }}
            </p>
        </header>
    
        <form wire:submit.prevent="updatePassword" class="mt-6 space-y-6">
            @csrf
            @method('put')
    
            <div>
                <x-input-label for="current_password" :value="__('Current Password')" />
                <x-text-input id="current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" />
                <x-input-error :messages="$errors->updatePassword->get('current_password')" class="mt-2" />
            </div>
    
            <livewire:update-passwords />
    
            <div class="flex items-center gap-4">
                <x-primary-button>{{ __('Save') }}</x-primary-button>
    
                @if (session('status') === 'password-updated')
                    <p
                        x-data="{ show: true }"
                        x-show="show"
                        x-transition
                        x-init="setTimeout(() => show = false, 2000)"
                        class="text-sm text-gray-600 dark:text-gray-400"
                    >{{ __('Saved.') }}</p>
                @endif
            </div>
        </form>
    </section>
    

By following these steps, you ensure that the validation errors are properly displayed in your Livewire component when the password does not meet the required strength level.

muuucho's avatar
Level 11

I do almost the same in Breeze's register process and the errors are displayed correctly. The only difference is that the Breeze's update method's validation looks like this

$validated = $request->validateWithBag('updatePassword', [
            'current_password' => ['required', 'current_password'],
            'password' => ['required', Password::defaults(), 'confirmed', new PasswordRule],
        ]);

I can't extract any errors to my Livewire component's view. However it's working upon registration using Breeze's registration method's validation in 'RegisteredUserController.php'. It looks like this

$request->validate([
                'name' => ['required', 'string', 'max:255'],
                'email' => ['sometimes', 'string', 'lowercase:', 'email', 'max:255', 'unique:'.User::class],
                'password' => ['required', 'confirmed', Rules\Password::defaults(), new PasswordRule],
            ]);

This works. I can echo out the error in the Livewirecomponent's view.

Please or to participate in this conversation.