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 each 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>

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

Note: Although storing values in 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>

Using <Context>

You can put or get values to/from the context using the Context component.

Putting values into the context

Let's take a look at our Form component.


defmodule Form do
use Surface.Component

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

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

def render(assigns) do
~H"""
{{ form = form_for(@for, "#",
phx_change: assigns.change.name,
autocomplete: assigns.autocomplete) }}
<Context put={{ form: form }}>
<slot/>
</Context>
<#Raw></form></#Raw>
"""
end
end

The value of variable form will be stored in the context under the key :form and will be available to any child component inside <Context>...</Context>, including any instance present in the content assigned to the "default" slot.

We can use the same concept in our Field component and add the field name to the context too:


defmodule Field do
use Surface.Component

import Phoenix.HTML.{Form, Tag}

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

def render(assigns) do
~H"""
<div class="field">
<Context get={{ form: form }}>
{{ label form, @name, class: "label" }}
<div class="control">
<Context put={{ field: String.to_atom(@name) }}>
<slot/>
</Context>
{{ error_tag form, @name }}
</div>
</Context>
</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

Retrieving values from the context

Now that we have both values, form and field properly stored in the context, the TextInput component can access those values and use them as need:


defmodule TextInput do
use Surface.Component

import Phoenix.HTML.Form

prop placeholder, :string

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

Scoping context values

One important thing to keep in mind it that storing values from different components might lead to naming conflicts. To avoid that, Surface allows you to "namespace" the values stored using an extra scope key.

The key is just an atom and can usually be the component's module. For instance:

Instead of:

<Context put={{ form: form }}>

you can use:

<Context put={{ __MODULE__, form: form }}>

That would create a composite key cointaining both atoms, i.e. {Form, :form} for that value.

Now, whenever you need to retrieve the value, you must pass the scope too:

<Context get={{ Form, form: f }}>

Note: If you want to distribute a library that store values into the context, it's highly recommended that you always scope those values as demonstrated. This way you make sure it can play nicely with other libraries that also use contexts.