livewire / flux

The official Livewire UI component library
https://fluxui.dev
476 stars 42 forks source link

<flux:input> with mask keeps wire:dirty toggled #577

Closed danielrona closed 1 week ago

danielrona commented 2 weeks ago

When using mask on flux:input I always end up in dirty state

<flux:input icon="phone" mask="(+99) 999 999999999999" wire:model="form.phone_number" :label="__('Phone')"/>
<div wire:dirty>Unsaved changes...</div>

Image

<flux:input icon="phone" wire:model="form.phone_number" :label="__('Phone')"/>
<div wire:dirty>Unsaved changes...</div>

Image

livewire : * v3.5.12
flux: * 1.0.19
flux-pro: * 1.0.19
├── @alpinejs/collapse@3.14.3
├── @alpinejs/focus@3.14.3
├── @alpinejs/intersect@3.14.3
├── @alpinejs/mask@3.14.3
├── @alpinejs/sort@3.14.3
├── alpinejs@3.14.3

Anyone else having this issue?

jeffchown commented 2 weeks ago

@danielrona I tried your code and it worked fine for me. Can you supply your component's code as well? (re: form.phone_number) Maybe there's something in there that might shed some light on what you're experiencing.

danielrona commented 1 week ago

@jeffchown gladly, this is stripped down and using the sushi driver

Laravel Model Company.php

<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Sushi\Sushi;

final class Company extends Model
{
    use Sushi;

    protected $rows = [
        ['id' => 1,
            'name' => 'Test Company',
            'street' => 'Test Street',
            'street_no' => '1',
            'zip_code' => '12345',
            'city' => 'Test City',
            'country' => 'de',
            'email' => '',
            'phone_number' => '1234567890',
            'created_at' => null,
            'deleted_at' => null,
            'updated_at' => null],
        ['id' => 2,
            'name' => 'Test Company2',
            'street' => 'Test Street',
            'street_no' => '2',
            'zip_code' => '12345',
            'city' => 'Test City',
            'country' => 'de',
            'email' => '',
            'phone_number' => '1234567890',
            'created_at' => null,
            'deleted_at' => null,
            'updated_at' => null,
        ],
    ];
}

Livewire Component CompaniesEdit.php

<?php

declare(strict_types=1);

namespace App\Livewire\Administration;

use App\Livewire\Forms\CompanyForm;
use App\Models\Company;
use App\Traits\WithCountries;
use Livewire\Component;

final class CompaniesEdit extends Component
{
    use WithCountries;

    public CompanyForm $form;

    public function mount(Company $company): void
    {
        $this->form->fill($company);
    }

    public function render()
    {
        return view('livewire.administration.companies-edit');
    }

    public function save(): void
    {
        $this->form->save();
    }
}

Livewire Form CompanyForm.php

<?php

declare(strict_types=1);

namespace App\Livewire\Forms;

use App\Models\Company;
use Flux;
use Livewire\Attributes\Validate;
use Livewire\Form;

final class CompanyForm extends Form
{
    #[Validate('string|nullable')]
    public ?string $city = null;

    #[Validate('string|nullable')]
    public ?string $country = 'de';

    #[Validate('email|nullable')]
    public ?string $email = null;

    #[Validate('integer|nullable')]
    public ?int $id = null;

    #[Validate('required|string|max:255')]
    public ?string $name = null;

    #[Validate('string|nullable')]
    public ?string $old_address = null;

    #[Validate('string|nullable')]
    public ?string $phone_number = null;

    #[Validate('string|nullable')]
    public ?string $street = null;

    #[Validate('string|nullable')]
    public ?string $street_no = null;

    #[Validate('string|nullable')]
    public ?string $zip_code = null;

    public function save(): Company
    {
        $this->validate();
        $company = $this->id ? Company::find($this->id) : new Company();
        $company->fill($this->toArray());
        $company->save();
        Flux::toast(__('Saved successfully'), variant: 'success');

        return $company;
    }
}

Blade File companies-form.blade.php

