tanthammar / tall-forms

Laravel Livewire (TALL-stack) form generator with realtime validation, file uploads, array fields, blade form input components and more.
https://github.com/tanthammar/tall-forms/wiki
MIT License
694 stars 86 forks source link

livewireValue is not defined #105

Closed swarakaka closed 2 years ago

swarakaka commented 2 years ago

Returns an error when changing a select several times.

module.esm.js?027e:1656 Uncaught ReferenceError: livewireValue is not defined
    at eval (eval at safeAsyncFunction (module.esm.js:1:1), <anonymous>:3:32)
    at eval (module.esm.js?027e:1718:1)
    at tryCatch (module.esm.js?027e:1645:1)
    at HTMLInputElement.el._x_forceModelUpdate (module.esm.js?027e:2974:1)
    at eval (module.esm.js?027e:2985:1)
    at reactiveEffect (module.esm.js?027e:484:1)
    at Object.effect3 [as effect] (module.esm.js?027e:459:1)
    at effect (module.esm.js?027e:1299:1)
    at eval (module.esm.js?027e:2392:1)
    at wrappedEffect (module.esm.js?027e:1315:1)
<?php

namespace App\Http\Livewire\Forms\Persons;

use App\Models\Person;
use App\Models\Staff;
use Illuminate\Support\Collection;
use phpDocumentor\Reflection\Types\Boolean;
use Tanthammar\TallForms\Input;
use Tanthammar\TallForms\Select;
use Tanthammar\TallForms\TallFormComponent;
use Tanthammar\TallFormsSponsors\DatePicker;
use Tanthammar\TallFormsSponsors\Email;
use Tanthammar\TallFormsSponsors\Tel;

class CreateOrUpdatePerson extends TallFormComponent
{
    public Collection $breads;
    public string  $formTitle = '';
    public array $staffs = [];
    public bool $show_birthday_and_certificate = true;
    public function mount(?Person $person)
    {
        $this->formTitle = $person->exists ?
            __('Edit :item ', ['item'=> $person->name])
            : __('Add :model ', ['model'=> __('Person')]);

        $this->breads = new Collection([
            ['text'=> __('Persons'), 'url'=> 'persons'],
            ['text'=> __($this->formTitle)],

        ]);

        //Gate::authorize()
        $this->mount_form($person); // $person from hereon, called $this->model
    }

    protected function formAttr(): array
    {
        return [
            'wrapWithView' => true,
            'showDelete' => false,
        ];
    }

    // OPTIONAL methods, they already exist
    protected function onCreateModel($validated_data)
    {
        $this->model = Person::create($validated_data);
    }

    protected function onUpdateModel($validated_data)
    {
        $this->model->update($validated_data);
    }

    protected function onDeleteModel()
    {
        $this->defaultDelete();
    }

    protected function beforeFormProperties()
    {
        // set the checkboxes options
        $this->staffs = Staff::orderBy('id','desc')->pluck('id', 'name')->all();
    }
    protected function fields(): array
    {
        return [
            Select::make(__('Staff'), 'staff_id')
                ->options($this->staffs)
                ->rules('required')
                ->colspan(6),

            Input::make(__('Name'),'name')
                ->rules('required')
                ->autocomplete('new-text')
                ->colspan(6),

            Email::make(__('Email'),'email')
                ->autocomplete('new-email')
                ->colspan(6),

            Tel::make(__('Phone'),'phone')
                ->autocomplete('new-tel')
                ->colspan(6),

           $this->show_birthday_and_certificate ? DatePicker::make(__('Birthday'),'birthday')
                ->locale('en')
                ->placeholder('Select a date...')
                ->includeExternalScripts() //only included once, if multiple DatePicker fields
                ->colspan(6) : null,

           $this->show_birthday_and_certificate ? Input::make(__('Certificate'),'certificate')
                ->rules('nullable')
                ->autocomplete("off")
                ->colspan(6) : null,

            Input::make(__('Address'),'address')
                ->rules('nullable')
                ->autocomplete('new-address'),
        ];
    }

    public function updatedStaffId($value)
    {
        if(filled($value) && ($value === "2" || $value === "5")){
            $this->show_birthday_and_certificate = false;
        }else{
            $this->show_birthday_and_certificate = true;
        }
    }

}
tanthammar commented 2 years ago

There are two problems. It is impossible to use "includeExternalScripts" with a conditional field. The external script is pushed to 'scripts' if the value is true, when Livewire mounts the component, and removed when the value becomes false, (and it can't be pushed later). The other problem is that the component won't load "birthday" and "certificate" if the condition is false when the component is mounted.

To get around it you will have to

  1. Bundle the Flatpickr script in your app.js and remove the "includeExternalScripts" in your field declaration. This also applies to Flatpickr locales.
  2. Manually set "birthday" and "certificate" in your updatedStaffId method
    public function updatedStaffId($value)
    {
        if(filled($value) && ($value === "2" || $value === "5")){
            $this->show_birthday_and_certificate = false;
        } else {
            data_set($this, 'form_data.birthday', ($this->model->birthday ?? "") );
            data_set($this, 'form_data.certificate', ($this->model->certificate ?? "") );
            $this->show_birthday_and_certificate = true;
        }
    }
swarakaka commented 2 years ago

Thank you for your answer, the problem wasn't solved, but it's the same.

tanthammar commented 2 years ago

@swara-mohammed I tested your code and there is a bug. Will work on it today.

tanthammar commented 2 years ago

Hello again @swara-mohammed

I figured out what is going wrong here :)

