livewire / flux

The official Livewire UI component library
445 stars 38 forks source link

flux:button not submitting #510

Open aydinza opened 3 hours ago

aydinza commented 3 hours ago

Hey guys, In my jetstream project, I have a livewire class and a form for it, to create a new company model. create-company form not submitting. I have a jetstream project, installed flux-ui. This form does not submit and save a "Company" model.

resources/views/livewire/create-company.blade.php

<form wire:submit.prevent="save">
    @csrf
    <div>
        <flux:card class="flex items-center justify-center">
            <flux:tab.group>
                <flux:tabs class="px-4 mt-4">
                    <flux:tab name="company">Company</flux:tab>
                    ...
                </flux:tabs>

                <!-- Company Info Tab Panel -->
                <flux:tab.panel name="company">
                    <flux:fieldset class="mx-8 mb-8">
                        <flux:legend>
                            <flux:badge color="zinc" class="mr-3 font-bold">1</flux:badge> Company Info
                        </flux:legend>
                        <div class="space-y-6">
                            <flux:input wire:model.blur="company.name" label="Company Name" type="text"
                                placeholder="Company legal name" />
                            @error('company.name') <span class="error">{{ $message }}</span> @enderror

                            // Other inputs 

                        </div>
                    </flux:fieldset>
                </flux:tab.panel>

                <!-- Contact Person Tab Panel -->

                <!-- BankAccount Info Tab Panel -->

                <!-- Address Info Tab Panel -->

            </flux:tab.group>
        </flux:card>
        <flux:button type="submit" variant="primary" wire:click="save">
            Submit
        </flux:button>
    </div>
</form>

app/Livewire/CreateCompany.php

<?php

namespace App\Livewire;

use App\Livewire\Forms\CompanyForm;
use Livewire\Component;
use App\Models\Company;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
use App\Traits\Addressable;
use App\Traits\BankAccountable;
use App\Traits\Contactable;

class CreateCompany extends Component
{
    use Addressable, BankAccountable, Contactable;

    public CompanyForm $form;

    public function mount()
    {
        $this->form = new CompanyForm($this, 'create-company'); // Pass the component and the form name
        $this->form->addresses = [];
        $this->form->bankAccounts = [];
        $this->form->contacts = [];
    }

    public function save()
{
    try {
        $this->form->store();
        session()->flash('message', 'Company created successfully!'); // Add flash message
        return $this->redirect('/companies');
    } catch (ValidationException $e) {
        Log::error('Company creation failed: ' . $e->getMessage());
        foreach ($e->validator->errors()->all() as $error) {
            $this->addError('form', $error); // Add individual error messages
        }
    }
}

    // Methods for managing contacts
    public function addContact()
    {
        $this->form->contacts[] = [
            'first_name' => '',
            'last_name' => '',
            'ext' => '',
            'fax' => '',
            'email' => '',
            'mobile' => ''
        ];
    }

    public function removeContact($index)
    {
        unset($this->form->contacts[$index]);
        $this->form->contacts = array_values($this->form->contacts); // Re-index array
    }

    // Methods for managing bank accounts
    public function addBankAccount()
    {
        $this->form->bankAccounts[] = [
            'name' => '',
            'branch' => '',
            'swift_bic' => '',
            'account' => '',
            'currency' => 'USD',
            'iban' => ''
        ];
    }

    public function removeBankAccount($index)
    {
        unset($this->form->bankAccounts[$index]);
        $this->form->bankAccounts = array_values($this->form->bankAccounts); // Re-index array
    }

    // Methods for managing addresses using the Addressable trait
    public function addAddress()
    {
        $this->form->addresses[] = [
            'type' => '',
            'alias' => '',
            'line_1' => '',
            'line_2' => '',
            'city' => '',
            'state' => '',
            'postal_code' => '',
            'country' => ''
        ];
    }

    public function removeAddress($index)
    {
        unset($this->form->addresses[$index]);
        $this->form->addresses = array_values($this->form->addresses); // Re-index array
    }

