JakovljevicFilip's avatar

JakovljevicFilip liked a comment+100 XP

1mo ago

Indeed a very complex lecture, in my opinion due to the nature that most stuff is specific to "alpine.js". Apart from that it would really help a lot to use an IDE/editor where the filename is permanently visible.

JakovljevicFilip's avatar

JakovljevicFilip liked a comment+100 XP

1mo ago

@radmax @andrewdv8 @halvarado77

Solution for Browser Test Issues with File Upload Forms

I experienced the same issue and found a solution based on a comment from the next video. The problem occurs when using enctype="multipart/form-data" on forms with Playwright browser tests.

The Fix

The solution is to dynamically set the enctype attribute using Alpine.js, only when a file is actually selected.

Step 1: Update the form's x-data

In resources/views/idea/index.blade.php, add a new hasImage property to track when a file is selected:

<form
    x-data="{
        status : 'pending',
        newLink: '',
        links: [],
        newStep: '',
        steps: [],
        hasImage: false
    }"
    action="{{ route('idea.store') }}"
    method="POST"
    class="space-y-4"
    x-bind:enctype="hasImage ? 'multipart/form-data' : false"
>

Step 2: Replace static enctype with dynamic binding

Replace the static enctype="multipart/form-data" attribute with:

x-bind:enctype="hasImage ? 'multipart/form-data' : false"

Step 3: Update the file input

Add an @change event to the file input to toggle hasImage when a file is selected:

<input 
    type="file" 
    name="image" 
    accept="image/*" 
    @change="hasImage = $event.target.files.length > 0" 
/>

Why This Works

This approach prevents the form from using multipart/form-data encoding when no image is uploaded, which resolves conflicts with Playwright browser tests. The enctype is only set when an actual file is selected by the user.

Benefits

  • ✅ Browser tests pass successfully without modifications to Composer packages
  • ✅ File uploads still work correctly for end users
  • ✅ No impact on form functionality
  • ✅ Clean, maintainable solution using Alpine.js

Test Results

After implementing these changes:

PASS  Tests\Browser\CreateIdeaTest
✓ it creates a new idea (1.95s)

Tests:    1 passed (9 assertions)
Duration: 2.42s

Hope this helps!

JakovljevicFilip's avatar

JakovljevicFilip liked a comment+100 XP

1mo ago

Hi just a quick question about security I may not be the only one wondering this so thought I'd ask. Anything in the public/ directory is public. So that means all featured images would be publically accessible providing you can guess the randomly generated URL. Is this covered in the Authorization is a requirement lesson where we ensure only the user can access their own idea images?

JakovljevicFilip's avatar

JakovljevicFilip liked a comment+100 XP

1mo ago

For anyone following along: if you're using Sail, you need to run the storage:link command through Sail.

Instead of:

php artisan storage:link

Use:

vendor/bin/sail artisan storage:link
JakovljevicFilip's avatar

JakovljevicFilip liked a comment+100 XP

1mo ago

@kumarayush Did you use the PATCH method in the form? Remember, HTML forms only support GET and POST.

The solution is to use POST and add a hidden input called _method, like this:

<form method="POST" action="{{ route('step.update', $step) }}">
    <input type="hidden" name="_method" value="PATCH">
</form>

But in Laravel, you can use the helper directive instead:

<form method="POST" action="{{ route('step.update', $step) }}">
    @csrf
    @method('PATCH')

    <!-- your inputs here -->

    <button type="submit">Update</button>
</form>

That way Laravel will correctly handle the PATCH request.

JakovljevicFilip's avatar

JakovljevicFilip liked a comment+100 XP

1mo ago

In the controller, I modified the logic for storing an idea a little bit. I believe that in this case it is good practice to use database transactions because we depend on two queries working correctly: creating the Idea and creating each of its steps.

If one of them fails, the whole operation should be rolled back to keep the database consistent.

public function store(StoreIdeaRequest $request)
    {
        DB::transaction(function() use ($request): void {
            $idea = Auth::user()->ideas()->create($request->safe()->except('steps'));

            $steps = collect($request->safe()->input('steps', []))
                ->map(fn($step) => ['description' => $step])
                ->all();

            if ($steps !== []) {
                $idea->steps()->createMany($steps);
            }
        });

        return to_route('idea.index')
            ->with('success', 'Idea created!');
    }
