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

nrudolph's avatar

Best way to handle livewire form data across parent-child components

TLDR: How can I bind form fields in a child component to properties of a Livewire Form Object instance in the parent component?

Context: I am working on a CRM-like program with TALL stack, and one component of this application is editing and creating Event Records each of which belongs to a Event Record Type. I have a parent component EventRecordModal, when a user edits an existing Event Record, the application gets the Event Record Type of that record, and gets the classname of the Livewire Form Object associated with that Event Record Type, initializes the class, and calls the load method, and assigns the class to a public property like $form. Here is one of my Form Objects so you can see the structure

<?php

namespace App\ClientSpecificLogic\Logic\ModelInterfaces\EventRecord;

use Livewire\Form;
use App\Models\EventRecord;
use App\ClientSpecificLogic\Logic\Factories\EventRecordFactory;
use App\Models\Customer;
use App\Models\EventRecordType;
use App\Models\SalesRep;
use Livewire\Attributes\Validate;
use App\Rules\PhoneNumber;
use App\Rules\FullText;

class PhoneCallEventRecord extends Form
{
    protected $jsonSchemaVersion = EventRecordFactory::JSON_SCHEMA_VERSION;
    protected $recordTypeId = 1;
    protected ?EventRecord $record = null;
    public $eventable;
    public SalesRep $owner;
    public EventRecordType $recordType;

    #[Validate(['required', 'date'])]
    public $timestamp;

    #[Validate(['required', new PhoneNumber])]
    public $owner_email;

    #[Validate(['required', new PhoneNumber])]
    public $customer_email;

    #[Validate(['required', new FullText])]
    public $content;
    
    #[Validate(['nullable', new FullText])]
    public $notes;

    public function isDirty() {
        // Define dirty logic here
    }

    public function load(int $id)
    {
        $this->record = EventRecord::with('recordType', 'ownerInfo', 'eventable')->findOrFail($id);
        $this->populateFromEventRecord($this->record);
    }

    public function loadNew($eventRecordTypeId, $customerId)
    {
        $this->record = EventRecordFactory::create($eventRecordTypeId, $customerId);
        $this->populateFromEventRecord($this->record);
    }

    public function save()
    {
        $data = [
            'record_type_id' => $this->recordTypeId,
            'timestamp' => $this->timestamp,
            'custom_data' => [
                'owner_number' => $this->owner_email,
                'customer_number' => $this->customer_email,
                'content' => $this->content,
                'notes' => $this->notes,
            ],
        ];

        if ($this->record->exists) {
            $this->record->update($data);
        } else {
            $this->record->create($data);
        }
    }

    public function validateForExport()
    {
        //Doesn't get exported
    }

    private function populateFromEventRecord(EventRecord $eventRecord)
    {
        $this->record = $eventRecord;
        $this->eventable = $eventRecord->eventable;
        $this->owner = $eventRecord->ownerInfo;
        $this->recordType = $eventRecord->recordType;
        $this->timestamp = $eventRecord->timestamp;
        $this->owner_email = $eventRecord->custom_data['owner_number'];
        $this->customer_email = $eventRecord->custom_data['customer_number'];
        $this->content = $eventRecord->custom_data['content'];
        $this->notes = $eventRecord->custom_data['notes'];
    }
}

Then I can bind form fields to properties of the Livewire Form Object like this

<input wire:model="form.timestamp" />

I have all this working but here is the problem

The Problem: I would like the parent component EventRecordModal to handle initializing the Livewire Form Object class, and contain the buttons for saving, deleting, etc. and generally manage the loaded data. Although there will be different Form Object classes for each Event Record Type they will have consistent methods such as save, isDirty, etc. I also would like to to extract all the form fields, their styling, etc. to a separate component that is then loaded in EventRecordModal. The problem then arises of how do I bind the form fields in the child component to properties in the parent component? I know that I could potentially pass the instance of the Form Object class as a property to the child component and then on change emit it back to the parent component but I really do not want to do this as I have to deal with serializing and deserializing the class repeatedly as well as just being generally inefficient and messy. Plus to have true reactivity in the parent component I will have to be emitting changes to the class basically every time the user does any input. I also know I could avoid this issue by initializing and managing the Form Object class in the child component but I am reluctant to do this as then I will have repeated code such as initialization logic, form controls such as Save Buttons, etc. in each of my child components for each of my Event Record Types.

Thank you for any help or recommendations!

0 likes
1 reply
Braunson's avatar
Braunson
Best Answer
Level 18

To bind form fields in a child component to properties of a Livewire Form Object instance in the parent component, you can use a shared state approach. Here's how you can do it:

  1. Use a Form Object:

    Define a form object instance in the parent component and bind the child component fields to this instance.

  2. Emit Events for Update:

    Have the child component emit events to update the form object in the parent component.

Here's an example:

Parent Component

namespace App\Http\Livewire;

use Livewire\Component;

class ParentComponent extends Component
{
    public $form;

    public function mount()
    {
        $this->form = (object)[
            'name' => '',
            'email' => '',
            'additionalField' => ''
        ];
    }

    protected $listeners = ['updateFormFromChild'];

    public function updateFormFromChild($data)
    {
        foreach ($data as $key => $value) {
            $this->form->$key = $value;
        }
    }

    public function render()
    {
        return view('livewire.parent-component');
    }

    public function submit()
    {
        // Handle the form submission logic
    }
}
<div>
    <form wire:submit.prevent="submit">
        <input type="text" wire:model="form.name" placeholder="Name">
        <input type="text" wire:model="form.email" placeholder="Email">
        @livewire('child-component', ['form' => $form])
        <button type="submit">Submit</button>
    </form>
</div>

Child Component

namespace App\Http\Livewire;

use Livewire\Component;

class ChildComponent extends Component
{
    public $form;

    public function mount($form)
    {
        $this->form = $form;
    }

    public function updatedForm()
    {
        $this->emitUp('updateFormFromChild', (array)$this->form);
    }

    public function render()
    {
        return view('livewire.child-component');
    }
}
<div>
    <input type="text" wire:model="form.additionalField" placeholder="Additional Field">
</div>

Explanation

  1. Initialization:

    • Parent Component: The parent component initializes the form object with the necessary properties. This form object is passed down to the child component.
  2. Binding and Reactivity:

    • Parent Component: It binds its form fields to the properties of the form object.
    • Child Component: It binds its additional field to the same form object received from the parent.
  3. Event Emission and Update:

    • Child Component: When any field within the child component (like additionalField) is updated, the updatedForm method emits an event updateFormFromChild with the updated form object.
    • Parent Component: It listens for this event and updates the relevant properties of its form object.

This approach ensures that all form fields, whether in the parent or child component, are bound to the same form object instance, maintaining a consistent state across both components.

Please or to participate in this conversation.