Packages
surface
0.12.1
0.12.3
0.12.2
0.12.1
0.12.0
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.1
0.1.0
0.1.0-alpha.2
0.1.0-alpha.1
0.1.0-alpha.0
A component based library for Phoenix LiveView
Current section
Files
Jump to
Current section
Files
lib/surface/components/context.ex
defmodule Surface.Components.Context do
@moduledoc """
> #### Deprecation warning {: .warning}
>
> Using this module as a component with `<Context>` has been deprecated. Support for
> scope-aware context will be removed in `v0.13` due to the lack of built-in support for
> the feature in Liveview itself, which leads to inefficient diff-tracking when using it.
>
> Global context related functions like `Context.put/3` and `Context.get/3`, as well as data/prop's
> option `:from_context`, will be kept and recommended as their usage don't affect diff-tracking
> negatively.
A built-in module and component that allows users to set and retrieve values
to/from the context.
"""
use Surface.Component
alias Surface.AST
alias Surface.IOHelper
alias Phoenix.LiveView.Socket
defguardp is_socket_or_assigns(value) when is_struct(value, Socket) or is_map_key(value, :__changed__)
@doc """
Puts a value into the context.
## Usage
```
<Context put={scope, values}>
...
</Context>
```
Where:
* `scope` - Optional. Is an atom representing the scope where the values will
be stored. If no scope is provided, the value is stored at the root of the
context map.
* `values`- A keyword list containing the key-value pairs that will be stored
in the context.
## Examples
With scope:
```
<Context put={__MODULE__, form: @form}>
...
</Context>
```
Without scope:
```
<Context put={key1: @value1, key2: "some other value"}>
...
</Context>
```
"""
prop put, :context_put, accumulate: true, default: []
@doc """
Retrieves a set of values from the context binding them to local variables.
## Usage
```
<Context get={scope, bindings}>
...
</Context>
```
Where:
* `scope` - Optional. Is an atom representing the scope where the values will
be stored. If no scope is provided, the value is stored at the root of the
context map.
* `bindings`- A keyword list of bindings that will be retrieved from the context
as local variables.
## Examples
```
<Context
get={Form, form: form}
get={Field, field: my_field}>
<MyTextInput form={form} field={my_field} />
</Context>
```
"""
prop get, :context_get, accumulate: true, default: []
@doc "The content of the `<Context>`"
slot default, required: true
@doc false
def transform(node) do
maybe_warn_on_context_put(node)
let =
node.props
|> Enum.filter(fn %{name: name} -> name == :get end)
|> Enum.map(fn %{value: %{value: value}} -> value end)
|> Enum.flat_map(fn {:__context_get__, scope, values} ->
if scope == nil do
values
else
Enum.map(values, fn {key, value} -> {{scope, key}, value} end)
end
end)
update_let_for_slot_entry(node, :default, let)
end
if Mix.env() != :test do
@deprecated "Use `Context.put/3`, `Context.get/3` or the data/prop's option `:from_context` instead"
end
@doc false
def render(assigns) do
~F"""
{render_slot(@default, context_map(@__context__, @put, @get))}
"""
end
@doc """
Puts a value into the context.
## Usage
Context.put(socket_or_assigns, scope, values)
Where:
* `socket_or_assigns` - The socket or assigns where the values will be stored.
* `scope` - Optional. Is an atom representing the scope where the values will
be stored. If no scope is provided, the value is stored at the root of the
context map.
* `values`- A keyword list containing the key-value pairs that will be stored
in the context.
## Examples
With scope:
Context.put(__MODULE__, form: @form)
Without scope:
Context.put(key1: @value1, key2: "some other value")
> **Note**: Whenever using `Context.put/3` inside the [`update/2`](`c:Phoenix.LiveComponent.update/2`) callback, make sure you call it passing the `socket`, not the `assigns`.
"""
def put(socket_or_assigns, scope \\ nil, values)
def put(socket_or_assigns, scope, values) when is_socket_or_assigns(socket_or_assigns) do
context =
socket_or_assigns
|> get_assigns_context()
|> put_values(scope, values)
Phoenix.Component.assign(socket_or_assigns, :__context__, context)
end
def put(socket_or_assigns, _scope, _values) do
raise ArgumentError, expects_socket_or_assigns_message("put/3", socket_or_assigns)
end
@doc """
Retrieves a value from the context.
## Usage
```
Context.get(socket_or_assigns, scope, key)
```
Where:
* `socket_or_assigns` - The socket or assigns where the values will be retrieved from.
* `scope` - Optional. Is an atom representing the scope where the values will
be stored. If no scope is provided, the value is stored at the root of the
context map.
* `key`- the key to look for when retrieving the value.
## Examples
form = Context.get(assigns, Form, :form)
field = Context.get(assigns, Field, :field)
...
~F"\""
<MyTextInput form={form} field={field} />
"\""
> **Note**: Whenever using `Context.get/3` inside the [`update/2`](`c:Phoenix.LiveComponent.update/2`) callback, make sure you call it passing the `socket`, not the `assigns`.
"""
def get(socket_or_assigns, scope \\ nil, key)
def get(socket_or_assigns, scope, key) when is_socket_or_assigns(socket_or_assigns) do
socket_or_assigns
|> get_assigns_context()
|> Map.get(normalize_key(scope, key))
end
def get(socket_or_assigns, _scope, _values) do
raise ArgumentError, expects_socket_or_assigns_message("get/3", socket_or_assigns)
end
@doc """
Copies a value from the context directly into `socket_or_assigns`.
## Usage
# Simple key
Context.copy_assign(socket_or_assigns, key)
# Simple key with options
Context.copy_assign(socket_or_assigns, key, opts)
# Key with scope
Context.copy_assign(socket_or_assigns, {scope, key})
# Key with scope + options
Context.copy_assign(socket_or_assigns, {scope, key}, opts)
## Options
* `:as` - Optional. The key to store the value. Default is the `key` without the scope.
"""
def copy_assign(socket_or_assigns, key, opts \\ []) do
{scope, key, to_key} = process_key(key, opts)
value = get(socket_or_assigns, scope, key)
Phoenix.Component.assign(socket_or_assigns, to_key, value)
end
@doc """
Copies a value from the context directly into `socket_or_assigns`
if the value hasn't already been set or if it's `nil`.
The value will be saved using the same `key`.
## Usage
Context.maybe_copy_assign(socket_or_assigns, scope, key)
"""
def maybe_copy_assign(socket_or_assigns, key, opts \\ []) do
{scope, key, to_key} = process_key(key, opts)
cond do
get_assigns(socket_or_assigns)[to_key] != nil ->
socket_or_assigns
value = get(socket_or_assigns, scope, key) ->
Phoenix.Component.assign(socket_or_assigns, to_key, value)
true ->
socket_or_assigns
end
end
@doc """
Copies a value from the context directly into `socket_or_assigns`.
It raises a runtime error in case the value is still `nil` after
after the operation. This is useful whenever you expect a non-nil
value coming either explicitly passed a `prop` or inplictly through
the context.
The value will be saved using the same `key`.
## Usage
Context.maybe_copy_assign!(socket_or_assigns, scope, key)
"""
def maybe_copy_assign!(socket_or_assigns, key, opts \\ []) do
socket_or_assigns = maybe_copy_assign(socket_or_assigns, key, opts)
{scope, key, to_key} = process_key(key, opts)
if get_assigns(socket_or_assigns)[to_key] != nil do
socket_or_assigns
else
scope_and_key = if scope, do: "#{inspect(scope)}, #{key}", else: key
message = """
expected assign #{inspect(key)} to have a value, got: `nil`.
If you're expecting a value from a prop, make sure you're passing it.
## Example
<YourComponent #{key}={...}>
If you expecting a value from the context, make sure you have used `Context.put/3` \
to store the value in a parent component.
## Example
Context.put(socket_or_assigns, #{scope_and_key}: ...)
If you expecting the value to come from a parent component's slot, make sure you add \
the parent component to the `:propagate_context_to_slots` list in your config.
## Example
config :surface, :propagate_context_to_slots, [
# For module components
ModuleComponentStoringTheValue,
# For function components
{FunctionComponentStoringTheValue, :func}
...
]
"""
raise message
end
end
defp expects_socket_or_assigns_message(fun, value) do
"#{fun} expects a socket or an assigns map from a function component as first argument, got: #{inspect(value)}"
end
defp get_assigns_context(socket_or_assigns) do
get_assigns(socket_or_assigns)[:__context__] || %{}
end
defp get_assigns(%Socket{} = socket) do
socket.assigns
end
defp get_assigns(assigns) do
assigns
end
defp context_map(context, puts, gets) do
updated_context =
for {scope, values} <- puts, reduce: %{} do
acc -> put_values(acc, scope, values)
end
full_context = Map.merge(context, updated_context)
props =
for {scope, values} <- gets, {key, _value} <- values, into: %{} do
key =
if scope == nil do
key
else
{scope, key}
end
{key, Map.get(full_context, key, nil)}
end
{props, nil, updated_context}
end
@doc false
def normalize_key(nil, key) do
key
end
def normalize_key(scope, key) do
{scope, key}
end
defp process_key(key, opts) do
{scope, key} =
case key do
{k, v} -> {k, v}
k -> {nil, k}
end
{scope, key, opts[:as] || key}
end
defp update_let_for_slot_entry(node, slot_entry_name, let) do
updated =
node.slot_entries
|> Map.get(slot_entry_name, [])
|> Enum.map(fn slot_entry ->
%{slot_entry | let: %AST.AttributeExpr{meta: node.meta, value: {:%{}, [], let}}}
end)
slot_entries = Map.put(node.slot_entries, slot_entry_name, updated)
Map.put(node, :slot_entries, slot_entries)
end
def has_vars?(ast) do
{_, value} =
Macro.prewalk(ast, false, fn
{:@, _meta, _args}, acc ->
{[], acc}
{name, _meta, _args}, acc when name in [:__MODULE__, :__ENV__, :__STACKTRACE__, :__DIR__] ->
{[], acc}
{name, _meta, ctx}, _acc when is_atom(name) and is_atom(ctx) ->
{[], true}
expr, acc ->
{expr, acc}
end)
value
end
defp put_values(context, scope, values) do
new_values =
for {key, value} <- values, reduce: %{} do
acc -> Map.put(acc, normalize_key(scope, key), value)
end
Map.merge(context, new_values)
end
defp maybe_warn_on_context_put(node) do
puts = Enum.filter(node.props, fn %{name: name} -> name == :put end)
for %AST.Attribute{value: %AST.AttributeExpr{value: {_scope, expr}, meta: meta, original: original}} <- puts do
unless Macro.quoted_literal?(expr) or has_vars?(expr) do
message = """
using <Context put=...> without depending on any variable has been deprecated.
If you're storing values in the context only to propagate them through slots, \
use the `context_put` property instead.
# Example
<#slot context_put={#{original}} ... />
If the values must be available to all other child components in the template, \
use `Context.put/3` instead.
# Example
socket_or_assigns = Context.put(socket_or_assigns, timezone: "UTC")
"""
IOHelper.warn(message, meta.caller, meta.file, meta.line)
end
end
end
end