When Livewire hides the DatePicker, FlatPickr doesn't destroy the instance, so "half" of the html is still in the DOM. Alpine cant find it's x-data, because it is removed.

I have a wire:ignore set on the FlatPickr div, which I can't remove because Livewire will destroy the instance on each render...

So the only way I can figure out at this moment is to add class hidden to the field root. This means that you can use ->includeExternalScripts(). Also please install latest version, I found a bug when merging rootAttr with an empty array.

OBSERVE

When hiding the field with classes, the data properties will always exist in $form_data and $validated_data This is important for you to know in onCreateModel($validated_data) and onUpdateModel($validated_data)

You might want to filter out those properties when saving your component. Depending on the state of $show_birthday_and_certificate

Suggestion for your component.

<?php

namespace App\Http\Livewire\Forms\Persons;

use App\Models\Person;
use App\Models\Staff;
use Illuminate\Support\Collection;
use Tanthammar\TallForms\Input;
use Tanthammar\TallForms\Select;
use Tanthammar\TallForms\TallFormComponent;
use Tanthammar\TallFormsSponsors\DatePicker;
use Tanthammar\TallFormsSponsors\Email;
use Tanthammar\TallFormsSponsors\Tel;

class CreateOrUpdatePerson extends TallFormComponent
{
    public Collection $breads;
    public string $formTitle = '';
    public array $staffs = [];
    public bool $show_birthday_and_certificate = true;

    public function mount(?Person $person)
    {
        //Gate::authorize()

        $this->formTitle = $person->exists ?
            __('Edit :item ', ['item' => $person->name])
            : __('Add :model ', ['model' => __('Person')]);

        $this->breads = new Collection([
            ['text' => __('Persons'), 'url' => 'persons'],
            ['text' => __($this->formTitle)],

        ]);

        // set the checkboxes options
        $this->staffs = Staff::orderBy('id', 'desc')->pluck('id', 'name')->all();

        $this->mount_form($person); // $person from hereon, called $this->model
    }

    protected function formAttr(): array
    {
        return [
            'formTitle' => $this->formTitle,
            'wrapWithView' => true,
            'showDelete' => false,
            'inline' => true
        ];
    }

    protected function onCreateModel($validated_data): void
    {
        Person::create($this->filterValidatedData($validated_data));
    }

    protected function onUpdateModel($validated_data): void
    {
        $this->model->update($this->filterValidatedData($validated_data));
    }

    protected function filterValidatedData($validated_data): array
    {
        return $this->show_birthday_and_certificate ? $validated_data : \Arr::except($validated_data, ['birthday', 'certificate']);
    }

    //optional method, identical to parent
    protected function onDeleteModel()
    {
        $this->defaultDelete();
    }

    public function updatedStaffId($value): void
    {
        if (($value === "2" || $value === "5") && filled($value)) {
            $this->show_birthday_and_certificate = false;
        } else {
            $this->show_birthday_and_certificate = true;
        }
    }

    protected function fields(): array
    {
        return [
            Select::make(__('Staff'), 'staff_id')
                ->options($this->staffs, false) //I think your data is [ $id => $label ]? If not, remove false, or change pluck()
                ->rules('required'),

            Input::make(__('Name'), 'name')
                ->rules('required')
                ->autocomplete('new-text'),

            Email::make(__('Email'), 'email')
                ->autocomplete('new-email'),

            Tel::make(__('Phone'), 'phone')
                ->autocomplete('new-tel'),

            DatePicker::make(__('Birthday'), 'birthday')
                ->rootAttr($this->show_birthday_and_certificate ? [] : ['class' => 'hidden'], true)
                ->locale('en')
                ->placeholder('Select a date...')
                ->includeExternalScripts(), //only included once, if multiple DatePicker fields

            Input::make(__('Certificate'), 'certificate')
                ->rootAttr($this->show_birthday_and_certificate ? [] : ['class' => 'hidden'], true)
                ->rules('nullable')
                ->autocomplete("off"),

            Input::make(__('Address'), 'address')
                ->rules('nullable')
                ->autocomplete('new-address'),
        ];
    }
}
tanthammar commented 2 years ago

I will add this to the documentation as well.

swarakaka commented 2 years ago

Thank you for your solution, but I felt another bug. I'll write the solution below.

implode(): Argument #2 ($array) must be of type ?array, string given

protected function mergeClasses(string $key, array $custom): void
    {
        $merged = array_merge_recursive($this->attributes[$key], $custom);
        if (Arr::has($merged, 'class')) {
            $merged['class'] = implode(" ", $merged['class']);
        }
        $this->attributes[$key] = $merged;
    }

solved:

protected function mergeClasses(string $key, array $custom): void
    {
        $merged = array_merge_recursive($this->attributes[$key], $custom);
        if (Arr::has($merged, 'class') && is_array($merged['class'])) {
            $merged['class'] = implode(" ", $merged['class']);
        }
        $this->attributes[$key] = $merged;
    }
tanthammar commented 2 years ago

@swara-mohammed Yes, that is why I wrote you need to upgrade to the latest version. I discovered that bug when I tested your component. :)