Using both afterStateUpdated AND afterStateUpdatedJs

TextInput::make('title')
->live(onBlur: true)
->afterStateUpdatedJs(SlugGenerator::slugifyWithJs(fieldName: 'slug'))
->afterStateUpdated(function (?string $state, Set $set, ?Model $record) {
$set('slug', SlugGenerator::unique($record, $state));
}),

TextInput::make('slug'),
TextInput::make('title')
->live(onBlur: true)
->afterStateUpdatedJs(SlugGenerator::slugifyWithJs(fieldName: 'slug'))
->afterStateUpdated(function (?string $state, Set $set, ?Model $record) {
$set('slug', SlugGenerator::unique($record, $state));
}),

TextInput::make('slug'),
Here, the php one doesn't work. Or maybe it's immediately updated with the JS one?
27 Replies
charlie
charlieOP4mo ago
Dan and Dennis seems to say it should work so I guess the JS is overriding the state update made by Livewire on field blur...
awcodes
awcodes4mo ago
Makes sense, the js isn’t going to wait for a blur if I’m thinking about it correctly.
charlie
charlieOP4mo ago
Now I'm even more confused 😄 if
the js isn’t going to wait for a blur
then Livewire should take control when I blur the field?
awcodes
awcodes4mo ago
I’m just thinking with live() they are both going to run, possibly creating a race condition. Not sure though. Listen to Dan or Dennis before trusting me. 😂 Just saying the whole point of updatedJs is so it doesn’t need to go to the backend with a livewire request.
charlie
charlieOP4mo ago
If anybody wants to reproduce, here is my full code: Here's the code I have in \App\Support\SlugGenerator.php:
namespace App\Support;

use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;

