How Do I Encapsulate My AlpineJS Logic

How Do I Encapsulate My AlpineJS Logic

Make Your Alpine Components More Readable

ยท

4 min read

Recently, AlpineJS becomes more and more popular. AlpineJS is to Javascript what TailwindCSS is to CSS. This means, that like in TailwindCSS, you are applying your code directly into your HTML. This is super handy, but as always, there are some traps and downsides. I am going to show you, how I keep the logic of my AlpineJS components clean.

What can make your AlpineJS experience worse

  1. The code for Alpine Components is written in Alpine-specific attributes like x-data, x-show or @ click. Their values are strings, therefore, code highlighting isn't working.

  2. For the same reason, any linter would not work.

  3. And last but not least, it might be harder to debug the code inside of string. You cannot set breakpoints, step through etc.

So I am going to show you how I encapsulate my logic and keep my HTML code readable, lintable and 'debuggable'.

Extract your x-data code to a separate script tag or even file

This is what I do in most of all use cases. Only if there is one statement, I leave the x-data as a string. In all other cases I extract them to a script tag and if this gets more than one page (between 35 and 40 lines of code) I will extract the logic to a separate file and leave the script tag with the basic logic.

Assume you have a simple form like this one below. If any input field is empty, the Sign In button isn't available.

<form x-data="{ username: '', password: '' }">

  <input type="text" x-model="username">
  <input type="password" x-model="password">
  <button :disabled="!username || !password">Sign in</button>

</form>

Imagine writing this in vanilla javascript! It's always a pleasure to see how simple it is to achieve this kind of tasks. Anyway, what if you want a more sophisticated respond to the user's input. You might check if an username is already taken. This would take additional line lines of code and would be hard to read, if you put it in a string. A script tag is a better place for that.

<script>
  const compSignIn = () => {
    return {

      username: '',

      // use getter and setter for more control over variables
      // i. e. set breakpoints or disable a setter
      get password () { return this.__password },
      set password (value) { this.__password = value },

      isValid () {

        // do a fetch to check wether the username is already taken
        // ...

        // for now, do the validation from the 1st example
        return !this.username || !this.password
      },

      // private
      __password: ''
    }
  }
</script>

<form x-data="compSignIn()">

  <input type="text" x-model="username">
  <input type="password" x-model="password">
  <button :disabled="isValid">Sign in</button>

</form>

Despite all advantages that I mentioned before, this implementation reduces the size of the HTML and is easier to read. So, if your logic goes beyond simple tasks like enabling/disabling buttons or changing text of an element, this is a good habit.

Use events to interchange states or objects

A website will have more than one Alpine Component most of the time. And if they have to interact with each other, there are two possible ways to do get it done.

This one uses a surrounding tag, and make the shared data available to both tags.

<body x-data="{ isVisible: false }">

  <button @click="isVisible = !isVisible">Say Hello!</button>

  <div x-show="isVisible">
    Hello, I am here ๐Ÿ‘‹!
  </div>

</body>

But imagine your website is rendered by a CMS and the different routes will be composed of different components. The body tag must include the logic of all possible components. In such case, I usually make the use of Events.

How we can achieve this. You might discover the property $dispatch in the AlpineJS documentation. With this so-called magic property, you can send events to any component at the DOM. And along with the name of the event, you can include a JavaScript object, which can hold additional data for the receiving component.

<body>

  <button
    @click="$dispatch('new-message', { message: 'Hello Marge!' })"
    x-data>Say hello to Marge</button>

    <button
    @click="$dispatch('new-message', { message: 'Hello Homer!' })"
    x-data>Say hello to Homer</button>

  <div
    @new-message.window="text = $event.detail.message"
    x-text="text"
    x-data="{ text: '' }">
  </div>

</body>

There are three independent Alpine Components in this example. Two buttons which emits a message and a div which receives messages. The buttons emit a message with different message content. The div receives and display them.

Important note: You need to use the window modifier, because the nodes are in the same nesting hierarchy.

Conclusion

  1. Use inline code rarely. If it's a one-liner, it's ok.
  2. Extract bigger components into script tags or even in a separate file.
  3. Use getter and setter inside of objects. You can debug any assignment or request of a variable.
  4. Use messages to interchange state and data between your AlpineJS Components.
  5. Install the official AlpineJS Extension to your browser, so you can inspect all your AlpineJS Components.
ย