    public function storeAddresses($company)
    {
        foreach ($this->form->addresses as $addressData) {
            $company->addAddress($addressData);
        }
    }

    public function render()
    {
        return view('livewire.create-company');
    }
}

app/Livewire/Forms/CompanyForm.php

<?php

namespace App\Livewire\Forms;

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

class CompanyForm extends Form
{
    #[Validate(['required', 'string', 'max:255', 'unique:companies,name', 'unique:companies,tax_id', 'unique:companies,email'])]
    public $company = [
        'name' => '', 'tax_office' => '', 'tax_id' => '', 'phone' => '', 'fax' => '', 'web_site' => '', 'email' => '',
    ];

    #[Validate([
        'contacts.*.first_name' => 'required|string|max:255',
        'contacts.*.last_name' => 'required|string|max:255',
        'contacts.*.ext' => 'nullable|string|max:10',
        'contacts.*.fax' => 'nullable|string|max:15|regex:/^[0-9\s\-\(\)]+$/',
        'contacts.*.email' => 'required|email|max:255',
        'contacts.*.mobile' => 'required|string|max:15|regex:/^[0-9\s\-\(\)]+$/'
    ])]
    public $contacts = [
        ['first_name' => '', 'last_name' => '', 'ext' => '', 'fax' => '', 'email' => '', 'mobile' => ''],
    ];

    #[Validate([
        'bankAccounts.*.name' => 'required|string|max:255',
        'bankAccounts.*.branch' => 'required|string|max:255',
        'bankAccounts.*.swift_bic' => 'nullable|string|max:11|regex:/^[A-Za-z0-9]{8,11}$/',
        'bankAccounts.*.account' => 'required|string|max:30',
        'bankAccounts.*.currency' => 'required|string|in:USD,EUR,TRY',
        'bankAccounts.*.iban' => 'required|string|max:34|regex:/^[A-Z0-9]+$/'
    ])]
    public $bankAccounts = [
        ['name' => '', 'branch' => '', 'swift_bic' => '', 'account' => '', 'currency' => 'USD', 'iban' => ''],
    ];

    #[Validate([
        'addresses.*.type' => 'required|string|in:invoice,delivery,warehouse',
        'addresses.*.alias' => 'nullable|string|max:255',
        'addresses.*.line_1' => 'required|string|max:255',
        'addresses.*.line_2' => 'nullable|string|max:255',
        'addresses.*.city' => 'required|string|max:255',
        'addresses.*.state' => 'nullable|string|max:255',
        'addresses.*.postal_code' => 'required|string|max:10',
        'addresses.*.country' => 'required|string|max:255'
    ])]
    public $addresses = [
        ['type' => '', 'alias' => '', 'line_1' => '', 'line_2' => '', 'city' => '', 'state' => '', 'postal_code' => '', 'country' => ''],
    ];

    public function store()
    {
        // Validate all fields
        $this->validate();

        // Create the company
        $company = Company::create($this->form->company);

        // Create the contacts for the company
        foreach ($this->contacts as $contact) {
            $company->contacts()->create($contact);
        }

        // Create the bank accounts for the company
        foreach ($this->bankAccounts as $bankAccount) {
            $company->bankAccounts()->create($bankAccount);
        }

        // Create the addresses for the company
        foreach ($this->addresses as $address) {
            $company->addresses()->create($address);
        }

        // Reset the form after successful submission
        $this->reset([
            'company',
            'contacts',
            'bankAccounts',
            'addresses'
        ]);
        $this->resetErrorBag(); // Reset error messages
    }
}

I don't know what's missing, but this does not submit the form.

aydinza commented 3 hours ago

Actually, this form had tab navigation on bottom, "save & next" and "back" for each tab, but had to remove to make it more simple. Still could not diagnose the problem. No matter what I try, the form does not submit.

jeffchown commented 3 hours ago

@aydinza People aren't going to be able to test your issue without copies of the Traits you use via use Addressable, BankAccountable, Contactable;

