Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for native form submissions #1240

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

Conversation

joshhanley
Copy link
Member

@joshhanley joshhanley commented Mar 4, 2025

Remaining issues

Read the proposal below first. This is so we don't miss things.

  • Decide whether selectedInitially and state names could be improved
  • Decide whether calendar preset array key should be supported
  • Hidden inputs are not durable if a Livewire request happens (if someone is using Livewire for pieces of a native form submission)
  • Using .append() to add the hidden inputs to the end of the element is causing issues with space-y- utilities as extra margin bottom is being added
  • Combobox not showing pre-selected value (see combobox under select heading in components section below)

The scenario

At the moment, if a user tries to use Flux with native form submissions, such an in a Blade only app, some components won't submit as they are custom components.

Those components are:

  • calendar
  • checkbox
  • date picker
  • editor
  • radio
  • select
  • switch

The proposal

There are two PRs for this, one each for Flux and Flux Pro

Fixes #341

These PRs add support for native form submissions by rendering hidden inputs for components that don't have native support.

The submittable mixin

To get this to work, a submittable mixin has has been created.

Submittable takes 3 pieces of information:

  • name
  • value
  • selectedInitially

Internally there are 3 pieces of information:

  • name
  • value
  • state

Name

name is pulled from the name attribute on the component. If there is no name attribute, the hidden inputs will not render.

Value
value initially is the value attribute pulled from the component and for some components this will be updated as the selectable value changes, such as in radios and date pickers. Where as for checkboxes and switches, once this is set, it should not change. This is the value that will be output to the hidden input.

State
state is a boolean which determines whether the hidden inputs should be rendered or not. State is set by the selectedInitially prop. Typically this tracks the selectable value and is true/false depending on the contents of the selectable value.

The names selectedInitially and state were just copied from selectable. They don't seem to capture exactly what it is for, so these could be improved.

The update process

Because of the different data types and checked/selected statuses, sometimes we need to update the value that submittable is tracking. We then need to update whether the hidden inputs should be rendered or not.

That is why you will see two calls .setValue() and .update() in most of the selection/ update hooks and others will just have .update().

.setValue() updates the internal value of the submittable that will be output to the hidden input.

.update() determines whether the hidden inputs should be rendered on not, based on whether the data that has been passed in has a value or not. For example an empty array/ empty object will not render hidden inputs.

Changes to name prop

Something that needed to change was the mapping of the name prop for the Blade components. Previously the name prop was set to the value of wire:model, if there was one. This name attribute was never forwarded onto the components.

@props([
    'name' => $attributes->whereStartsWith('wire:model')->first(),
    ...
])

But for native form submissions to work correctly, we first need to detect whether a name prop had been passed in. To do this, I've had to set the name prop to null by default. This allows us to detect whether name has been passed in or not by checking if it's null. We can now determine whether name should be rendered on the component as an attribute or not.

@props([
    'name' => null,
    ...
])

If it's null, we don't render the name attribute and instead set it's value to the value of the wire:model for error handling purposes only.

// We only want to show the name attribute it has been set manually
// but not if it has been set from the `wire:model` attribute...
$showName = isset($name);
if (! isset($name)) {
    $name = $attributes->whereStartsWith('wire:model')->first();
}

Where as if name has a value, we shouldn't override it from wire:model and we also need to ensure it gets rendered as an attribute on the component.

@if ($showName) name="{{ $name }}" @endif

Calendar and date picker support for arrays from old() and manually passed values

To support passing values directly from Laravel's old() and request() helpers directly into the calendar and date picker components, I've updated how the value attribute gets processed.

I've made value a prop now, so it can accept strings like is has to date, but it now also supports passing in arrays.

For a range picker, it can accept an associative array with start and end keys and this will be processed into the start/end string format. If one of start or end is missing, then it will just be an empty string.

[
	'start' => '2025-03-05',
	'end' => '2025-03-17',
];

