{{--
Grid of checkboxes that submit as one array — the wireframe's Amenities
block (docs/wireframes-v12/add-project.html ~line 250).
Props:
name Base field name. Submitted as name[]; writing the
brackets yourself ("amenities[]") is accepted too and the
error key resolves to "amenities" either way.
label Label for the group as a whole.
value Fallback list of checked values, used on a fresh form only.
Flashed old input wins — including the *absence* of old
input for this key once a submit has happened, which is
how "the user unticked every box" survives a failed
validation instead of being reverted to :value.
required Red asterisk only. A `required` attribute on each box
would force the user to tick *every* one, which is never
what "this group is required" means — enforce it with a
validation rule instead. A `required` attribute passed by
a caller is stripped from the boxes rather than obeyed.
options Assoc array of value => label.
group-class Extra classes for the wrapper, e.g. "col-md-12".
Checked matching is strict on the string form of the value, so a 0-valued
box behaves like any other.
CR-2 (spec §12): the layout wrapper is the wireframe's four-column grid
rather than a Bootstrap `row`/`col-md-3` pair. v12 writes it inline on
add-project.html as `.amenity-grid` / `.amenity-item`; those two names are
domain-specific and this component is not, so the same declarations are in
the app-additions tail of nrems-wireframe.css under `.form-check-grid`.
The boxes themselves keep Bootstrap's `.form-check` / `.form-check-input` /
`.form-check-label`. Those are already bridged to the wireframe's look
(accent colour, 18px box) and, unlike the layout, they carry no wireframe
equivalent to move to.
---
spatie/laravel-html variant. html()->checkbox() is not used: it resolves
old input for the field name itself, which for an array field hands back an
array where a string value is expected.
--}}
@props([
'name',
'label' => null,
'value' => [],
'required' => false,
'options' => [],
'groupClass' => null,
])
@php
$fieldName = str_ends_with($name, '[]') ? $name : $name.'[]';
$baseId = form_field_id($name);
$error = form_error_message($name);
$checked = form_selected_values(form_old_array($name, $value));
// `id` and `required` never reach a box: each box owns its id, and a
// required attribute would demand every single one be ticked.
$passthrough = form_passthrough_attributes($attributes, ['id', 'required']);
$row = html()->div()->class('form-check-grid');
foreach ($options as $optionValue => $optionLabel) {
// Ids are derived from the option value, and never from a caller
// supplied id: every box in the group needs its own. Shared flaw,
// shared with the copy in radio.blade.php: 'a b' and 'a_b' slug to the
// same thing and would render duplicate ids — disambiguate when the two
// copies are pulled out into a form_option_id() helper.
$optionId = $baseId.'_'.preg_replace('/[^A-Za-z0-9_-]+/', '_', (string) $optionValue);
// Every box carries the group's error state: the message lives once,
// under the group, so each box points at that one id.
$box = html()->input('checkbox')
->class('form-check-input')
->addClassIf($error !== null, 'is-invalid')
->id($optionId)
->name($fieldName)
->attribute('value', (string) $optionValue)
->attributeIf(in_array((string) $optionValue, $checked, true), 'checked')
->attributeIf($error !== null, 'aria-invalid', 'true')
->attributeIf($error !== null, 'aria-describedby', $baseId.'_error')
->attributes($passthrough);
$row = $row->addChild(
html()->div()->class('form-check')->addChildren([
$box,
html()->label()
->class('form-check-label')
->attribute('for', $optionId)
->text((string) $optionLabel),
])
);
}
@endphp
{!! $row->render() !!}