loading...

Rocket — becoming a known developer… and building an app to help make it happen — Part 3

mattkingshott profile image Matt Kingshott 👨🏻‍💻 Originally published at itnext.io on ・9 min read

Rocket — becoming a known (Laravel) developer… and building an application to help make it happen — Part 3

In this series, we will be reviewing the steps that we as developers need to take in order to create a suitable web presence and build an audience that we can communicate ideas, projects and commercial offerings with.

In addition to theory articles discussing marketing, presentation and methods of engagement, we will also be building Rocket — a Laravel application that will allow you and other developers to create a personal site and grow an audience through social media and article publishing… let’s dive in!

Today’s agenda

In this article, we’ll be adding the user interface elements required to provide a suitable experience when subscribing to the site. We’ll also begin creating a library of Blade components to support this. Finally, we’ll add the missing email verification functionality to the subscribe action.

Step #1 — Add email verification

Right now, our Laravel app registers new subscribers, but there’s no way to verify them. Let’s fix that by sending them an email with a link they can use to confirm that their email address exists and is in use.

We’ll start by creating the notification:

php artisan make:notification VerifyEmail

Next, we’ll need to add a signed route that the subscriber will use to verify the email address. A signed route is a URL that Laravel constructs in such a way to prevent it being modified. This is perfect for our needs, as we want a user to be able to only verify their own email address and not someone else’s:

Route::get('/verify/{subscriber}', 'SubscriptionController@update')
     ->name('verify');

Sweet. Now, let’s add the relevant code to the VerifyEmail notification to enable it to send an email to the subscriber:

class VerifyEmail extends Notification
{
    /**
     * Get the mail representation of the notification.
     *
     */
    public function toMail($notifiable) : MailMessage
    {
        $url = url()->signedRoute('verify', [
            'subscriber' => $notifiable->id,
        ]);

        return (new MailMessage)
            ->from(config('mail.from.address'))
            ->subject(config('site.name') . ' - Verify your email')
            ->greeting('Hey there 👋🏻')
            ->line('Thanks for subscribing to my site!')
            ->line('Would you mind please verifying your email?')
            ->action('Verify Email', $url);
    }
}

Let’s review what’s happening here. We’re using the URL helper to generate a signed URL to the route, and we’re setting the {subscriber} parameter to be equal to the $notifiable (subscriber) id.

After that, we’re just setting up the various elements of the email, such as the from, subject and greeting fields. Finally, we’re adding an action, which will create a link button configured to use the signed URL.

The final step, is to send this email to the subscriber when they register:

use App\Notifications\VerifyEmail;
use App\Requests\Subscriber\StoreRequest;

class SubscriptionController extends Controller
{
    /**
     * Store a new subscriber.
     *
     */
    public function store(StoreRequest $request)
    {
        $subscriber = Subscriber::create($request->validated());

        $subscriber->notify(new VerifyEmail());
    }
}

Of course, if we were to try this, it would fail. The reason, is that we haven’t told Laravel that our Subscriber model is capable of handling notifications. To do that, we need to add the Notifiable trait to the model:

use Illuminate\Notifications\Notifiable;

class Subscriber extends Model
{
    use Notifiable;
}

Perfect! We’re now sending an email to new subscribers asking them to verify.

Step #2 — Sending flash messages

Right now, our app just sends back simple strings when we visit our routes. This was sufficient for testing, but it’s high time we replaced them with proper flash messages / notifications. To do that, let’s add a helper method to our base controller that we can use to easily send information to the session:

class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;

    /**
     * Flash a notification to the session.
     *
     **/
    public function flash($message, $type = 'info', $fixed = false)
    {
        session()->flash(
            'notification', compact('message', 'type', 'fixed')
        );
    }
}

Next, we’ll replace our string messages with calls to this method:

$this->flash('Got it! You have been unsubscribed');

Finally, we’ll need to create an inline Blade component to style the message and display it to the user when the page is rendered. We’ll also need to allow for different types of messages e.g. ‘info’, ‘success’ or ‘error’, as well as handle situations where the notification should be static / not fade out after a while.

Let’s first define the Blade component (simplified for brevity):

{{-- Notification --}}
<div class="notification absolute right-0 top-0 mt-6 mx-6
            {{ $fixed ? '' : 'fade-out' }}
            {{ filled($message) ? '' : 'hidden' }}">

    {{-- Content --}}
    <div class="flex rounded-lg shadow-md bg-white px-5 py-3">

        {{-- Icon --}}
        <div class="mr-3 flex">
            <svg width="20"
                 height="20"
                 viewBox="0 0 24 24"
                 stroke="currentColor"
                 class="@if ($type == 'info') text-blue @endif
                        @if ($type == 'success') text-green @endif
                        @if ($type == 'error') text-red @endif">

                {{-- Path --}}
                <path stroke-width="2"
                      stroke-linecap="round"
                      stroke-linejoin="round"
                      d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9"/>

            </svg>
        </div>

        {{-- Message --}}
        <span class="text-md text-gray-700 leading-normal">
            {{ $message }}
        </span>

    </div>

