Contexts

Sometimes you need to initialize some kind of context before using a component. For instance, when working with forms in Phoenix templates, you usually need to define a form variable that can be passed to form elements along with the field name. Here’s an example:

can't be blank

{
  "email": null,
  "name": "John Doe"
}

<%= form = form_for @changeset, "#", phx_change: :validate, autocomplete: "off" %>
<div class="field">
<%= label form, :name, class: "label" %>
<div class="control">
<%= text_input form, :name, class: input_class(form, :name) %>
<%= error_tag form, :name %>
</div>
</div>
<div class="field">
<%= label form, :email, class: "label" %>
<div class="control">
<%= text_input form, :email, class: input_class(form, :email), placeholder: "Try me!" %>
<%= error_tag form, :email %>
</div>
</div>
</form>

When using contexts, you can improve the developer experience by not forcing one to pass the form and field values multiple times. Instead, you can save those values in the context and retrieve them in the child component when needed.

Note : Although setting values into contexts might be an interesting way to avoid “Property drilling” , you must use them carefully. Overusing them will make your code less explicit, which may lead to components that are harder to reason about.

Here’s the updated version of our form now using components and contexts:

can't be blank

{
  "email": null,
  "name": "John Doe"
}

<Form for={{ @changeset }} change="validate" autocomplete="off">
<Field field="name">
<TextInput/>
</Field>
<Field field="email">
<TextInput placeholder="Try me!"/>
</Field>
</Form>

Contexts assigns are initialized using the init_context/1 callback and cleaned up automatically at the end of the component’s scope. Let’s take a look at our Form component to see how it works:


defmodule Form do
use Surface.Component

import Phoenix.HTML.Form
alias Surface.Components.Raw

property for, :any, required: true
property change, :event
property autocomplete, :string, values: ["on", "off"]

@doc """
The Form struct defined by the parent <Form/> component.
"""
context set form, :form

def init_context(assigns) do
opts = [phx_change: assigns.change.name, autocomplete: assigns.autocomplete]
form = form_for(assigns.for, "#", opts)
{:ok, form: form}
end

def render(assigns) do
~H"""
{{ @form }}
{{ @inner_content.([]) }}
<#Raw></form></#Raw>
"""
end
end

Now, we use the same concept in our Field component and add the field name to the context:


defmodule Field do
use Surface.Component

import Phoenix.HTML.{Form, Tag}

@doc "The field name"
property field, :string, required: true

@doc "The field name specified by the parent <Field/> component"
context set field, :atom, scope: :only_children

context get form, from: Form

def init_context(assigns) do
{:ok, field: String.to_atom(assigns.field)}
end

def render(assigns) do
~H"""
<div class="field">
{{ label @form, @field, class: "label" }}
<div class="control">
{{ @inner_content.([]) }}
{{ error_tag @form, @field }}
</div>
</div>
"""
end

defp error_tag(form, field) do
Enum.map(Keyword.get_values(form.errors, String.to_atom(field)), fn {error, _} ->
content_tag(:p, error, class: "help is-danger")
end)
end
end

And finally, the TextInput component retrieves both, the form and field from the context:


defmodule TextInput do
use Surface.Component

import Phoenix.HTML.Form

property placeholder, :string

context get form, from: Form
context get field, from: Field

def render(assigns) do
~H"""
{{ text_input(@form, @field,
class: css_class(["input", isDanger: Keyword.has_key?(@form.errors, @field)]),
placeholder: @placeholder
) }}
"""
end
end