JakovljevicFilip's avatar

JakovljevicFilip liked a comment+100 XP

2mos ago

In this case I would have created a table that contain the statuses, and with a simple join select the count of each instance, that way I don't need to use any map to get what I need.

JakovljevicFilip's avatar

JakovljevicFilip wrote a comment+100 XP

2mos ago

@Tray2, that's one way of doing it, and it's not wrong. I'd personally still store them as ENUMs anyway. Here's why:

Storing data in a database seems to imply (in my mind at least) that such data can change. I expect that I am able to add new entries and/or change/remove existing ones. Maybe you know that this isn't the case, but if I am a new developer on the team or someone who's not familiar with that particular domain, it wouldn't be easy to tell.

There are clean ways to bypass this. Hopefully, you've written tests that emphasize that we expect these particular values to always be there. That should be enough to raise a couple of red flags about changing the setup. It's not wrong, strictly speaking; you can make it work.


Storing something inside an ENUM has clear implications. If I moved to a new, unfamiliar project and saw an ENUM, I'd immediately know that:

  1. These are all of the possible values.
  2. Whoever made this relies on these specific values being here.
  3. Changing anything will probably require making changes elsewhere in the system. Do I truly understand the scope of these changes?

These are obvious red flags. Ideally, there would still be specific test cases that would further secure the ENUM.


It's neither strictly correct nor wrong to do it one way or the other. Both options come with compromises. This is mainly a perspective on why I'd prefer ENUM in this case.

JakovljevicFilip's avatar

JakovljevicFilip wrote a comment+100 XP

2mos ago

@codeplumber, I agree that it reads better, but be aware of potential performance issues down the line.

Doing a GROUP BY like in the video delegates counting to the DB. This is something that DBs do all the time and is already fairly optimized. In this case, PHP receives already prepared counts for each of the categories.

Doing an ideas()->get()->countBy('status') will:

  1. Select rows from the database;
  2. Pass all of these rows from the DB to PHP;
  3. Load them as Eloquent models (the reason we have all of these different helper methods available); and
  4. Iterate over all of these Eloquent objects to get the count.

This is fine for a tutorial project, but it won't scale well down the line. While it would take 1000s of records to notice any real difference, I'd still suggest using the first approach. It's better to be safe than sorry.

JakovljevicFilip's avatar

JakovljevicFilip liked a comment+100 XP

2mos ago

As much as I like to practice array mapping and manipulation, countBy() helps to clean the logic up a bit:

// Get counts in the controller
'counts' => $request->user()->ideas()->get()->countBy('status');

// Get counts in view, including those with no records
{{ $counts[$ideaStatus->value] ?? 0 }}
JakovljevicFilip's avatar

JakovljevicFilip liked a comment+100 XP

2mos ago

Here are the full CSS component files for those looking to copy:

resources/css/components/btn.css

resources/css/components/form.css

JakovljevicFilip's avatar

JakovljevicFilip liked a comment+100 XP

2mos ago

Color theme for those looking to copy:

    --color-background: oklch(0.12 0 0);
    --color-foreground: oklch(0.95 0 0);
    --color-card: oklch(0.16 0 0);
    --color-primary: oklch(0.65 0.15 160);
    --color-primary-foreground: oklch(0.12 0 0);
    --color-border: oklch(0.24 0 0);
    --color-input: oklch(0.24 0 0);
    --color-muted-foreground: oklch(0.6 0 0);
JakovljevicFilip's avatar

JakovljevicFilip liked a comment+100 XP

3mos ago

@nosthas

JakovljevicFilip's avatar

JakovljevicFilip wrote a comment+100 XP

3mos ago

@katamariBall

You're welcome, we're all here to learn.

That is helpful. If the form was filled at some point and then became empty, that almost certainly means that the page refreshed. One way for a page to refresh is to click the Register button at the top of the page. This is most likely what's happening here.

One way to confirm this would be to place debug() before press():

        ->debug()		
        ->press('@register-button')
        ->assertPathIs('/idea');