</div>

Next, we’ll add the component to our general content area so that it will be displayed directly beneath the header:

{{-- Notification --}}
<x-notification :type="session('notification.type') ?? 'success'"
                :fixed="session('notification.fixed') ?? false"
                :message="session('notification.message') ?? ''">
</x-notification>

Excellent. We now have a working notification system! Of course, there’s more to this than what’s shown above (such as fading), so check out the repo for all the gritty details regarding how that works.

Step #3 — Creating the subscribe form

Our final major task for today, is to upgrade our subscribe call-to-action view to include a proper form with an email address field and a submit button.

Let’s begin by creating some additional reusable Blade components for these input / form elements. We’ll start with the button:

{{-- Button --}}
<div class="flex mt-4 justify-end">
    <button onclick="event.preventDefault(); {{ $action ?? '' }}"
            class="font-bold text-sm uppercase py-3 px-6 transition 
                   duration-300 rounded-md text-white
                   bg-blue-700 hover:bg-blue-500
                   outline-none focus:outline-none">
        {{ $text }}
    </button>
</div>

For now, this creates a simple blue button that, when clicked, ignores all the standard JavaScript behaviour and instead executes the given action e.g.

<x-button text="Subscribe"
          action="alert('hello')">
</x-button>

Now, let’s move on to the textbox. We can add many optional attributes here to enable easy customisation later on:

{{-- Textbox --}}
<div class="field">

    {{-- Input --}}
    <input type="{{ $type ?? 'text' }}"
           name="{{ $name ?? 'textbox' }}"
           placeholder="{{ $placeholder ?? '' }}"
           autocomplete="{{ $autocomplete ?? '' }}"
           value="{{ $value ?? old($name ?? 'textbox') }}"
           class="input border border-gray-300 rounded-md text-md 
                  leading-normal py-3 px-4 outline-none w-full 
                  focus:outline-none">

    {{-- Validation --}}
    <span class="validation text-red-700 font-bold text-xs uppercase 
                 w-full pl-1 mt-3
                 {{ $errors->has($name ?? 'textbox') 
                    ? 'inline-block' 
                    : 'hidden' 
                 }}">

        @error ($name ?? 'textbox')
            {{ $message }}
        @enderror

    </span>

</div>

Some nice takeaways in the component include:

  1. The textbox allows us to set its value, otherwise it falls back to using any old data from the request (perfect for failed validation).
  2. Placeholder and autocomplete support.
  3. The ability to change the input type e.g. textbox, email etc.
  4. A clean validation section that displays errors when any are present.

Our final component, is a friendly wrapper around the standard HTML form. We can use this to automatically include CSRF protection and set the method type so Laravel can properly route PUT, PATCH and DELETE requests:

@php

$method = in_array(($type ?? 'post'), ['put', 'patch', 'delete']) 
        ? 'post' 
        : ($type ?? 'post');

@endphp

{{-- Form --}}
<form action="{{ $url }}"
      method="{{ $method }}">

    {{-- CSRF Token --}}
    @csrf

    {{-- Method --}}
    @method($type ?? 'post')

    {{-- Fields --}}
    {{ $slot }}

</form>

Now, let’s put these elements together so that we can add a proper subscribe form to our call-to-action view:

{{-- Form --}}
<x-form url="/subscribe">

    {{-- Email --}}
    <x-textbox type="email"
               name="email"
               autocomplete="email"
               placeholder="Email address...">
    </x-textbox>

    {{-- Submit --}}
    <x-button text="Subscribe"
              action="Form.submit()">
    </x-button>

</x-form>

Excellent. It looks nice, clean and is easy to maintain, which is exactly what we should be aiming for. Here’s how it looks in the wild:

Step #4 — Making it snappy

Right now, all form submissions require a full post-back to the server and thus a complete re-rendering of the view. There’s nothing wrong with this, indeed many are advocating a return to the relative simplicity of this approach over the complexity of client-side scripting. See a recent post by Jeffrey Way:

I’m very much in agreement, and I feel Laravel 7’s Blade components have gone a great way to helping with this issue. However there is one thing I really love from the client-side scripting / SPA world… fast page transitions.

Fortunately, for GET requests, we can resolve this problem very easily thanks to the Turbolinks library. All we have to do, is import the script and call the start method when our page loads:

<script src="[unpkg.com/turbolinks/dist/turbolinks.js](https://unpkg.com/turbolinks@5.2.0/dist/turbolinks.js)"></script>

<script>
    Turbolinks.start();
</script>

Turbolinks will then replace all anchor tags with JavaScript versions that load the desired page and replace the current page’s HTML. I won’t go into its exact mechanics for time reasons, so feel free to check out the full readme for more.

