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

wire:modeling checkboxes to sub-key of array adds extra "" checked item #896

Open
3 tasks done
benborgers opened this issue Dec 19, 2024 · 5 comments
Open
3 tasks done
Assignees

Comments

@benborgers
Copy link

Flux version

v1.1.0

Livewire version

v3.5.17

What is the problem?

When a set of Flux checkboxes wire:modeled to someProperty.subkey, the first item in the array is "".

For example: ["", "first_selection", "second_selection"]. When nothing is selected, it's [""].

2024-12-19.at.12.32.29.mp4

Code snippets

(not Volt, sorry!)

<?php

namespace App\Livewire;

use Livewire\Component;

class Reproduction extends Component
{
    public array $notifications = [];
}
<div>
    @json($notifications)

    <flux:checkbox.group wire:model.live="notifications.subkey" label="Notifications">
        <flux:checkbox label="Push notifications" value="push" />
        <flux:checkbox label="Email" value="email" />
        <flux:checkbox label="In-app alerts" value="app" />
        <flux:checkbox label="SMS" value="sms" />
    </flux:checkbox.group>
</div>

How do you expect it to work?

If zero checkboxes are checked, the array's length should be 0; if 1 checkbox is checked, the array's length should be 1; etc.

Please confirm (incomplete submissions will not be addressed)

  • I have provided easy and step-by-step instructions to reproduce the bug.
  • I have provided code samples as text and NOT images.
  • I understand my bug report will be closed if I haven't met the criteria above.
@jeffchown
Copy link

@benborgers To avoid the extra blank entry, try initializing the subkey element in your property definition or in your mount() method, e.g.:

public array $notifications = [
    'subkey' => [],
];

or

public function mount()
{
    $this->notifications['subkey'] = [];
}

@benborgers
Copy link
Author

That works, thank you! Is binding to an undefined key undefined behavior in Livewire? Or is this a different code path that has a small bug?

@jeffchown
Copy link

jeffchown commented Dec 19, 2024

You're welcome @benborgers !

To the best of my understanding, if you don't initialize an array used in a flux.*.group's wire:model, then Flux initializes it to an empty array and I suspect the extra blank element occurs during this process and/or the related serialization (though I'm sure @calebporzio @joshhanley could provide a better answer)

In my apps, I make sure I initialize all properties used in my wire:models to expected defaults to avoid any such 'auto-initializations' and their related potential side-effects.

@joshhanley
Copy link
Member

joshhanley commented Dec 19, 2024

@benborgers thanks for the report! Yeah seems like a bug in how the checkbox groups are initialised.

For future reference, this is what the Volt component would look like (basically the same, just one file) 😉

<?php

use Livewire\Volt\Component;

new class extends Component {
    public array $notifications = [];
};
?>

<div>
    <div>
        @json($notifications)

        <flux:checkbox.group wire:model.live="notifications.subkey" label="Notifications">
            <flux:checkbox label="Push notifications" value="push" />
            <flux:checkbox label="Email" value="email" />
            <flux:checkbox label="In-app alerts" value="app" />
            <flux:checkbox label="SMS" value="sms" />
        </flux:checkbox.group>
    </div>
</div>

@joshhanley joshhanley self-assigned this Jan 5, 2025
@joshhanley
Copy link
Member

I've investigated this and found the issue. I've submitted a PR with a fix.

Here's the PR description for anyone that is interested.

The scenario

Currently if you wire:model a checkbox group to an empty property and then select a checkbox, the resulting array has an addition empty string item prepended to it.

image
<?php

use Livewire\Volt\Component;

new class extends Component {
    public $notifications;
};
?>

<div>
    @json($notifications)   

    <flux:checkbox.group wire:model.live="notifications" label="Notifications">
        <flux:checkbox label="Push notifications" value="push" />
        <flux:checkbox label="Email" value="email" />
        <flux:checkbox label="In-app alerts" value="app" />
        <flux:checkbox label="SMS" value="sms" />
    </flux:checkbox.group>
</div>

The problem

The problem is that it is never being initialised to an empty array like it should be. Instead internally it's state is being initialised to an empty string. So then when a new item is added, we check to see if it currently is an array, and if not call Array.from(this.state) which is then creating an array where the first item is an empty string.

getState() {
    return this.options().multiple ? Array.from(this.state) : this.state
}

The solution

The reason it was never being initialised to an empty array is because there was a check for this.multiple instead of this.options().multiple in SelectableGroup in selectable.js.

if (value === null) value = this.options().multiple ? [] : ''

So this PR fixes that.

The bonus scenario

But there is also an issue where if the property is not initialised, for example if you are binding to a key in array that doesn't yet exist, like settings.notifications, the same issue happens.

image
<?php

use Livewire\Volt\Component;

new class extends Component {
    public $settings = [];
};
?>

<div>
    @json($settings)   

    <flux:checkbox.group wire:model.live="settings.notifications" label="Notifications">
        <flux:checkbox label="Push notifications" value="push" />
        <flux:checkbox label="Email" value="email" />
        <flux:checkbox label="In-app alerts" value="app" />
        <flux:checkbox label="SMS" value="sms" />
    </flux:checkbox.group>
</div>

The bonus problem

This is what was originally reported and what I started investigating, which sent me to the depths of the Alpine/Livewire/Flux divide, and also discovered the first problem above.

What is happening in this scenario, is that when wire:model is initialised on the <flux:checkbox.group>, under the hood Livewire is actually using x-model. Inside the initialisation for x-model, there is the following comment:

// If nested model key is undefined, set the default value to empty string.

Meaning thatx-model is initialising settings.notifications to an empty string "".

Now when the SelectableGroup is then initialising, to make sure that the value is first an array (same line as the issue above), it's failing the value === null check, and keeping the state initialised as an empty string instead of an array.

if (value === null) value = this.options().multiple ? [] : ''

The bonus solution

So the solution is to update the check for both null and empty string values, and now everything works happily.

if (value === null || value === '') value = this.options().multiple ? [] : ''

Side note

Normally I wouldn't put two fixes together in the same PR, but I believe this warrants it due to the intricate and similar nature of it, meant it's easier to just explain it all in one 😁 I can split though if you'd prefer.

The solution fixes #812

The bonus solution fixes #896

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

No branches or pull requests

3 participants