bvfi-dev's avatar

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's avatar

bvfi-dev wrote a reply+100 XP

5mos ago

Yea, that actually makes sense, I can just abstract and extend that, then use it instead of the form with a snapshot. Thank you for the idea

bvfi-dev's avatar

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's avatar

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:

  1. In my default app layout, I insert the livewire component:
@stack('modals')
@livewire('components.notification-manager')

Then my component is like:

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's avatar

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's avatar

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's avatar

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's avatar

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:

Streamable URL

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's avatar

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:

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's avatar

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]);
		}
	}