...and the Company model, etc.

Are you able to recreate the issue with a simplified code example?

aydinza commented 3 hours ago

Addressable.php trait

<?php

namespace App\Traits;

use App\Models\Address;
use Illuminate\Database\Eloquent\Relations\MorphMany;

trait Addressable
{
    /**
     * Get all of the model's addresses.
     */
    public function addresses(): MorphMany
    {
        return $this->morphMany(Address::class, 'addressable');
    }

    /**
     * Add an address to the model.
     */
    public function addAddress(array $addressData)
    {
        return $this->addresses()->create($addressData);
    }

    /**
     * Add multiple addresses to the model.
     */
    public function addAddresses(array $addressesData)
    {
        foreach ($addressesData as $addressData) {
            $this->addAddress($addressData);
        }
    }

    /**
     * Get a formatted address string for the model.
     * This will return a string of the first address found.
     */
    public function getFormattedAddress()
    {
        $address = $this->addresses()->first();

        if ($address) {
            return "{$address->line_1}, {$address->line_2}, {$address->city}, {$address->state}, {$address->postal_code}, {$address->country}, {$address->type}, {$address->alias}";
        }

        return 'No address available';
    }

    /**
     * Get all formatted addresses as an array.
     */
    public function getAllFormattedAddresses()
    {
        return $this->addresses->map(function ($address) {
            return "{$address->line_1}, {$address->line_2}, {$address->city}, {$address->state}, {$address->postal_code}, {$address->country}, {$address->type}, {$address->alias}";
        })->toArray();
    }
}

Contactable.php trait

<?php

namespace App\Traits;

use App\Models\Contact;
use Illuminate\Database\Eloquent\Relations\morphMany;

trait Contactable
{
    /**
     * Get all of the model's contacts.
     */
    public function contacts(): morphMany
    {
        return $this->morphMany(Contact::class, 'contactable');
    }

    /**
     * Add a contact to the model.
     */
    public function addContact(array $contactData)
    {
        return $this->contacts()->create($contactData);
    }

    /**
     * Add multiple contacts to the model.
     */
    public function addContacts(array $contactsData)
    {
        $failedContacts = [];

        foreach ($contactsData as $contactData) {
            try {
                $this->addContact($contactData);
            } catch (\Exception $e) {
                // Log the error or add the contact data to the failed array
                $failedContacts[] = $contactData;
            }
        }

        return $failedContacts; // Return any failed contacts for further handling
    }
}

BankAccountable.php trait

<?php

namespace App\Traits;

use App\Models\BankAccount;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Model;

trait BankAccountable
{
    /**
     * Get all of the model's bank accounts.
     *
     * @return MorphMany
     */
    public function bankAccounts(): MorphMany
    {
        return $this->morphMany(BankAccount::class, 'bankable');
    }

    /**
     * Add a bank account to the model.
     *
     * @param array $bankAccountData
     * @return Model
     */
    public function addBankAccount(array $bankAccountData): Model
    {
        return $this->bankAccounts()->create($bankAccountData);
    }

    /**
     * Add multiple bank accounts to the model.
     *
     * @param array $bankAccountsData
     * @return void
     */
    public function addBankAccounts(array $bankAccountsData): array
{
    $createdBankAccounts = [];

    \DB::transaction(function () use ($bankAccountsData, &$createdBankAccounts) {
        foreach ($bankAccountsData as $bankAccountData) {
            $createdBankAccounts[] = $this->addBankAccount($bankAccountData);
        }
    });

    return $createdBankAccounts;
}

    /**
     * Get a specific bank account by ID.
     *
     * @param int $accountId
     * @return BankAccount|null
     */
    public function getBankAccountById(int $accountId): ?BankAccount
    {
        return $this->bankAccounts()->find($accountId);
    }

    /**
     * Update a bank account by ID.
     *
     * @param int $accountId
     * @param array $bankAccountData
     * @return bool
     */
    public function updateBankAccount(int $accountId, array $bankAccountData): bool
    {
        $bankAccount = $this->getBankAccountById($accountId);

        if ($bankAccount) {
            return $bankAccount->update($bankAccountData);
        }

        return false;
    }