I expect that the form would be filled in at this point. Then I would try to click on the Register button inside the debug view to make sure that the form is being submitted.

If clicking on the Register button signs you up then there is a problem with ->press(). If it fails, then there is some other problem.


Another possible problem might be related to the RefreshDatabase. This needs to be enabled in the tests/Pest.php for the flow to work - around 06:00 in the video.

Without it database will not clear up between tests. Your first-ever attempt at registration would succeed, but every next run will fail because registration requires a unique email address, and that email already exists in the database (because the database didn't reset/refresh).

JakovljevicFilip's avatar

JakovljevicFilip wrote a comment+100 XP

3mos ago

Hey @katamariBall, maybe you could place a debug() after the assertPathIs() so that you can see where you are at that point?

Something like this:

        ->press('@register-button')
        ->debug()
        ->assertPathIs('/idea');

If you're still on the same page, then the press didn't work correctly.


Bear in mind that even if you try to do something like this:

<a ... disabled="true">Register</a>

The test will effectively behave the same. It won't skip the disabled button, it will click on it, which won't do anything.


My main culprits:

  1. There's a typo in either ->press('@register-button') or data-test="register-button".
  2. Maybe you've accidentally placed the data-test="register-button" in the navbar's registration button instead of on the form.

If you still require assistance, I'd be happy to help. Happy coding!

JakovljevicFilip's avatar

JakovljevicFilip liked a comment+100 XP

3mos ago

Is 'password' => Hash::make($attributes['password']) truly needed when User model has this cast 'password' => 'hashed' by default? I know the clear password issue should be mentioned here but I think this is also an opportunity to mention casts.

JakovljevicFilip's avatar

JakovljevicFilip liked a comment+100 XP

3mos ago

At 7:28 I assume you meant to remove a validation rule, however there isn't one currently in the update method. Instead you remove the update on the Idea model. Presumably this is a mistake? Great tutorial so far btw :)

JakovljevicFilip's avatar

JakovljevicFilip wrote a comment+100 XP

3mos ago

@abordolo I believe that you are correct, well spotted!

From the docs:

You may specify which attributes should be considered data variables using the @props directive at the top of your component's Blade template. All other attributes on the component will be available via the component's attribute bag. If you wish to give a data variable a default value, you may specify the variable's name as the array key and the default value as the array value:

 
@props(['type' => 'info', 'message'])
 
<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
    {{ $message }}
</div>

Source:

https://laravel.com/docs/12.x/blade#data-properties-attributes


So in this case, the component will assume that $type is always set to 'info', unless specified otherwise.

On the other hand, if $message is not passed, Laravel won't know what to do since it has no default value. That would cause the page to break.

JakovljevicFilip's avatar

JakovljevicFilip liked a comment+100 XP

3mos ago

Blade prop

Hi @jeffry, in the error component, I think, following code gives a default value of 'required' to $name and not make it mandatory like in Vue prop.

@props([
    'name' => 'required',
])
JakovljevicFilip's avatar

JakovljevicFilip liked a comment+100 XP

3mos ago

One important thing when creating routes like that, is the order.

Route::get('/ideas/create', [IdeaController::class, 'index']);

Must be defined before

Route::get('/ideas/{idea}', [IdeaController::class, 'show']);

Or you will scratch your head as to why you get a 404 when trying to navigate to the create Idea route.

JakovljevicFilip's avatar

JakovljevicFilip wrote a comment+100 XP

3mos ago

Warning:

If it seems like there are no ideas stored in session at 15:00, it might be happening because you tried to register the GET route like this:

Route::view('/', 'idea', [
    'ideas' => session('ideas', []),
]);

Instead, use the approach from the video and register the route under Route::get()


Brief explanation:

Route::view is trying to be efficient, so it loads its arguments only once at the very beginning.

From then on, whenever we try to access the route again, Route::view will always use the original session value instead of the updated one.

If you tried to render the session variable inside the blade view, you would always see the default value - in this case, an empty array, because that's what was set at:

'ideas' => session('ideas', []),


As Jeffrey mentioned in an earlier lesson (4), Route::view is meant for simpler, mostly static pages. Loading pages this way is more efficient, but it has downsides, as you can see in this example.