Creating components with AdonisJS + AlpineJS
Tags: adonisjs, alpinejs
Published: 2021-06-09 12:36:03

I recently stumbled my way into AdonisJS and fell in love with the framework.  My background is in Ruby on Rails with a ❤️ for Vue.  But I been experimenting with AdonisJS to understanding how it works and how AlpineJS can be used to add a little bit of delight to the users.

## Building forms

As an engineer I feel like my life has been a full of building forms.  So I thought I would build some form pages to understand the framework lifecycle and how easily you can create components. In the end I want to have a sign in page with:

- Components for Form, Input, Button
- Disabling all inputs when submitting
- Disabling and showing spinner on button when submitting

![ezgif.com-gif-maker (1)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t5me9em7xmmn4bzalon1.gif)

## Creating components

Components are a well [documented concept in AdonisJS](https://docs.adonisjs.com/guides/views/components).  Using edge templates you can separate out reusable components.  The trick is to try and make these the components as dumb as possible.

### Form component

Starting from something simple, you can create a form component to display consistent form behaviour.  In this case it's not that special, but you can add your success or failure success `flashMessages` here to have a consistent way of displaying errors.

- action: The route to use for form submission
- method: Form method to use for submission ( eg. PUT, POST, ... ) 
- classNames: Styling classes to apply to the form

```javascript
// app/resources/views/components/form/base.edge

<form action="{{action}}" method="{{method}}" class="{{classNames}}">
  {{ csrfField() }}

  {{{ await $slots.main() }}}
</form>

```

### TextField component
A textField component will be used to handle label and input field elements.  I used this [guide](https://dev.to/amanvirk1/introducing-adonisjs-designing-the-web-page-a8d) to create the text field component and styled it to my needs.

- label: Input label to use for display
- name: The name of the input field
- type: Input type ( eg. text, password, ... )
- flashMessages: Flash object received from AdonisJS

```javascript
// app/resources/views/components/form/text_field.edge

<div>
  <label>
    {{ label }}
    <input
      id="{{ name }}"
      name="{{ name }}"
      type="{{ type ? type : 'text' }}"
      @if(required)
        required
      @endif
      @if(placeholder)
        placeholder="{{ placeholder }}"
      @endif
      value="{{ flashMessages.get(name, '') || '' }}"
      @if(flashMessages.has(`errors.${name}`))
        class="text-red"
      @end
    >
  </label>

  @if(flashMessages.has(`errors.${name}`))
    <p>{{ flashMessages.get(`errors.${name}`) }}</p>
  @end
</div>
```

### Button component
The button component is where we can finally see some AlpineJS directives coming into play.  

Two things to note here,
1. We are coupling alpine data to the button.  If a `loading` variable is not set in a parent element then it will complain.
2. We are making the button component dumb so it doesn't mutate the state in anyway.  By bubbling up the event it lets the parent decide how the event should be handled.  In this case it could be simple as setting a `loading` value to true, but it could be more complicated in other cases.

- event: When the button is clicked what event name do we want to bubble up.
- loading: If the form/button is in a loading state
- text: The button text
- type: The type of button ( eg. submit, reset, ... )

```javascript
// app/resources/views/components/app_button.edge

<button @click="$dispatch('{{event}}')" type="{{ type }}" :disabled="loading" >
  <div x-show="loading" class="loader"></div>
  {{ text }}
</button>
```

## Putting it all together

In the sign in page we can now put all the components together to build up the page quickly.  I like to use `fieldset` as it provides a container to store the AlpineJS dataset.

For this example I am storing `{ loading: false }` as my only data variable.  The data is mutated to `true` when `submit-event` occurs.  A great feature of `fieldset` is that I can disable all input fields just by disabling the `fieldset`.  Lastly we tell the button component what event to fire when clicked, in this case that is `submit-event`.

```javascript
 @form.base({
      action: route('auth/SessionsController.create'),
      method: 'POST',
      classNames: 'mt-6',
    })
      <fieldset x-data="{ loading: false }" @submit-event="loading = true" :disabled="loading" :aria-busy="loading">
        <div class="mt-1">
          @!form.textField({
            label: 'Email address',
            name: 'email',
            type: 'email',
            flashMessageS: flashMessages
          })
        </div>

        <div class="mt-4">
          @!form.textField({
            label: 'Password',
            name: 'password',
            type: 'password',
            flashMessageS: flashMessages
          })
        </div>

        <div class="mt-4">
          @!appButton({
            event: 'submit-event',
            type: 'submit',
            text: 'Sign in',
          })
        </div>
      </fieldset>
    @end
```

## Conclusion

So far it has been an interesting journey using AdonisJS.  I am only at the beginning of trying to understand how AdonisJS works and how to get the best out of it.  But so far love the work that the AdonisJS team has put into it especially with the v5 release.