Step #5 — Submitting AJAX forms

Turbolinks is great, but it doesn’t work for submitting forms to the server. The sad truth, at least for now, is that there’s no simple way to address this issue… it requires some custom JavaScript, unless of course you want to go down the Laravel Livewire road, which is also an option.

Fortunately, I’ve written a nice 4 KB library, called Form.js, that handles form submission, CSRF, expired tokens, notifications, redirects, validation errors and scrolling the page to the first issue.

It works with our current setup of Blade components, and best of all, like Turbolinks, you don’t actually have to have any interaction with it. Instead, all you need to do, is simple call Form.submit() from a button inside the form. The library will take care of the rest, which is ideal!

There is one caveat, in order to enable flash messages between page loads, we have to add a few lines of code to our app.js file to enable state:

window.notification = null;

Turbolinks.start();

document.addEventListener("turbolinks:load", () => Form.notify());

That’s it for the client-side, but what about the server-side of things? We can’t use the session when responding to form submissions via AJAX, because there is no session. Instead, we have to send back the relevant data as JSON.

To make this easier, let’s create a custom JsonResponse class that will wrap this functionality behind a set of friendly helper methods:

use Illuminate\Http\JsonResponse;

class Response extends JsonResponse
{
    /**
     * The response payload.
     *
     */
    protected array $payload = [
        'redirect' => '',
        'notification' => [
            'message' => '',
            'type' => 'success',
            'fixed' => false,
        ],
    ];

    /**
     * Add a notification to the response.
     *
     */
    public function notify($message, $type = 'info', $fixed = false)
    {
        $this->payload['notification'] = compact(
            'message', 'type', 'fixed'
        );

        $this->setData($this->payload);

        return $this;
    }

    /**
     * Add a redirect to the response.
     *
     */
    public function redirect($url)
    {
        $this->payload['redirect'] = $url;

        $this->setData($this->payload);

        return $this;
    }
}

Next, within our base controller, let’s add a helper method to generate one of these response classes. We’ll also allow for the status code, headers etc.

use App\Types\Response;

class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;

    /**
     * Create a response to send to the client.
     *
     */
    protected function response($data, $status, $headers, $options)
    {
        return new Response($data, $status, $headers, $options);
    }
}

Finally, we need to update our SubscriptionController to use the helper:

/**
 * Store a new subscriber.
 *
 */
public function store(StoreRequest $request) : Response
{
    $subscriber = Subscriber::create($request->validated());

    $subscriber->notify(new VerifyEmail());

    return $this
        ->response()
        ->notify('Great! Please verify your email')
        ->redirect(url()->previous());
}

Wonderful! We’re all set to go. We now have SPA-style browsing and form submission within our application. It took a little bit of setup, but in future, adding this to any POST-like request will be a trivial affair 😎

Step #6 — Extra credit

There’s only one thing left to do. We need to update our tests to handle the changes we’ve made. Firstly, we’ll need to switch the session assertions to JSON assertions. Then, we’ll need to fake the notification facade and inspect its contents to confirm that an email was configured correctly, and then sent:

/** @test */
public function a_subscriber_can_subscribe_themselves()
{
    Notification::fake();

    $this->post('/subscribe', ['email' => 'john@example.com'])
        ->assertSuccessful()
        ->assertJsonFragment([
            'notification' => [
                'type' => 'success',
                'fixed' => false,
                'message' => 'Great! Please verify your email',
            ],
        ]);

    $this->assertCount(1, Subscriber::get());
    $this->assertNull(Subscriber::first()->verified_at);

    $this->assertEquals(
        'john@example.com', Subscriber::first()->email
    );

    Notification::assertSentTo(
      Subscriber::first(), VerifyEmail::class, function($obj) {
        $email = $obj->toMail(Subscriber::first());

        $this->assertEquals(
            config('mail.from.address'), $email->from[0]
        );

        $this->assertEquals(
            config('site.name') . ' - Verify your email', 
            $email->subject
        );

        $this->assertEquals('Hey there 👋🏻', $email->greeting);

        $this->assertTrue(
            Str::contains(
                $email->actionUrl, url('/subscribe/verify/')
            )
        );

        return true;
    });
}

Wrapping up

Now that we can fully register and unsubscribe users, and do so in a way that feels like an app rather than HTML 1.0 website, we’re ready to move on to the “projects” section of the application.

That’s our next job in part #4. I hope you’re excited for it!

To ensure you’re notified when it comes out, why not go ahead and follow me here on Medium, or better yet, on Twitter, where I’ll also be posting additional updates as well as links to new articles.

Thanks, and have a great day! 😎

Posted on May 7 by:

mattkingshott profile

Matt Kingshott 👨🏻‍💻

@mattkingshott

Founder. Developer. Writer. Lunatic. Created Pulse, IodineJS, Axiom, and more. #PHP #Laravel #Vue #TailwindCSS

Discussion

markdown guide