<div x-data="{input: false}" @keydown.ctrl.i.window.prevent="input = true">
    <div class="flex justify-between items-center">

        <div class="flex items-center justify-center">
          <span class="relative inline-flex">
            <flux:button wire:click="save" kbd="Strg+S" @keydown.ctrl.s.window.prevent="$wire.save()">
            {{ __('Save') }}
            </flux:button>
            <span class="flex absolute h-3 w-3 top-0 right-0 -mt-1 -mr-1 hidden" wire:dirty.class.remove="hidden">
              <span
                  class="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 dark:bg-red-500 opacity-75"></span>
              <span class="relative inline-flex rounded-full h-3 w-3 bg-sky-500 dark:bg-red-500"></span>
            </span>
          </span>
        </div>

        <flux:button variant="ghost" href="{{ route('companies.index') }}" wire:navigate
                     icon-trailing="arrow-uturn-left">
            {{ __('Back') }}
        </flux:button>
    </div>

    <flux:tab.group>
        <flux:tabs>
            <flux:tab name="company">{{ __('Company') }}</flux:tab>
        </flux:tabs>

        <flux:tab.panel name="company">
            <form>
                <div class="grid grid-cols-2 gap-4">
                    <flux:input wire:model="form.name" :label="__('Name')" :badge="__('Required')"
                                @focus="input = false" x-trap="input" kbd="Strg+I"/>

                    <flux:input icon="at-symbol" wire:model="form.email" :label="__('Email')"/>
                </div>

                <div class="grid grid-cols-4 gap-4">
                    <div class="col-span-3">
                        <flux:input wire:model="form.street" :label="__('Street')"/>
                    </div>

                    <flux:input wire:model="form.street_no" :label="__('Street No')"/>
                </div>

                <div class="grid grid-cols-4 gap-4">
                    <flux:input wire:model="form.zip_code" :label="__('Zip Code')"/>

                    <div class="col-span-3">
                        <flux:input wire:model="form.city" :label="__('City')"/>
                    </div>
                </div>

                <div class="grid grid-cols-2 gap-4">
                    <flux:select variant="listbox" :label="__('Country')" wire:model="form.country"
                                 :placeholder="__('Please Choose')" searchable>
                        <flux:option class="option" :value="$form->country">
                            <img class="w-4 aspect-auto mr-4"
                                 src="{{ url('/images/countries/' . $form->country . '.svg') }}"
                                 alt="{{ $form->country }}" loading="lazy">
                            {{ $this->listOfCountries[$form->country] }}
                        </flux:option>
                        @foreach($this->listOfCountries as $idx => $country)
                            @if($idx !== $form->country)
                                <flux:option wire:key="{{ $idx }}">
                                    <img class="w-4 aspect-auto mr-4"
                                         src="{{ url('/images/countries/' . $idx . '.svg') }}" alt="{{ $idx }}"
                                         loading="lazy">
                                    {{ $country }}
                                </flux:option>
                            @endif
                        @endforeach
                    </flux:select>
                    {{-- https://github.com/livewire/flux/issues/577 --}}
{{--                    <flux:input icon="phone" mask="(+99) 999 999999999999" wire:model="form.phone_number"--}}
{{--                                :label="__('Phone')"/>--}}
                                        <flux:input icon="phone" wire:model="form.phone_number" :label="__('Phone')"/>

                </div>
            </form>
        </flux:tab.panel>
    </flux:tab.group>
</div>

With the mask attribute I get Image

Without the blue dot is not showing till the first edit, as it's supposed to.

jeffchown commented 1 week ago

Thanks @danielrona - great re: Sushi. One piece missing - your WithCountries trait

danielrona commented 1 week ago

@jeffchown sorry

WithCountries.php

<?php

declare(strict_types=1);

namespace App\Traits;

trait WithCountries
{
    public array $listOfCountries;

    public function __construct()
    {
        $this->listOfCountries = ['de' => 'Germany', 'fi' => 'Test 1', 'pl' => 'Test 2'];
    }
}
jeffchown commented 1 week ago

@danielrona I tried your code but there were still some dependencies.

Are you able to recreate the issue in a single-file Volt component to make it easier to cut & paste?

danielrona commented 1 week ago

Will do, probably not before Monday though.

joshhanley commented 1 week ago

@danielrona try to get rid of as much code as possible in your demo too while still being able to demonstrate the issue.

Like if the issue is with the flux:input component, then only have that in your component (plus maybe something showing dirty state), and remove everything else. If possible, we would prefer a simple Volt component (or a simple Livewire class and view) that doesn't have any dependencies (no models, sushi, etc.).

If you can do that, it will make investigating the issue quicker and easier. Thanks!

calebporzio commented 1 week ago

Yep, closing this as it's not immediately reproducible. Also I suspect this has nothing to do with flux directly and instead alpine/livewire but I could be wrong.