Playing Next Lesson In
seconds

EdgeJS Components #3.1

Adding Interactivity to our Button

In This Lesson

We'll add extendable interactivity to our button using AlpineJS. We'll also walk through a demonstration of how we can utilize event propagation in AlpineJS to our advantage.

Created by
@tomgobich
Published

Before we start extending off our base button with our different styles, let’s first add interactivity so it’s not just a basic HTML button.

Event Propagation

First, let’s add x-data to the parent div of our button within our demonstration page. This will bind AlpineJS to the element, enabling Alpine state, events, etc. Meaning, we can also add a click handler to it.

@layout.app({ title: request.params().name })

  <h3 class="font-bold">Base Button</h3>
  <div 
    x-data 
    @click="console.log('clicked')" 
    class="mb-8 space-x-3 space-y-3">
  <div class="mb-8 space-x-3 space-y-3">
    @button()
      Base Button
    @end
  </div>

@end
Copied!
  • resources
  • views
  • pages
  • components
  • buttons.edge

Currently, this won’t do anything in terms of our actual button. However, if we add x-data to our button as well, we’ll see that AlpineJS propagates the click even from our button, up to our parent div’s click handler.

<button 
  {{ $props.serializeExcept(['type', 'class']) }}
  x-data
  type="{{ type || 'button' }}"
  class="{{ $props.class }} inline-flex items-center justify-center px-4 py-2 text-sm font-medium tracking-wide text-white transition-colors duration-200 rounded-md bg-neutral-950 hover:bg-neutral-900 focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900 focus:shadow-outline focus:outline-none">
  {{{ await $slots.main() }}}
</button>
Copied!
  • resources
  • views
  • components
  • button
  • index.edge

Now if we jump into the browser, inspect our console, then give our button a click we should see “clicked” logged out.

AlpineJS Event Propagation

As you can see, now that our button has x-data attached to it AlpineJS is now capturing the click event, propagating it up the AlpineJS tree where our parent @click handler is capturing the event and logging out.

This exercise demonstrates that you don’t have to have the handler defined inside our button component and we can instead rely on propagation to capture one-off events not directly related to the component’s functionality.

Direct Event Handling

There’s plenty of use-cases were we’ll instead want the event handler residing directly on our button. We can do this as well with two different approaches, either using the serialize prop approach or a separate atClick prop.

AtClick Prop

First we’ll want to conditionally add the Alpine @click onto our element if an atClick prop was passed into our button component.

<button 
  @if (atClick)
    @click="{{ atClick }}"
  @endif

  {{ $props.serializeExcept(['type', 'class']) }}
  x-data
  type="{{ type || 'button' }}"
  class="{{ $props.class }} inline-flex items-center justify-center px-4 py-2 text-sm font-medium tracking-wide text-white transition-colors duration-200 rounded-md bg-neutral-950 hover:bg-neutral-900 focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900 focus:shadow-outline focus:outline-none">
  {{{ await $slots.main() }}}
</button>
Copied!
  • resources
  • views
  • components
  • button
  • index.edge

Next, we simply provide a click handler as the atClick prop into our component. Note that we’re passing this click handler inside a string. This is so we can pass it from EdgeJS to AlpineJS and have it be a valid client side script.

@layout.app({ title: request.params().name })

  <h3 class="font-bold">Base Button</h3>
  <div 
    x-data 
    @click="console.log('clicked')" 
    class="mb-8 space-x-3 space-y-3">

    @button({ atClick: `() => console.log('atClick')` })
    @button()
      Base Button
    @end

  </div>

@end
Copied!
  • resources
  • views
  • pages
  • components
  • buttons.edge

AlpineJS Prop

The next approach, and my personal preference, is to directly pass the @click as a prop and have $props.serializeExcept define it directly as an attribute.

For this, we don’t need to change anything inside our component. since we already have serialize except added.

<button 
  {{ $props.serializeExcept(['type', 'class']) }}
  x-data
  type="{{ type || 'button' }}"
  class="{{ $props.class }} inline-flex items-center justify-center px-4 py-2 text-sm font-medium tracking-wide text-white transition-colors duration-200 rounded-md bg-neutral-950 hover:bg-neutral-900 focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900 focus:shadow-outline focus:outline-none">
  {{{ await $slots.main() }}}
</button>
Copied!
  • resources
  • views
  • components
  • button
  • index.edge

Instead, all we need to do is directly provide the prop with a key of @click.

@layout.app({ title: request.params().name })

  <h3 class="font-bold">Base Button</h3>
  <div 
    x-data 
    @click="console.log('clicked')" 
    class="mb-8 space-x-3 space-y-3">

    @button({ '@click': `() => console.log('atClick')` })
    @button()
      Base Button
    @end

  </div>

@end
Copied!
  • resources
  • views
  • pages
  • components
  • buttons.edge

Note that the @ character will invalidate our prop object, so we do need to define our key as a string.

The added benefit of this approach is that we can extend our handlers with chainable options. For example, if we want to stop the propagation behavior we walked through earlier, we could use the .stop chain on our click handler to tell it to stop propagation.

@layout.app({ title: request.params().name })

  <h3 class="font-bold">Base Button</h3>
  <div 
    x-data 
    @click="console.log('clicked')" 
    class="mb-8 space-x-3 space-y-3">

    @button({ '@click.stop': `() => console.log('atClick')` })
      Base Button
    @end

  </div>

@end
Copied!
  • resources
  • views
  • pages
  • components
  • buttons.edge

Now if we run the above with our console open, you’ll see “atClick” gets console logged while “clicked” does not, because our propagation was stopped and this handler was never called.

AlpineJS Stop Event Propagation

Join the Discussion 0 comments

Create a free account to join in on the discussion
robot comment bubble

Be the first to comment!