For a multiple picker, it can accept an array with date values, and this array will be imploded into a comma separated string, like date1,date2,date3.

[
	'2025-03-05',
	'2025-03-08',
	'2025-03-14',
]

One issue
The only thing that is not supported is for the range picker, if a preset has been selected, then it will submit an array like the below (see the components section below for more details).

[
	'start' => '2025-03-13',
	'end' => '2025-03-19',
	'preset' => 'last7days'
];

But currently there is no way to pass the preset key into the calendar or date picker components so the preset is selected again.

Should we add support for this?

Hidden inputs not durable

Currently hidden inputs are not durable if a Livewire request happens. So we need to find a way to include them. This is needed if someone is using Livewire for pieces of a native form submission, we don't want Livewire to wipe out any hidden inputs.

Append breaks because of space-y-

We are appending the hidden elements to the end of the component's contents. This causes issues if the component is using space-y- like the checkbox group is, as it's causing margin bottom to appear on the element before the hidden inputs.

<flux:checkbox.group name="notifications" label="Notifications">
    <flux:checkbox label="Push notifications" value="push" :checked="is_array(request()->input('notifications')) && in_array('push', request()->input('notifications2'))" />
    <flux:checkbox label="Email" value="email" :checked="is_array(request()->input('notifications')) && in_array('email', request()->input('notifications'))" />
</flux:checkbox.group>

Without anything selected
Pasted image 20250319144743

With something selected
Notice the space now below the SMS checkbox
Pasted image 20250319144753

Components

Below is a list of all the components with details on how to use them and what they will output.

Calendar/ date picker

The calendar and date picker both return the same data structures, so will be shown together using calendars as the example.

If you just have a calendar component and it has a name and a value, a hidden input will be rendered. If no value is provided initially, then the hidden input will only be rendered once something is selected. If the selection is removed, the hidden input is removed. This is different to how the native date input is handled which returns an empty string "" by default.

<flux:calendar name="calendar" label="Calendar" value="2025-03-19" />

Returns: calendar="2025-03-19"

<flux:calendar name="calendar" label="Calendar" :value="old('calendar', request()->input('calendar'))" />

Returns: calendar="the-last-value"

If a calendar range picker is used and a range is selected, then it will return two pieces of data.

<flux:calendar name="calendarrange" mode="range" label="Calendar Range" :value="old('calendarrange', request()->input('calendarrange'))" />

Returns:

  • calendarrange[start] = "2025-03-04"
  • calendarrange[end] = "2025-03-07"

This is translated by Laravel into a single array:

$calendarRange = [
	'start' => '2025-03-04',
	'end' => '2025-03-07',
];

It's because of this, that the calendar and date picker components were updated to accept arrays being passed into the :value attribute, as the old() and request()->input() will both return an array.

If presets are also used, then preset will be an additional key in the above array.

If a calendar with multiple is used, then it will return a piece of data for each selected date.

<flux:calendar name="calendarmultiple" multiple label="Calendar Multiple" :value="old('calendarmultiple', request()->input('calendarmultiple'))" />

Returns:

  • calendarmultiple[0] = '2025-03-03'
  • calendarmultiple[1] = '2025-03-05'
  • calendarmultiple[2] = '2025-03-07'

This is translated by Laravel into a single array:

$calendarRange = [
	'2025-03-03',
	'2025-03-05',
	'2025-03-07',
];

Checkbox

If a checkbox has a name and a value, a hidden input will be rendered with both of those attributes as long as the checkbox is checked. If it is unchecked, the hidden input is removed. This is inline with the native checkbox which only sends a value if it's checked.

<flux:checkbox name="notifications" value="email" />

Returns: notifications="email"

If a checkbox only has a name and not a value, the value is set to on which is what the browser defaults to on the native checkbox if there is no value.

<flux:checkbox name="notifications" />

Returns: notifications="on"

If a checkbox on page load has the checked attribute, or it's set to true, then the hidden input will be rendered.

