Packages
React inside Phoenix LiveView — async code splitting, client-side props diffing, and zero-config component discovery.
Retired package: Release invalid - Not production-ready
Current section
Files
Jump to
Current section
Files
lib/react_phx/encoder.ex
defmodule ReactPhx.Encoder.EncodeError do
@moduledoc "Raised when an un-derivable struct is passed to `ReactPhx.Encoder.encode/1`."
defexception [:message]
end
defprotocol ReactPhx.Encoder do
@moduledoc """
Protocol for encoding Elixir values before they are serialized to JSON for
React props. The default implementation for unknown structs raises
`ReactPhx.Encoder.EncodeError` to prevent accidentally leaking private data.
## Deriving
Structs can opt in by deriving the protocol and specifying either `:only`
(whitelist) or `:except` (blacklist) fields:
@derive {ReactPhx.Encoder, only: [:name, :age]}
defstruct [:name, :age, :secret]
@derive {ReactPhx.Encoder, except: [:secret]}
defstruct [:name, :age, :secret]
"""
@fallback_to_any true
@doc "Encode a value for use as a React prop."
def encode(value)
end
# ---------------------------------------------------------------------------
# Built-in implementations
# ---------------------------------------------------------------------------
defimpl ReactPhx.Encoder, for: Integer do
def encode(value), do: value
end
defimpl ReactPhx.Encoder, for: Float do
def encode(value), do: value
end
defimpl ReactPhx.Encoder, for: BitString do
def encode(value), do: value
end
defimpl ReactPhx.Encoder, for: Atom do
def encode(nil), do: nil
def encode(true), do: true
def encode(false), do: false
def encode(value), do: Atom.to_string(value)
end
defimpl ReactPhx.Encoder, for: List do
def encode(list), do: Enum.map(list, &ReactPhx.Encoder.encode/1)
end
defimpl ReactPhx.Encoder, for: Map do
def encode(map) do
Map.new(map, fn {k, v} -> {k, ReactPhx.Encoder.encode(v)} end)
end
end
defimpl ReactPhx.Encoder, for: Date do
def encode(date), do: Date.to_iso8601(date)
end
defimpl ReactPhx.Encoder, for: Time do
def encode(time), do: Time.to_iso8601(time)
end
defimpl ReactPhx.Encoder, for: NaiveDateTime do
def encode(ndt), do: NaiveDateTime.to_iso8601(ndt)
end
defimpl ReactPhx.Encoder, for: DateTime do
def encode(dt), do: DateTime.to_iso8601(dt)
end
# ---------------------------------------------------------------------------
# Phoenix.HTML.Form
# ---------------------------------------------------------------------------
defimpl ReactPhx.Encoder, for: Phoenix.HTML.Form do
def encode(form) do
%{
id: form.id,
name: form.name,
source: form.source && (Map.get(form.source, :__struct__) |> then(fn s -> s && to_string(s) end)),
action: form.action && ReactPhx.Encoder.encode(form.action),
params: ReactPhx.Encoder.encode(form.params || %{}),
errors: encode_errors(form.errors || []),
data: encode_data(form)
}
end
defp encode_errors(errors) do
errors
|> Enum.reduce(%{}, fn {field, {msg, _opts}}, acc ->
field_str = to_string(field)
Map.update(acc, field_str, [msg], &(&1 ++ [msg]))
end)
end
defp encode_data(form) do
case form.source do
%{data: %{__struct__: _} = data} ->
ReactPhx.Encoder.encode(Map.from_struct(data))
%{data: data} when is_map(data) ->
ReactPhx.Encoder.encode(data)
_ ->
%{}
end
end
end
# ---------------------------------------------------------------------------
# Phoenix.LiveView.UploadConfig / UploadEntry
# ---------------------------------------------------------------------------
if Code.ensure_loaded?(Phoenix.LiveView.UploadConfig) do
defimpl ReactPhx.Encoder, for: Phoenix.LiveView.UploadConfig do
def encode(config) do
%{
ref: config.ref,
name: ReactPhx.Encoder.encode(config.name),
accept: split_accept(config.accept),
max_entries: config.max_entries,
max_file_size: config.max_file_size,
entries: Enum.map(config.entries, &ReactPhx.Encoder.encode/1),
errors: ReactPhx.Encoder.encode(config.errors)
}
end
defp split_accept(accept) when is_binary(accept) do
accept |> String.split(",", trim: true) |> Enum.map(&String.trim/1)
end
defp split_accept(accept) when is_list(accept), do: accept
defp split_accept(:any), do: []
defp split_accept(_), do: []
end
end
if Code.ensure_loaded?(Phoenix.LiveView.UploadEntry) do
defimpl ReactPhx.Encoder, for: Phoenix.LiveView.UploadEntry do
def encode(entry) do
%{
ref: entry.ref,
upload_ref: entry.upload_ref,
name: entry.client_name,
size: entry.client_size,
progress: entry.progress,
valid: entry.valid?,
errors: []
}
end
end
end
# ---------------------------------------------------------------------------
# Any / struct fallback (with @derive support)
# ---------------------------------------------------------------------------
defimpl ReactPhx.Encoder, for: Any do
# Called by the Elixir protocol system when a module uses @derive.
defmacro __deriving__(module, _struct, opts) do
fields_ast =
quote do
struct_fields =
unquote(module).__struct__()
|> Map.keys()
|> Enum.reject(&(&1 == :__struct__))
case unquote(opts) do
[only: only] -> only
[except: except] -> struct_fields -- except
_ -> struct_fields
end
end
quote do
defimpl ReactPhx.Encoder, for: unquote(module) do
def encode(struct) do
fields = unquote(fields_ast)
fields
|> Enum.map(fn key -> {key, ReactPhx.Encoder.encode(Map.fetch!(struct, key))} end)
|> Map.new()
end
end
end
end
def encode(%{__struct__: mod} = value) do
raise ReactPhx.Encoder.EncodeError,
message:
"cannot encode struct #{inspect(mod)}. " <>
"Add `@derive {ReactPhx.Encoder, only: [...]}` to #{inspect(mod)} " <>
"or implement the ReactPhx.Encoder protocol for it. " <>
"Got: #{inspect(value)}"
end
def encode(value) do
raise ReactPhx.Encoder.EncodeError,
message: "cannot encode value #{inspect(value)}"
end
end