    /**
     * Delete a bank account by ID.
     *
     * @param int $accountId
     * @return bool
     */
    public function deleteBankAccount(int $accountId): bool
    {
        $bankAccount = $this->getBankAccountById($accountId);

        if ($bankAccount) {
            return $bankAccount->delete();
        }

        return false;
    }
}
aydinza commented 2 hours ago

and the Company.php model

<?php

namespace App\Models;

use App\Models\Address;
use App\Models\BankAccount;
use App\Models\Contact;
use App\Traits\Addressable;
use App\Traits\BankAccountable;
use App\Traits\Contactable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;

class Company extends Model
{
    use Addressable, BankAccountable, Contactable, HasFactory;

    protected $fillable = [
        'name',
        'tax_office',
        'tax_id',
        'phone',
        'fax',
        'web_site',
        'email',
    ];

// Other functions...

    public function contacts()
    {
        return $this->hasMany(Contact::class);
    }

    public function bankAccounts()
    {
        return $this->hasMany(BankAccount::class);
    }

    public function addresses()
    {
        return $this->hasMany(Address::class);
    }
}
jeffchown commented 1 hour ago

@aydinza You're example is still very complex and would require someone to create multiple database tables/migrations, Models, etc. (e.g. Company, Contact, Address, ...) to try and help you diagnose the issue.

My recommendation would be to create and provide a simplified code example that exhibits the same issue, and is easy for people to cut & paste.

jeffchown commented 1 hour ago

One quick thing I noticed, is that your submit button does not need the wire:click="save"

Your form already calls save on submit: <form wire:submit.prevent="save"> and your button is type="submit" so, when it is clicked it will execute the wire:submit in the form

Maybe try changing the submit button from:

<flux:button type="submit" variant="primary" wire:click="save">

to

<flux:button type="submit" variant="primary">

jeffchown commented 1 hour ago

...and, more importantly, in your Form's store() method, shouldn't:

$company = Company::create($this->form->company);

be

$company = Company::create($this->company);

hugoaf commented 1 hour ago

One quick thing I noticed, is that your submit button does not need the wire:click="save"

Your form already calls save on submit: <form wire:submit.prevent="save"> and your button is type="submit" so, when it is clicked it will execute the wire:submit in the form

Maybe try changing the submit button from:

<flux:button type="submit" variant="primary" wire:click="save">

to

<flux:button type="submit" variant="primary">

I believe original code might cause double submission.

This issue needs clarification, is totally different issue the form not being submitted and the task after submitting not being performed.

I recommend the following troubleshooting.

  1. to determine if there is a flux issue, just replace the button with a regular <button type='button'>Send</button>.
  2. Add a dd('submitted') to the
    public function save()
    {
    dd('submitted');
    ...
aydinza commented 45 minutes ago

I updated below caps

<flux:button type="submit" variant="primary">

and

$company = Company::create($this->company);

no effect.

I've tried the standard button solution, but tried again right now. Negative.

            </flux:tab.group>
        </flux:card>
        <button type="button">Send</button>
    </div>
</form>

I also tried the CreateCompany.php update

public function save()
{
    dd('submitted');
    try {
        $this->form->store();
        session()->flash('message', 'Company created successfully!'); // Add flash message
        return $this->redirect('/companies');
    } catch (ValidationException $e) {
        Log::error('Company creation failed: ' . $e->getMessage());
        foreach ($e->validator->errors()->all() as $error) {
            $this->addError('form', $error); // Add individual error messages
        }
    }
}
aydinza commented 42 minutes ago

@hugoaf @jeffchown

What's worse, I dont even get the validation errors when trying to submit without filling anything.

hugoaf commented 34 minutes ago

If you have replaced the flux button for a regular button and by "negative" you mean that the form still not being submitted and your didn't see the dd() output, then I believe this issue is not related to flux.