<flux:checkbox
    name="notifications"
    value="email"
    :checked="old('notifications', request()->input('notifications'))" />

Checkbox group

Checkbox groups support adding name attributes to the individual checkbox components, but the name needs to use the standard HTML name array syntax notifications[] so it is grouped accordingly and the checkboxes don't overwrite each other when the form submits.

<flux:checkbox.group label="Notifications">
    <flux:checkbox
	    label="Push notifications"
        name="notifications[]"
        value="push"
        :checked="is_array(request()->input('notifications')) && in_array('push', request()->input('notifications'))" />
        
    <flux:checkbox
	    label="Email"
	    name="notifications[]"
        value="email"
        :checked="is_array(request()->input('notifications')) && in_array('email', request()->input('notifications'))" />
</flux:checkbox.group>

But we also have support for just setting the name on the checkbox.group component, so it doesn't need to be set on every checkbox. In this scenario, the checkbox group already knows it's array based, so jus tadding notifications is enough, the array syntax isn't needed.

<flux:checkbox.group name="notifications" label="Notifications">
    <flux:checkbox
	    label="Push notifications"
	    value="push"
	    :checked="is_array(request()->input('notifications')) && in_array('push', request()->input('notifications'))"
	/>
	    
    <flux:checkbox
	    label="Email"
	    value="email"
	    :checked="is_array(request()->input('notifications')) && in_array('email', request()->input('notifications'))"
	/>
</flux:checkbox.group>

Checkboxes can also be used on their own without the checkbox.group component and as long as they are using the HTML name array syntax, they will be grouped together.

<flux:checkbox
	label="Push notifications"
	name="notifications[]"
	value="push"
	:checked="is_array(request()->input('notifications')) && in_array('push', request()->input('notifications3'))"
/>

<flux:checkbox
	label="Email"
	name="notifications3[]"
	value="email"
	:checked="is_array(request()->input('notifications')) && in_array('email', request()->input('notifications3'))"
/>

Editor

The editor component can work in a couple of ways.

The first is with shorthand syntax. A value attribute can be added to the editor component to pass initial data in.

<flux:editor name="content" label="Content" description="Some content" :value="old('content', request()->input('content'))" />

Returns: content="the last value"

But it can also be used long form, where the previous data is passed into the editor.content slot.

<flux:editor name="long-content">
	<flux:editor.toolbar />

	<flux:editor.content>{!! request()->input('long-content') !!}</flux:editor.content>
</flux:editor>

Radio

Radio components cannot be used outside of a radio.group component, so it is required that the name attribute goes on the radio.group component.

<flux:radio.group name="payment" label="Select your payment method">
    <flux:radio value="cc" label="Credit Card" :checked="request()->input('payment') ==='cc'" />
    <flux:radio value="paypal" label="Paypal" :checked="request()->input('payment') ==='paypal'" />
    <flux:radio value="ach" label="Bank transfer" :checked="request()->input('payment') ==='ach'" />
</flux:radio.group>

Returns: payment="ach"

This also works the same way for segmented and card variants.

<flux:radio.group name="role" label="Role" variant="segmented">
    <flux:radio value="admin" label="Admin" :checked="request()->input('role') === 'admin'" />
    <flux:radio value="editor" label="Editor" :checked="request()->input('role') === 'editor'" />
    <flux:radio value="viewer" label="Viewer" :checked="request()->input('role') === 'viewer'" />
</flux:radio.group>

Returns: role="editor"

<flux:radio.group name="shipping" label="Shipping" variant="cards" class="max-sm:flex-col">
    <flux:radio value="standard" label="Standard" description="4-10 business days" :checked="request()->input('shipping') === 'standard'" />
    <flux:radio value="fast" label="Fast" description="2-5 business days" :checked="request()->input('shipping') === 'fast'" />
    <flux:radio value="next-day" label="Next day" description="1 business day" :checked="request()->input('shipping') === 'next-day'" />
</flux:radio.group>

Returns: shipping="standard"

