Events

In order to declare an event property, you must use the property function and define the type as :event :

property name, :event , options

Where:

  • name - is the name of the event.
  • options - a keyword list of options for additional customization.

Supported options

  • required - declares the event as required. Default is false .
  • default - defines a default value for an optional event.

Example:


defmodule MyButton do
use Surface.Component

@doc "Triggers on click"
property click, :event

def render(assigns) do
...
end
end

Handling events in LiveView

In Phoenix LiveView (without Surface) when dispatching events in live components, the default target is the parent live view, not the component itself. If you need to handle events locally, you usually need to:

  • Set the phx-[event] attributes on the elements which events need to be listened to.

  • Set the phx-target attribute on those same elements to indicate that you want to handle them locally.

This can be non-intuitive, especially if you’re coming from any existing component-based library like React or Vue.js .

Note : The main reason behind this design choice, as explained by José Valim in this discussion , is that, when using Phoenix templates, it’s impossible to know what is the parent and what is the child. There’s no way to retrieve that information since templates are not treated as structured code, they are just text.

Here’s the Counter example used in the Data section, but this time using Phoenix templates :

0


defmodule Counter do
use Phoenix.LiveComponent

def mount(socket) do
{:ok, assign(socket, count: 0)}
end

def render(assigns) do
~L"""
<div>
<h1 class="title">
<%= @count %>
</h1>
<div>
<button class="button is-info" phx-click="dec" phx-target="<%= @myself %>">-</button>
<button class="button is-info" phx-click="inc" phx-target="<%= @myself %>">+</button>
<button class="button is-danger" phx-click="reset" phx-target="<%= @myself %>">Reset</button>
</div>
</div>
"""
end

def handle_event("inc", _, socket) do
{:noreply, update(socket, :count, & &1 + 1)}
end

def handle_event("dec", _, socket) do
{:noreply, update(socket, :count, & &1 - 1)}
end

def handle_event("reset", _, socket) do
{:noreply, assign(socket, :count, 0)}
end
end

Handling events in Surface

Instead of treating templates as plain text, Surface parses the code identifying its structure (the hierarchy of components) and uses that information to restore the initially desired behaviour of handling events in LiveView. Bear in mind that in order to keep the behaviour consistent and predictable across multiple components, you should:

  • always use the :on-[event] directive.
  • always declare event properties as :event

Note : You can still use Phoenix’s built-in phx-[event] directly if you want, however, if you need to pass that event as a property, you should declare that property as :string instead of :event .

The :on-[event] directive

Let’s rewrite our example again using Surface’s :on-phx-click directive:

0


defmodule Counter do
use Surface.LiveComponent

data count, :integer, default: 0

def render(assigns) do
~H"""
<div>
<h1 class="title">
{{ @count }}
</h1>
<div>
<button class="button is-info" :on-phx-click="dec">-</button>
<button class="button is-info" :on-phx-click="inc">+</button>
<button class="button is-danger" :on-phx-click="reset">Reset</button>
</div>
</div>
"""
end
# Event handlers
...
end

As you can see, we didn’t have to define phx-target for any of the buttons. Sweet!

Another great thing about Surface’s approach is that it makes passing events as properties also more intuitive. Using phoenix templates, unless you always pass both, the event and the target, you cannot be sure where the event will be handled. You need to know upfront if there’s a phx-target defined for that DOM element inside that component.

Using Surface, the event is always passed along with the related target, assuming, by default, that the target is the caller component/view. This should cover most of the cases you have to face when working with events. In the rare cases when you need to handle the event somewhere else, you can explicitly pass the target, e.g., click={{ "click", target: "#target_id" }} . If you want the target to be the parent LiveView, you can set the target option as :live_view .

Note : The complete list of available events, as well as other types of bindings, can be found at section Bindings in the Phoenix LiveView’s docs.