bvfi-dev liked a comment+100 XP
5mos ago
One clean approach is to store a snapshot of the form state after preFillEntry() and compare against it later. Instead of manually coding this in every FormObject, you can abstract it once in a base Form class:
public array $original = [];
public function snapshot(): void { $this->original = $this->toArray(); }
public function isDirty(array $only = []): bool { $current = $this->toArray();
if ($only) {
$current = Arr::only($current, $only);
$original = Arr::only($this->original, $only);
} else {
$original = $this->original;
}
return $current !== $original;
}
Then call $this->form->snapshot() after preFillEntry() in mount().
Now every FormObject automatically gets a reusable isDirty() without repeating logic and you can even check only specific fields.
bvfi-dev wrote a reply+100 XP
5mos ago
bvfi-dev started a new conversation+100 XP
5mos ago
Im trying to use FormObjects for all my livewire components to reduce duplicate code, but I keep running into an issue where I need to check if the model was changed, like when in an edit form and checking if the initial state and save state are different, so that I can save on my API calls. In my Livewire Form, I have:
public function preFillEntry(User $entry):void
{
$entry->loadMissing('contact');
$this->email = $entry->email;
$this->first_name = $entry->first_name;
$this->last_name = $entry->name;
which I call in my mount() method in my Component like so:
$this->form->preFillEntry($entry);
And then in my save() method, Id like to check if a couple of properties have changed, but of course I cant use isDirty(), so Im looking into alternatives and whats the best and optimal way around this issue?
I know i can just make an array in the FormObject, with the original state and then compare this in a custom isDirty() function that I call in the save method, but Im not sure if this is the most efficient way of doing it, since this will get very repetitive if i start introducing it in all my FormObjects
bvfi-dev wrote a reply+100 XP
5mos ago
The thing with flash messages is you have to consider 2 scenarios when using Livewire:
- One where you want instant notifications as soon as the button is clicked
- One where there is a redirect in the session
I like making my stuff custom so that I always have 100% control over it, and also from my experience you need separate handles for both. I have for example an instant flash notification that appears on a click of a button that I call via Livewire's dispatch().
However If I redirect it would disappear so I need a session flash for after the session has been redirected and depending on your scenario you need to use one of these options.
Lets say I have a Livewire Index component and within it, I have create modal. What I would do on submitting the form is show a flash notification and make the modal not show -> User clicks "Submit" modal closes, notification is shown with results success/fail. This creates a smooth UI experience. However If I were to want to redirect to the edit page after submitting the create form, I would need a session flash notification.
The instant notification banner I make like so:
- In my default app layout, I insert the livewire component:
@stack('modals')
@livewire('components.notification-manager')
Then my component is like:
class NotificationManager extends Component
{
public $banners = [];
public function render()
{
return view('livewire.components.notification-manager');
}
/**
* Appends a banner to banners[]
* @param string|null $message
* @param int $type Types:<br>0 - Error/Fail<br>1 - Success<br>2 - Info (Default)
* @param int $timeout Defaults to 3 (seconds)
* @return void
*/
#[On('show-banner')]
public function addBanner(?string $message, int $type = 2, int $timeout = 3): void
{
if(is_null($message) || empty(trim($message))) { $message = 'Failed to generate a message!'; $type = 0; }
$key = uniqid();
$timeout = $timeout *1000;
$this->banners[] = ['key' => $key, 'message' => $message, 'type' => $type, 'timeout' => $timeout];
}
public function removeBanner($key): void
{
$this->banners = array_filter($this->banners, function($banner) use ($key) {
return $banner['key'] !== $key;
});
}
}
This way I can have multiple notifications stack on each other, I can give custom timeouts, I set the type, etc
Now from anywhere in the Livewire Component I can call:
$this->dispatch('show-banner', message: 'Success message' , type: 1);
And this will make the notification appear The frontend:
<div class="w-full fixed mx-auto top-2 right-4 flex flex-col space-y-2" style="z-index: 9999 !important;">
@foreach ($banners as $banner)
<div wire:key="banner-{{ $banner['key'] }}" x-data="{ show: true }" x-init="setTimeout(() => show = false, {{ $banner['timeout'] }})" x-show="show"
x-on:click="show = false"
...
You can use alpineJs to make it appear from top with smooth transitions and animations and you also need to display the $banner['message']
And depending on type I give it a different background color.
With session banner just make a blade component: , put it in your app layout, or the main html file you want it to be used, and have the html like so:
@props(['style' => session('flash.bannerStyle', 'success'), 'message' => session('flash.banner')])
<div x-data="{{ json_encode(['show' => true, 'style' => $style, 'message' => $message]) }}"
:class="{ 'bg-green-500': style == 'success', 'bg-red-700': style == 'danger', 'bg-yellow-500': style == 'warning', 'bg-gray-500': style != 'success' && style != 'danger' && style != 'warning'}"
style="display: none;"
x-show="show && message"
x-on:banner-message.window="
style = event.detail.style;
message = event.detail.message;
show = true;
">
...
Remember to use x-text="message" to display the message in a <p> or something, and don't forget the x-on:click="show = false" so that the notification can be closed.
Now anywhere in your livewire components you can do:
return redirect()->route('route.name')
->with('flash.banner', $message)
->with('flash.bannerStyle', 'success');
And when you return, the banner will pop out. I dont have full code for you, because mine has a lot more code in it and It took me a long time to figure it out. Im giving you an idea and more than a start to learn how the notifications should work
bvfi-dev was awarded Best Answer+1000 XP
5mos ago
I updated the child Blade component root to have:
<div wire:key="edit-course-form-root" class="grid grid-cols-1 gap-6"
x-data="{
statusId: $wire.entangle('courseForm.status_id'),
ztComing: {{ (int) $ztCourseComingSoon }},
hydrated: false,
isComingSoon(){ return Number(this.statusId) === this.ztComing; }
}" x-init="$nextTick(() => { hydrated = true })">
And then changed the Coming Soon section to have:
<div x-cloak x-show="hydrated && isComingSoon()"
Now when the Model has status Coming soon and I load the page, the Coming Soon section appears smoothly with animation, but changing it to Active still does the same, so Im thinking it has something to do with changing the Status for the first time. What I did was remove the .live from the select in the section when Coming Soon was active and that fixed the issue...Im not sure why .live would cause it to behave that way. I technically dont need the live for the edit form, but I need it for my create form.
bvfi-dev started a new conversation+100 XP
5mos ago
I replied to my own post about a significant update and I am unable to see what I have written, eventhough its mentioned that I have done it on the "Forums" frontpage. The related post is: Post
When I am in the post it says "2 Replies", however on the frontpage it says 3, which is correct if my Reply is included.
bvfi-dev wrote a reply+100 XP
5mos ago
I updated the child Blade component root to have:
<div wire:key="edit-course-form-root" class="grid grid-cols-1 gap-6"
x-data="{
statusId: $wire.entangle('courseForm.status_id'),
ztComing: {{ (int) $ztCourseComingSoon }},
hydrated: false,
isComingSoon(){ return Number(this.statusId) === this.ztComing; }
}" x-init="$nextTick(() => { hydrated = true })">
And then changed the Coming Soon section to have:
<div x-cloak x-show="hydrated && isComingSoon()"
Now when the Model has status Coming soon and I load the page, the Coming Soon section appears smoothly with animation, but changing it to Active still does the same, so Im thinking it has something to do with changing the Status for the first time. What I did was remove the .live from the select in the section when Coming Soon was active and that fixed the issue...Im not sure why .live would cause it to behave that way. I technically dont need the live for the edit form, but I need it for my create form.
bvfi-dev wrote a reply+100 XP
5mos ago
I use last Laravel 11 version, Livewire 3 installed with Jetstream 5. But this shouldn't influence the AlpineJS. I noticed that it kind of starts smooth, but then suddenly instantly opens down. I uploaded a video, at 0:15 I refresh the page:
I cant figure out why it happens consistently only on the first try on the page, how can I even debug this, its literally just JS harmonicas.
bvfi-dev started a new conversation+100 XP
5mos ago
I have a pretty simple form. Basically I have a status select dropdown and I want an animation when the status is a certain ID:
<div class="grid grid-cols-1 gap-6" x-data="{statusId: $wire.entangle('courseForm.status_id'),ztComing: {{ (int) $ztCourseComingSoon }},isComingSoon(){ return Number(this.statusId) === this.ztComing; }}">
<!-- Section 1 => Main Settings -->
<div class="grid grid-cols-2 {{ $sectionClasses }}">
<!-- Category -->
<div class="text-center transition-all duration-300" :class="isComingSoon() ? 'col-span-full' : ''">
<x-label class="text-center">{{ db_trans(60, 'category') }}</x-label>
<select wire:model.live="courseForm.category_id">
@foreach($categoryOptions as $option)
<option value="{{ $option->id }}">{{ ucfirst($option->name) }}</option>
@endforeach
</select>
</div>
<!-- Status => When not coming soon -->
<div x-cloak x-show="!isComingSoon()"
x-transition:enter="duration-2000 ease-out"
x-transition:enter-start="opacity-0 translate-x-8"
x-transition:enter-end="opacity-100 translate-x-0"
class="text-center will-change-transform"
>
<x-label>{{ db_trans(60, 'state') }}</x-label>
<select wire:model.live="courseForm.status_id">
@foreach($statusOptions as $option)
<option value="{{ $option->id }}">{{ ucfirst($option->name) }}</option>
@endforeach
</select>
</div>
</div>
<!-- Section 2 => Coming Soon Settings (Only appears when the status is "Coming Soon") -->
<div x-cloak x-show="isComingSoon()"
x-collapse.min.1px.duration.2000ms
x-transition:enter="duration-2000 ease-out"
x-transition:enter-start="opacity-0 -translate-y-6"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="duration-2000 ease-in"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 -translate-y-6"
wire:key="coming-soon-wrapper" class="overflow-hidden will-change-transform"
>
<div class="grid grid-cols-2 {{ $sectionClasses }} mb-6" >
<h2>Coming Soon {{ db_trans(60, 'settings') }}</h2>
<!-- Status -->
<div class="text-center">
<x-label>{{ db_trans(60, 'state') }}</x-label>
<select wire:model.live="courseForm.status_id">
@foreach($statusOptions as $option)
<option value="{{ $option->id }}">{{ ucfirst($option->name) }}</option>
@endforeach
</select>
</div>
<!-- Publish At Date -->
<div>
<x-label for="create-course-publish-at" class="text-center">{{ db_trans(60, 'published') }} am</x-label>
<input min="{{ now()->addDay()->toDateString() }}" wire:model="courseForm.publish_at" type="date" >
<x-input-error for="courseForm.publish_at" class="mt-2" />
</div>
<!-- Coming Soon Text -->
<div class="col-span-full" x-data="{ txt: $wire.entangle('courseForm.coming_soon_text') }">
<div class="flex items-center justify-center">
<x-label>"Coming Soon" {{ db_trans(60, 'description') }} <i>(<span x-text="(txt ?? '').length" class="text-primary">0</span>/250)</i></x-label>
</div>
<x-input-textarea wire:model="courseForm.coming_soon_text" name="create-course-coming-soon-text" maxlength="250"></x-input-textarea>
<x-input-error for="courseForm.coming_soon_text" class="mt-2"/>
</div>
</div>
<x-section-border sectionClasses="py-2"/>
</div>
What happens here is basically when the status is NOT "Coming Soon" There are 2 dropdowns next to each other -> Category + Status dropdowns
When the Status is changed from anything -> "Coming Soon" the Category dropdown takes the whole row, the status disappears from next to the category and a new section appears. This new section is the "Coming Soon" section.
THE ISSUE I HAVE: When I first enter the page and change the status from lets say Active -> Coming Soon, the Coming Soon section appears almost instantly, as if lagging (Instead of the animations I have set it). Then I switch it from Coming Soon -> Active, the animation is smooth. If I then without refreshing switch it again from Active -> Coming Soon the animation is smooth and how I want it (Which DOESNT happen the first time this is done). So, this only happens when the page INITIALLY LOADS and the status is switched to Coming soon. Then the rest of the workflow is smooth. I cant for the love of god figure out whats wrong and why it only happens the first time, I have tried removing classes, changing input elements, wrapping in , nothing helps. And I know it is probably something stupid or simple I have missed, but I cant find it.
EDIT: I have uploaded a video: Streamable URL
bvfi-dev wrote a reply+100 XP
5mos ago
Hey, just a suggestion, I have similar code, but I use livewire components and then use alpineJS to sort the livewire components, and it works great, as long as I provide the components keys. Heres my example:
<ul x-ref="section_list"
x-sort="() => $wire.saveOrder([...$refs.section_list.children].map(li => li.dataset.id))"
x-sort:config="{ handle: '.can-drag', filter: '.no-drag', preventOnFilter: false }"
>
@foreach ($this->sections as $section)
<li x-sort:item="{{ $section->id }}"
data-id="{{ $section->id }}"
wire:key="section-{{ $section->id }}"
>
<livewire:academy.modules.content-list
:section-id="$expandSectionId"
:section-order="$section->order"
:key="'content-' .$section->id .'-'. $section->order"
/>
And then inside the content-list component I use another x-sort, for a sub-sort. The difference is, with my code you cant put content outside their section, but it can be adjusted to work any way you need it to, the main thing is to group it with livewire components. The code becomes more manageable that way as well. I save my order like so:
public function saveOrder(array $orderedIds): void
{
foreach ($orderedIds as $index => $id) {
AcademySection::whereKey($id)->update(['order' => $index + 1]);
}
}