Select

With all selects, the name attribute needs to go on the select component.

If using a default select, if there are no values on the select.option components, then the value of the label will be returned.

<flux:select name="industry-default" placeholder="Choose industry...">
    <flux:select.option :selected="request()->input('industry-default') === 'Photography'">Photography</flux:select.option>
    <flux:select.option :selected="request()->input('industry-default') === 'Design services'">Design services</flux:select.option>
</flux:select>

Returns: industry-default="Design services"

Where as if value attributes are added to each of the select.option components, then those values will be used.

<flux:select name="industry-values" placeholder="Choose industry values...">
    <flux:select.option value="photography" :selected="request()->input('industry-values') === 'photography'">Photography</flux:select.option>
    <flux:select.option value="design-services" :selected="request()->input('industry-values') === 'design-services'">Design services</flux:select.option>
</flux:select>

Returns: industry-values="photography"

If using the listbox variant, the principals are the same as above.

<flux:select name="industry-listbox" variant="listbox" placeholder="Choose industry..." clearable>
    <flux:select.option :selected="request()->input('industry-listbox') === 'Photography'">Photography</flux:select.option>
    <flux:select.option :selected="request()->input('industry-listbox') === 'Design services'">Design services</flux:select.option>
</flux:select>

Returns: industry-listbox="Design services"

<flux:select name="industry-listbox-values" variant="listbox" placeholder="Choose industry listbox values...">
    <flux:select.option value="photography" :selected="request()->input('industry-listbox-values') === 'photography'">Photography</flux:select.option>
    <flux:select.option value="design-services" :selected="request()->input('industry-listbox-values') === 'design-services'">Design services</flux:select.option>
</flux:select>

Returns: industry-listbox-values="photography"

If using the multiple option, there will be a hidden input created for each of the selected options using the array syntax.

<flux:select name="industry-multiple" variant="listbox" multiple placeholder="Choose industries...">
    <flux:select.option :selected="is_array(request()->input('industry-multiple')) && in_array('Photography', request()->input('industry-multiple'))">Photography</flux:select.option>
    <flux:select.option :selected="is_array(request()->input('industry-multiple')) && in_array('Design services', request()->input('industry-multiple'))">Design services</flux:select.option>
    <flux:select.option :selected="is_array(request()->input('industry-multiple')) && in_array('Web development', request()->input('industry-multiple'))">Web development</flux:select.option>
</flux:select>

Returns:

  • industry-multiple[0] = "Photography"
  • industry-multiple[1] = "Web development"

Combobox

The combobox currently has issues, it should work like the other options. But for some reason, it won't load the value attribute and select the correct option and update the input with the selected value.

But it submits happily, which is the main piece of this PR.

<flux:select name="industry-combobox" variant="combobox" placeholder="Choose industry...">
    <flux:select.option :selected="request()->input('industry-combobox') === 'Photography'">Photography</flux:select.option>
    <flux:select.option :selected="request()->input('industry-combobox') === 'Design services'">Design services</flux:select.option>
</flux:select>

Returns: industry-combobox="Design services"

Using value on select.option also works happily (again won't load from value initially).

<flux:select name="industry-combobox-values" variant="combobox" placeholder="Choose industry...">
    <flux:select.option value="photography" :selected="request()->input('industry-combobox-values') === 'photography'">Photography</flux:select.option>
    <flux:select.option value="design-services" :selected="request()->input('industry-combobox-values') === 'design-services'">Design services</flux:select.option>
</flux:select>

Returns: industry-combobox-values="design-services"

Switch

The switch component just needs a name attribute. It will then return "true" if the switch has been selected and not return anything if it hasn't.

<flux:switch name="enable-notifications" label="Enable notifications" :checked="request()->input('enable-notifications')" />

Returns: enable-notifications="true"

@joshhanley joshhanley changed the title WIP - Add support for native form submissions Add support for native form submissions Mar 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Custom inputs aren't working with native form submissions
1 participant