3 Commits
Author SHA1 Message Date
Andreas Reinhold / reiniandClaude Opus 5 7798352cfc Compile unprefixed components to the same view on every machine
tests / lint (push) Successful in 1m3s
tests / feature (8.4) (push) Successful in 1m14s
tests / feature (8.5) (push) Successful in 1m14s
tests / browser (chrome, chromium) (push) Successful in 3m16s
tests / browser (firefox, firefox) (push) Successful in 4m36s
tests / browser (safari, webkit) (push) Successful in 5m8s
Without a prefix, Blade named the components' view namespace after the
folder's absolute path, while compiled views are named relative to the
application. A view compiled on a laptop and served from a container
sharing storage/framework/views (or from another release directory)
printed 'eb5f…::input' as text. The components now register under the
prefix livewire-material when none is configured, which unprefixed tags
still resolve through, and the path's namespace stays registered for
views compiled before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
2026-09-13 11:16:52 +02:00
Andreas Reinhold / reiniandClaude Opus 5 cb3aa798a0 Keep a full-screen dialog's subtitle on a phone
tests / lint (push) Successful in 1m20s
tests / feature (8.4) (push) Successful in 1m13s
tests / feature (8.5) (push) Successful in 1m13s
tests / browser (chrome, chromium) (push) Successful in 3m17s
tests / browser (firefox, firefox) (push) Successful in 4m37s
tests / browser (safari, webkit) (push) Successful in 5m10s
The bar names the dialog there, so only the headline hides; the subtitle
used to disappear with it, and a subtitle without a title never showed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
2026-09-13 10:57:36 +02:00
Andreas Reinhold / reiniandClaude Opus 5 648a4cfe0f Show a plain form field's errors under its name
tests / lint (push) Successful in 1m5s
tests / feature (8.4) (push) Successful in 1m9s
tests / feature (8.5) (push) Successful in 1m9s
tests / browser (chrome, chromium) (push) Successful in 3m19s
tests / browser (safari, webkit) (push) Successful in 5m0s
tests / browser (firefox, firefox) (push) Successful in 4m34s
Fields read their errors only under the wire:model name, so a Fortify
login form (name="email", no wire:model) never showed "These
credentials do not match". Without wire:model they now read the name,
turning brackets into dots.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
2026-09-13 10:41:37 +02:00
17 changed files with 106 additions and 23 deletions
@@ -467,7 +467,7 @@ A one-column grid of fields with an `actions` slot at the foot (the slot takes i
### `<x-field>`, `<x-input>`, `<x-password>`, `<x-textarea>`, `<x-select>`, `<x-file>` ### `<x-field>`, `<x-input>`, `<x-password>`, `<x-textarea>`, `<x-select>`, `<x-file>`
M3 text fields. `variant`: `outlined` or `filled`; without it, `config('livewire-material.fields.variant')` (`outlined`). All take `label`, `hint`, `variant`, and read their errors from the bag under the `wire:model` name (the error replaces the hint, sets `aria-invalid`). `class` lands on the field's outer element (margins, widths); every other attribute (`wire:model`, `type`, `required`, `readonly`, `autocomplete`) reaches the control. Never pass `placeholder` expecting it to show while a label rests in the field: it shows once the field has focus. M3 text fields. `variant`: `outlined` or `filled`; without it, `config('livewire-material.fields.variant')` (`outlined`). All take `label`, `hint`, `variant`, and read their errors from the bag under the `wire:model` name, or the `name` in a plain form (`photos[]``photos`, `address[city]``address.city`); the error replaces the hint and sets `aria-invalid`. `class` lands on the field's outer element (margins, widths); every other attribute (`wire:model`, `type`, `required`, `readonly`, `autocomplete`) reaches the control. Never pass `placeholder` expecting it to show while a label rests in the field: it shows once the field has focus.
- `<x-input>`: `icon`, `icon-right`, `prefix`, `suffix`, `clearable`, `copyable` (copies the value, confirms with a snackbar), `size` (`sm` 40px, `xs` 32px — for unlabelled toolbar controls; give them `aria-label`), `mono`. - `<x-input>`: `icon`, `icon-right`, `prefix`, `suffix`, `clearable`, `copyable` (copies the value, confirms with a snackbar), `size` (`sm` 40px, `xs` 32px — for unlabelled toolbar controls; give them `aria-label`), `mono`.
- `<x-password>`: a reveal button; `icon`, `size`. - `<x-password>`: a reveal button; `icon`, `size`.
@@ -18,7 +18,9 @@
@php @php
$model = $attributes->whereStartsWith('wire:model')->first(); $model = $attributes->whereStartsWith('wire:model')->first();
$id = $attributes->get('id') ?? 'check-'.substr(md5($model.'|'.$label.'|'.$attributes->get('value')), 0, 12); $id = $attributes->get('id') ?? 'check-'.substr(md5($model.'|'.$label.'|'.$attributes->get('value')), 0, 12);
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : []; // A plain form's field is named, not bound: its errors are under its name (`files[]` `files`, `a[b]` `a.b`).
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
@endphp @endphp
<div {{ $attributes->only(['class', 'wire:key'])->class(['min-w-0']) }}> <div {{ $attributes->only(['class', 'wire:key'])->class(['min-w-0']) }}>
+4 -2
View File
@@ -35,8 +35,10 @@
@php @php
$model = $attributes->wire('model')->value() ?: null; $model = $attributes->wire('model')->value() ?: null;
$messages = $model !== null && isset($errors) // A plain form's field is named, not bound: its errors are under its name (`files[]` → `files`, `a[b]` → `a.b`).
? array_values(array_unique(\Illuminate\Support\Arr::flatten([$errors->get($model), $errors->get($model.'.*')]))) $errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
$messages = $errorKey !== null && isset($errors)
? array_values(array_unique(\Illuminate\Support\Arr::flatten([$errors->get($errorKey), $errors->get($errorKey.'.*')])))
: []; : [];
$choices = collect($options)->map(fn ($option): array => [ $choices = collect($options)->map(fn ($option): array => [
'value' => data_get($option, $optionValue), 'value' => data_get($option, $optionValue),
@@ -57,8 +57,10 @@
$mode = in_array($mode, ['docked', 'modal', 'input'], true) ? $mode : 'docked'; $mode = in_array($mode, ['docked', 'modal', 'input'], true) ? $mode : 'docked';
$id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|datepicker'), 0, 12); $id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|datepicker'), 0, 12);
$anchor = '--material-datepicker-'.preg_replace('/[^A-Za-z0-9_-]/', '-', $id); $anchor = '--material-datepicker-'.preg_replace('/[^A-Za-z0-9_-]/', '-', $id);
$messages = $model !== null && isset($errors) // A plain form's field is named, not bound: its errors are under its name (`files[]` → `files`, `a[b]` → `a.b`).
? array_values(array_unique(\Illuminate\Support\Arr::flatten([$errors->get($model), $errors->get($model.'.*')]))) $errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
$messages = $errorKey !== null && isset($errors)
? array_values(array_unique(\Illuminate\Support\Arr::flatten([$errors->get($errorKey), $errors->get($errorKey.'.*')])))
: []; : [];
$locale = str_replace('_', '-', app()->getLocale()); $locale = str_replace('_', '-', app()->getLocale());
+4 -2
View File
@@ -21,8 +21,10 @@
@php @php
$model = $attributes->whereStartsWith('wire:model')->first(); $model = $attributes->whereStartsWith('wire:model')->first();
$id = $attributes->get('id') ?? 'field-'.substr(md5('file|'.$model.'|'.$label), 0, 12); $id = $attributes->get('id') ?? 'field-'.substr(md5('file|'.$model.'|'.$label), 0, 12);
$messages = $model !== null && isset($errors) // A plain form's field is named, not bound: its errors are under its name (`files[]` → `files`, `a[b]` → `a.b`).
? array_values(array_unique(\Illuminate\Support\Arr::flatten([$errors->get($model), $errors->get($model.'.*')]))) $errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
$messages = $errorKey !== null && isset($errors)
? array_values(array_unique(\Illuminate\Support\Arr::flatten([$errors->get($errorKey), $errors->get($errorKey.'.*')])))
: []; : [];
@endphp @endphp
+3 -1
View File
@@ -33,7 +33,9 @@
@php @php
$model = $attributes->whereStartsWith('wire:model')->first(); $model = $attributes->whereStartsWith('wire:model')->first();
$name ??= $model; $name ??= $model;
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : []; // A plain form's field is named, not bound: its errors are under its name (`files[]` `files`, `a[b]` `a.b`).
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
$size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'sm'; $size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'sm';
$segment = [ $segment = [
+3 -1
View File
@@ -30,7 +30,9 @@
$model = $attributes->whereStartsWith('wire:model')->first(); $model = $attributes->whereStartsWith('wire:model')->first();
$placeholder = filled($attributes->get('placeholder')) ? $attributes->get('placeholder') : ' '; $placeholder = filled($attributes->get('placeholder')) ? $attributes->get('placeholder') : ' ';
$id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|'.$placeholder), 0, 12); $id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|'.$placeholder), 0, 12);
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : []; // A plain form's field is named, not bound: its errors are under its name (`files[]` `files`, `a[b]` `a.b`).
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
@endphp @endphp
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$prefix :$suffix :$size :$variant :$mono :class="$attributes->get('class')" :data-readonly="$attributes->get('readonly') ? '' : null"> <x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$prefix :$suffix :$size :$variant :$mono :class="$attributes->get('class')" :data-readonly="$attributes->get('readonly') ? '' : null">
+5 -4
View File
@@ -78,18 +78,19 @@
@endif @endif
<div @class(['min-h-0 flex-1', 'max-sm:overflow-y-auto max-sm:px-6 max-sm:pb-6' => $fullscreen])> <div @class(['min-h-0 flex-1', 'max-sm:overflow-y-auto max-sm:px-6 max-sm:pb-6' => $fullscreen])>
@if (filled($title) || $icon) @if (filled($title) || filled($subtitle) || $icon)
<div @class(['mb-4', 'max-sm:hidden' => $fullscreen && ! $icon, 'text-center' => $icon])> {{-- A full-screen dialog's bar names it on a phone, so only the subtitle stays there. --}}
<div @class(['mb-4', 'max-sm:hidden' => $fullscreen && ! $icon && blank($subtitle), 'text-center' => $icon])>
@if ($icon) @if ($icon)
<x-livewire-material::icon :name="$icon" class="mx-auto mb-4 size-6 text-secondary" /> <x-livewire-material::icon :name="$icon" class="mx-auto mb-4 size-6 text-secondary" />
@endif @endif
@if (filled($title)) @if (filled($title))
<h2 id="{{ $id }}-title" class="type-headline-sm">{{ $title }}</h2> <h2 id="{{ $id }}-title" @class(['type-headline-sm', 'max-sm:hidden' => $fullscreen && ! $icon])>{{ $title }}</h2>
@endif @endif
@if (filled($subtitle)) @if (filled($subtitle))
<p class="mt-4 type-body-md text-on-surface-variant">{{ $subtitle }}</p> <p @class(['type-body-md text-on-surface-variant', 'mt-4' => filled($title) || $icon, 'max-sm:mt-0' => $fullscreen && ! $icon])>{{ $subtitle }}</p>
@endif @endif
@if ($separator) @if ($separator)
@@ -17,7 +17,9 @@
@php @php
$model = $attributes->whereStartsWith('wire:model')->first(); $model = $attributes->whereStartsWith('wire:model')->first();
$id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|password'), 0, 12); $id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|password'), 0, 12);
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : []; // A plain form's field is named, not bound: its errors are under its name (`files[]` → `files`, `a[b]` → `a.b`).
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
@endphp @endphp
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant :class="$attributes->get('class')" x-data="{ shown: false }"> <x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant :class="$attributes->get('class')" x-data="{ shown: false }">
+3 -1
View File
@@ -23,7 +23,9 @@
$model = $attributes->whereStartsWith('wire:model')->first(); $model = $attributes->whereStartsWith('wire:model')->first();
$name = $attributes->get('name') ?? $model ?? 'radio-'.substr(md5($label.'|'.json_encode($options)), 0, 12); $name = $attributes->get('name') ?? $model ?? 'radio-'.substr(md5($label.'|'.json_encode($options)), 0, 12);
$id = 'radio-'.substr(md5($name.'|'.$label), 0, 12); $id = 'radio-'.substr(md5($name.'|'.$label), 0, 12);
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : []; // A plain form's field is named, not bound: its errors are under its name (`files[]` → `files`, `a[b]` → `a.b`).
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
@endphp @endphp
<fieldset <fieldset
+3 -1
View File
@@ -29,7 +29,9 @@
@php @php
$model = $attributes->whereStartsWith('wire:model')->first(); $model = $attributes->whereStartsWith('wire:model')->first();
$id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|select'), 0, 12); $id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|select'), 0, 12);
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : []; // A plain form's field is named, not bound: its errors are under its name (`files[]` `files`, `a[b]` `a.b`).
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
@endphp @endphp
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant floated :class="$attributes->get('class')"> <x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant floated :class="$attributes->get('class')">
@@ -19,7 +19,9 @@
$model = $attributes->whereStartsWith('wire:model')->first(); $model = $attributes->whereStartsWith('wire:model')->first();
$placeholder = filled($attributes->get('placeholder')) ? $attributes->get('placeholder') : ' '; $placeholder = filled($attributes->get('placeholder')) ? $attributes->get('placeholder') : ' ';
$id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|textarea'), 0, 12); $id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|textarea'), 0, 12);
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : []; // A plain form's field is named, not bound: its errors are under its name (`files[]` `files`, `a[b]` `a.b`).
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
@endphp @endphp
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$variant :class="$attributes->get('class')" :data-readonly="$attributes->get('readonly') ? '' : null"> <x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$variant :class="$attributes->get('class')" :data-readonly="$attributes->get('readonly') ? '' : null">
@@ -58,7 +58,9 @@
@php @php
$model = $attributes->wire('model')->value() ?: null; $model = $attributes->wire('model')->value() ?: null;
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : []; // A plain form's field is named, not bound: its errors are under its name (`files[]` → `files`, `a[b]` → `a.b`).
$errorKey = $model ?? (filled($name) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $name) : null);
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
$id = $attributes->get('id') ?? 'timepicker-'.substr(md5($model.'|'.$label.'|'.$name.'|timepicker'), 0, 12); $id = $attributes->get('id') ?? 'timepicker-'.substr(md5($model.'|'.$label.'|'.$name.'|timepicker'), 0, 12);
$cycle = in_array((string) $format, ['12', '24'], true) ? (int) $format : null; $cycle = in_array((string) $format, ['12', '24'], true) ? (int) $format : null;
+13 -2
View File
@@ -5,6 +5,7 @@ namespace NoNameWeb\LivewireMaterial;
use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\Factory;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class LivewireMaterialServiceProvider extends ServiceProvider class LivewireMaterialServiceProvider extends ServiceProvider
@@ -113,7 +114,15 @@ class LivewireMaterialServiceProvider extends ServiceProvider
} }
/** /**
* Register the components as anonymous Blade components under the configured prefix. * Register the components as anonymous Blade components under the configured prefix, or
* under `livewire-material` without one.
*
* A compiled `<x-button>` names the view namespace Blade derives from the prefix, or from the
* component folder's absolute path when there is none. That path differs between a laptop and
* a container sharing `storage/framework/views`, or between release directories, while the
* compiled file's name does not — so a view compiled on one showed `a1b2…::button` as text on
* the other. A prefix does not stop an unprefixed tag resolving. The path's namespace stays
* registered, so views compiled before this release still render until they are recompiled.
* *
* The package's own views never go through this registration: they write * The package's own views never go through this registration: they write
* `<x-livewire-material::button>`, which resolves through the view namespace whatever the * `<x-livewire-material::button>`, which resolves through the view namespace whatever the
@@ -123,8 +132,10 @@ class LivewireMaterialServiceProvider extends ServiceProvider
{ {
Blade::anonymousComponentPath( Blade::anonymousComponentPath(
static::componentPath(), static::componentPath(),
filled(config('livewire-material.prefix')) ? config('livewire-material.prefix') : null, filled(config('livewire-material.prefix')) ? config('livewire-material.prefix') : 'livewire-material',
); );
View::addNamespace(hash('xxh128', static::componentPath()), static::componentPath());
} }
/** /**
+11
View File
@@ -44,6 +44,17 @@ it('uses the surrounding Alpine scope without wire:model, and stays open when pe
->toContain('aria-label="Close"'); ->toContain('aria-label="Close"');
}); });
it('keeps a full-screen dialog\'s subtitle on a phone, where its bar carries the title', function () {
$html = (string) $this->blade('<x-modal title="Enable 2FA" subtitle="Scan the code" fullscreen>Text</x-modal>');
expect($html)
->toMatch('/<h2 id="[^"]+-title" class="type-headline-sm max-sm:hidden">/')
->toContain('<p class="type-body-md text-on-surface-variant mt-4 max-sm:mt-0">Scan the code</p>')
->not->toContain('<div class="mb-4 max-sm:hidden">')
->and((string) $this->blade('<x-modal title="Help" fullscreen>Text</x-modal>'))->toContain('<div class="mb-4 max-sm:hidden">')
->and((string) $this->blade('<x-modal subtitle="Only a subtitle">Text</x-modal>'))->toContain('<p class="type-body-md text-on-surface-variant">Only a subtitle</p>');
});
it('slides a side sheet in from either edge, and is a pane from xl when asked', function () { it('slides a side sheet in from either edge, and is a pane from xl when asked', function () {
expect((string) $this->blade('<x-drawer title="Details" with-close-button>Body</x-drawer>')) expect((string) $this->blade('<x-drawer title="Details" with-close-button>Body</x-drawer>'))
->toContain('x-trap.inert.noscroll="open && ! wide"') ->toContain('x-trap.inert.noscroll="open && ! wide"')
@@ -0,0 +1,24 @@
<?php
it('shows the errors of a plain form field under its name', function (string $blade, string $key) {
$html = (string) $this->withViewErrors([$key => ['Something is wrong here.']])->blade($blade);
expect($html)
->toContain('Something is wrong here.')
->toContain('aria-invalid="true"');
})->with([
'input' => ['<x-input name="email" label="Email" />', 'email'],
'password' => ['<x-password name="password" label="Password" />', 'password'],
'textarea' => ['<x-textarea name="message" label="Message" />', 'message'],
'select' => ['<x-select name="hours" label="Expires" :options="[[\'id\' => 1, \'name\' => \'1 hour\']]" />', 'hours'],
'checkbox' => ['<x-checkbox name="terms" label="Terms" />', 'terms'],
'radio' => ['<x-radio name="audience" :options="[[\'id\' => \'a\', \'name\' => \'A\']]" />', 'audience'],
'file with brackets' => ['<x-file name="photos[]" label="Photos" multiple />', 'photos'],
'nested name' => ['<x-input name="address[city]" label="City" />', 'address.city'],
'timepicker' => ['<x-timepicker name="starts_at" label="Starts at" />', 'starts_at'],
]);
it('keeps reading the wire:model name when both are there', function () {
expect((string) $this->withViewErrors(['form.email' => ['Taken.']])->blade('<x-input name="email" wire:model="form.email" label="Email" />'))
->toContain('Taken.');
});
+16 -2
View File
@@ -20,9 +20,23 @@ function packageComponentPaths(): Collection
->values(); ->values();
} }
it('registers the components without a prefix', function () { it('registers the components under its own name when no prefix is configured', function () {
expect(packageComponentPaths())->toHaveCount(1) expect(packageComponentPaths())->toHaveCount(1)
->and(packageComponentPaths()->first()['prefix'])->toBeNull(); ->and(packageComponentPaths()->first()['prefix'])->toBe('livewire-material')
->and(Blade::render('<x-badge value="New" tonal />'))->toContain('New');
});
it('compiles an unprefixed component to the same view on every machine', function () {
$compiled = Blade::compileString('<x-input label="Name" />');
expect($compiled)->toContain("'view' => '".hash('xxh128', 'livewire-material')."::input'")
->not->toContain(hash('xxh128', LivewireMaterialServiceProvider::componentPath()));
});
it('still renders a view compiled when the namespace came from the component path', function () {
$legacy = hash('xxh128', LivewireMaterialServiceProvider::componentPath());
expect(view()->exists($legacy.'::input'))->toBeTrue();
}); });
it('registers the components under a configured prefix', function () { it('registers the components under a configured prefix', function () {