class SlugGenerator
{
/**
* Generate a unique slug from the title
*
* @param class-string<Model> $modelClass
* @param string|null $title
* @param Model|null $ignoreRecord
*
* @return string
*/
public static function unique(string $modelClass, ?string $title, ?Model $ignoreRecord = null): string
{
$baseSlug = Str::slug($title);
$slug = $baseSlug;
$count = 2;

while (
$modelClass::query()
->where('slug', $slug)
->when($ignoreRecord, fn ($query) => $query->whereKeyNot($ignoreRecord->getKey()))
->exists()
) {
$slug = $baseSlug . '-' . $count++;
}

return $slug;
}

/**
* Optimistic slug generation with JS
*
* @param string $fieldName
*
* @return string
* @noinspection JSUnresolvedReference
*/
public static function slugifyWithJs(string $fieldName = 'slug'): string
{
return <<<JS
const slug = (\$state ?? '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
\$set('$fieldName', slug);
JS;
}
}
namespace App\Support;

use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;

class SlugGenerator
{
/**
* Generate a unique slug from the title
*
* @param class-string<Model> $modelClass
* @param string|null $title
* @param Model|null $ignoreRecord
*
* @return string
*/
public static function unique(string $modelClass, ?string $title, ?Model $ignoreRecord = null): string
{
$baseSlug = Str::slug($title);
$slug = $baseSlug;
$count = 2;

while (
$modelClass::query()
->where('slug', $slug)
->when($ignoreRecord, fn ($query) => $query->whereKeyNot($ignoreRecord->getKey()))
->exists()
) {
$slug = $baseSlug . '-' . $count++;
}

return $slug;
}

/**
* Optimistic slug generation with JS
*
* @param string $fieldName
*
* @return string
* @noinspection JSUnresolvedReference
*/
public static function slugifyWithJs(string $fieldName = 'slug'): string
{
return <<<JS
const slug = (\$state ?? '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
\$set('$fieldName', slug);
JS;
}
}
And here is my form:
TextInput::make('title')
->live(onBlur: true)
->required()
->afterStateUpdatedJs(SlugGenerator::slugifyWithJs('slug'))
->partiallyRenderComponentsAfterStateUpdated(['slug'])
->afterStateUpdated(function (?string $state, Set $set, ?Model $record) {
$slug = SlugGenerator::unique(Post::class, $state, $record);
$set('slug', $slug);
}),

TextInput::make('slug')
->disabled()
->dehydrated(),
TextInput::make('title')
->live(onBlur: true)
->required()
->afterStateUpdatedJs(SlugGenerator::slugifyWithJs('slug'))
->partiallyRenderComponentsAfterStateUpdated(['slug'])
->afterStateUpdated(function (?string $state, Set $set, ?Model $record) {
$slug = SlugGenerator::unique(Post::class, $state, $record);
$set('slug', $slug);
}),

TextInput::make('slug')
->disabled()
->dehydrated(),
sure! Here I was trying to have super snappy field update + check the DB for duplicates and add my-slug-2 when needed I'll try to see if it could be a race condition
awcodes
awcodes4mo ago
I wonder if the problem is using a php class/method instead of the JS heredoc directly?
charlie
charlieOP4mo ago
I don't think so because they work independently fine. And the JS one works very well
awcodes
awcodes4mo ago
Hmm, ok, yea then I’d say both are trying to run What exactly is the use cases for using both?
charlie
charlieOP4mo ago
Here I was trying to have super snappy field update + check the DB for duplicates and add my-slug-2 when needed
awcodes
awcodes4mo ago
I don’t see how that’s possible with JS since it requires a connection to the db.
charlie
charlieOP4mo ago
Of course
awcodes
awcodes4mo ago
I think for this use case you would need to forego the updatedJs
charlie
charlieOP4mo ago
Or do the DB stuff before save btw, I'm not looking for a workaround 🙂 I was just wondering why and how it couldn't work
awcodes
awcodes4mo ago
Could potentially do something to automatically change it in dehydrate state to force it, then just have a validation rule if they try to change it after it’s created. But uniqueness, in my mind, is a validation issue. Well, in my mind it won’t work because it’s trying to use js to check against something that requires the server, so going through livewire instead of js makes the most sense despite implied speed. I’m sure Dan and Dennis know something I don’t though.
LeandroFerreira
LeandroFerreira4mo ago
I believe the issue is isLive: <?= Js::from($schemaComponent->isLive()) ?> inside filamentSchemaComponent, because it doesn't check for onBlur, even when you set ->live(onBlur: true) in the component
No description
LeandroFerreira
LeandroFerreira4mo ago
let me know if you want a workaround ✌️
charlie
charlieOP4mo ago
thanks @LeandroFerreira but that's the Livewire method which doesn't trigger, not the JS one
LeandroFerreira
LeandroFerreira4mo ago
you mean, afterStateUpdated isn't working with live(onBlur: true`?
charlie
charlieOP4mo ago
afterStateUpdated doesn't work on blur when I have afterStateUpdatedJS. possibly a race condition
LeandroFerreira
LeandroFerreira4mo ago
what happens?
TextInput::make('title')
->afterStateUpdatedJs(<<<'JS'
console.log($state)
JS)
->live(onBlur: true)
->afterStateUpdated(function (?string $state) {
dd($state);
})
TextInput::make('title')
->afterStateUpdatedJs(<<<'JS'
console.log($state)
JS)
->live(onBlur: true)
->afterStateUpdated(function (?string $state) {
dd($state);
})
charlie
charlieOP4mo ago
it works fine. Let me find what's the problem in my code... The problem is with $set() I think:
TextInput::make('title')
->afterStateUpdatedJs(<<<'JS'
$set('slug', 'frontend');
JS)
->live(onBlur: true)
->afterStateUpdated(function (?string $state, $set) {
$set('slug', 'backend');
}),

TextInput::make('slug'),
TextInput::make('title')
->afterStateUpdatedJs(<<<'JS'
$set('slug', 'frontend');
JS)
->live(onBlur: true)
->afterStateUpdated(function (?string $state, $set) {
$set('slug', 'backend');
}),

TextInput::make('slug'),
With this code, I'm not able to set "backend" in slug field on blur event
LeandroFerreira
LeandroFerreira4mo ago
dd state, what happens?
->afterStateUpdated(function (?string $state) {
dd($state);
})
->afterStateUpdated(function (?string $state) {
dd($state);
})
charlie
charlieOP4mo ago
It triggers the dd on each change, not on blur
LeandroFerreira
LeandroFerreira4mo ago
I think blur is trigged within watch inside updatedJs, which I believe is causing the conflict.. because ->live() enables isLive: true it should be false if we are using onBlur I think
charlie
charlieOP4mo ago
makes total sense I tried to add this:
x-data="filamentSchemaComponent({
path: <?= Js::from($schemaComponentStatePath) ?>,
containerPath: <?= Js::from($statePath) ?>,
isLive: <?= Js::from($schemaComponent->isLive() && !$schemaComponent->isLiveOnBlur()) ?>,
$wire,
})"
x-data="filamentSchemaComponent({
path: <?= Js::from($schemaComponentStatePath) ?>,
containerPath: <?= Js::from($statePath) ?>,
isLive: <?= Js::from($schemaComponent->isLive() && !$schemaComponent->isLiveOnBlur()) ?>,
$wire,
})"
It fixes the dd() on change, but doesn't fixes the $set() from livewire on blur I'll PR this change though (do you agree?)
charlie
charlieOP4mo ago
GitHub
fix: afterStateUpdatedJS live when onBlur:true by CharlieEtienne ·...
Description On this code, dd() shouldn&amp;#39;t be triggered each time the input changes, only on blur. This PR aims to fix it. TextInput::make(&amp;#39;title&amp;#39;) -&amp;gt;afterStateUpda...
LeandroFerreira
LeandroFerreira4mo ago
take a look I think the blur event works, but the watch inside the updatedJs overrides the current value
TextInput::make('title')
->afterStateUpdated(function (Set $set, Get $get) {
$set('slug', 'backend');
dump($get('slug'));
})
->afterStateUpdatedJs(<<<'JS'
$set('slug', 'frontend');
JS)
->extraInputAttributes(fn (TextInput $component): array => ['wire:model.blur' => $component->getStatePath()])
TextInput::make('title')
->afterStateUpdated(function (Set $set, Get $get) {
$set('slug', 'backend');
dump($get('slug'));
})
->afterStateUpdatedJs(<<<'JS'
$set('slug', 'frontend');
JS)
->extraInputAttributes(fn (TextInput $component): array => ['wire:model.blur' => $component->getStatePath()])
it will reproduce isLive: false

Did you find this page helpful?