Components Basics

In order to create a new component you need to define a module and use one of the available component types:

  • Surface.Component - A stateless component.
  • Surface.LiveComponent - A live stateful component.
  • Surface.LiveView - A wrapper component around Phoenix.LiveView .
  • Surface.MacroComponent - A low-level component which is responsible for translating its own content at compile time.

Components instances can be injected in a template using the same notation as any other HTML tag.

Hello, I'm a component!

# Defining the component

defmodule Hello do
use Surface.Component

def render(assigns) do
~H"""
Hello, I'm a component!
"""
end
end

# Using the component

defmodule Example do
use Surface.Component

def render(assigns) do
~H"""
<Hello />
"""
end
end

The component API

Surface provides built-in functions that should be used to declare the essential building blocks of any component:

  • property - Defines a property for the component.

  • data - Defines a data assign for a stateful LiveComponent or LiveView . The set of all data assigns represents the state of the component/view.

  • slot - Defines a placeholder ( slot ) that can be filled up with custom content .

  • context set - Defines a context assign that should be saved in the component’s context and can be retrieved later by any of its child components.

  • context get - Defines a context assign to be retrieved from the parent’s context and made available locally.

All values declared using any of the above functions will be merged into the components assigns and will be available inside the template using the @ prefix.

Having everything explicitly declared brings a lot of benefits since all information provided can be used later for introspection allowing Surface to provide:

  • Syntactic sugar on attributes definition - e.g. CSS style classes.
  • Improved API for events - automatically setting phx-target .
  • Compile-time checking - validations of required properties, incompatible slots, etc.
  • Integration with editor/tools - for warnings/errors, syntax highlighting, jump-to-definition, auto-completion and more.
  • Docs generation - see example below.

Let’s create a Button component and see how we can use Surface’s API to provide useful information to the user.


defmodule Button do
use Surface.Component

@doc "The type (color) of the button"
property type, :string, values: ["primary", "success", "info"]

@doc "The Button is expanded (full-width)"
property expanded, :boolean, default: false

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

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

@doc "The content of the button"
slot default, required: true
...
end

The public API of the Button above can be automatically generated, including all information about properties , slots and events , divided by group in each individual tab as follows:

Name Description Type Values Default
type The type (color) of the button.

:string primary, success, info
expanded The Button is expanded (full-width).

:boolean false

Note : Information about exposed context assigns as well as any public function can also be introspected when defined.

Public vs private

Using property , slot or context set defines the public API of the component as their values are either initialized outside the component or are exposed to other components. Assigns declared as data or context get are considered private since they can only be accessed inside the component’s scope.

It’s important to keep that distinction in mind when designing a new component. Remember that users need to be able to easily identify the public interface so they can properly interact with the component. The recommendation is to have everything explicitly declared and well documented using Surface’s component API.

Directives

Directives are built-in attributes that can modify the translated code of a component at compile time. Currently, the following directives are supported:

  • :for - Iterates over a list (generator) and renders the content of the tag (or component) for each item in the list.

  • :if - Conditionally render a tag (or component). The code will be rendered if the expression is evaluated to a truthy value.

  • :show - Conditionally shows/hides an HTML tag, keeping the rendered element in the DOM even when the value is false .

  • :let - Declares which slot props will be used in the current scope.

  • :props - Used to pass information from the slot’s scope to the associated content that is being prepared to fill the slot. For more information see section “ Slots props “ in the Slots documentation.

  • :on-[event] - Sets a phx event binding defining the component itself as the default handler (target). This is the prefered way to use phx events in Surface as it can properly handle properties of type :event . Available directives are: :on-phx-click , :on-phx-blur , :on-phx-focus , :on-phx-change , :on-phx-submit , :on-phx-keydown and :on-phx-keyup .

Here’s an example using the :for directive:


<ul>
<li :for={{ item <- @items }}>
{{ item.name }}
</li>
</ul>

The :for directive will be responsible for injecting the necessary code to iterate over the list of items rendering each item’s name.

Interpolation

In the example above, the {{ item.name }} instructs the translator to inject the expression inside {{ }} into the generated code. Any valid expression is accepted.

Important note : Pay attention that just like React, Surface does not allow incomplete expressions to be interpolated in the template. So something like:


<!-- DON'T DO THIS!!! -->
<div>
{{ if @condition do }}
<span>It's true!</span>
{{ else }}
<span>It's false!</span>
{{ end }}
<div>

it’s not accepted and will throw a compile error . If you find yourself in a situation where you need to write this kind of code, try to create assigns/variables that represent the final state and use directives like :if or :show instead. You can also try to move the logic to a separate function. Having this type of conditionals inside your templates tends to pollute the code a lot and it’s, in general, not recommended.