Types for widget fields and events.
Widget field declarations like field :name, :string or
field :color, Plushie.Type.Color each name a type. The type
determines what values a field accepts, how they appear in
typespecs, and how they encode for the renderer.
The SDK includes types for common values; when your application
needs something more specific, you can define your own by
implementing the Plushie.Type behaviour.
Primitive types
Types for basic values like numbers, strings, and booleans, represented as atoms in field declarations:
:integer-Plushie.Type.Integer:float-Plushie.Type.Float:string-Plushie.Type.String:boolean-Plushie.Type.Boolean:atom-Plushie.Type.Atom:any-Plushie.Type.Any:map-Plushie.Type.Map
Example in a field declaration:
field :label, :string
field :count, :integer, default: 0Domain types
The SDK includes additional types beyond the primitives, referenced by module name. Some examples:
Plushie.Type.Color- CSS colors (named atoms, hex strings, RGBA maps)Plushie.Type.Padding- uniform number,{v, h}tuple, or per-side mapPlushie.Type.Font- font family, weight, style, and stretchPlushie.Type.Border- color, width, and radiusPlushie.Type.A11y- accessibility annotations
Example in a field declaration:
field :color, Plushie.Type.Color
field :padding, Plushie.Type.Padding, default: 8Composite types
When you need a list of values, a set of specific atoms, or a choice between types, you can express that directly in the field declaration:
{:enum, [:a, :b, :c]}- one of a fixed set of atoms{:list, :string}- list where every element matches the inner type{:map, {:string, :integer}}- map with typed keys and values{:map, [name: :string, age: :integer]}- map with specific named fields{:tuple, [:float, :float]}- fixed-size tuple with typed positions{:union, [Plushie.Type.Color, Plushie.Type.StyleMap]}- tries each type in order, first successful cast wins
Example in a field declaration:
field :tags, {:list, :string}
field :mode, {:enum, [:read, :write]}
field :scores, {:map, {:string, :integer}}Building your own type
If the built-in types and composite types don't quite fit,
you can define your own. All custom types start with
use Plushie.Type. From there, you can either compose existing
types using the DSL macros, or implement callbacks directly for
more control.
Composing with the DSL
The DSL macros let you build types from existing ones. All callbacks are generated automatically.
Enum for a fixed set of atom values:
defmodule MyApp.Type.Priority do
use Plushie.Type
enum [:low, :medium, :high, :critical]
endStruct for grouping related fields into a single value:
defmodule MyApp.Type.Dimensions do
use Plushie.Type
struct do
field :width, :float
field :height, :float
end
endUnion when a field accepts multiple forms (first match wins):
defmodule MyApp.Type.Size do
use Plushie.Type
union do
enum [:small, :medium, :large]
type MyApp.Type.Dimensions
end
endUse them in widget field declarations by module name:
field :priority, MyApp.Type.Priority, default: :medium
field :size, MyApp.Type.SizeCustom callbacks
When you need custom validation, coercion from multiple input
forms, or encoding logic that the DSL can't express, you can
implement the callbacks directly. You still start with
use Plushie.Type.
A type needs at least cast/1 and typespec/0. Adding guard/1
enables pattern matching in generated setters, and encode/1
handles cases where the wire representation differs from the
Elixir value:
defmodule MyApp.Type.Percentage do
use Plushie.Type
# Accept integers 0..100 or floats 0.0..1.0, normalize to float
@impl Plushie.Type
def cast(v) when is_integer(v) and v >= 0 and v <= 100, do: {:ok, v / 100}
def cast(v) when is_float(v) and v >= 0.0 and v <= 1.0, do: {:ok, v}
def cast(_), do: :error
@impl Plushie.Type
def typespec, do: quote(do: float())
@impl Plushie.Type
def guard(var), do: quote(do: is_float(unquote(var)))
# encode/1 not needed: floats are already wire-safe
endYou can also delegate to other types within your callbacks. This is useful when your type wraps or combines existing types with custom logic:
def cast(%{fg: fg, bg: bg}) do
with {:ok, fg} <- Plushie.Type.Color.cast(fg),
{:ok, bg} <- Plushie.Type.Color.cast(bg) do
{:ok, %{fg: fg, bg: bg}}
else
_ -> :error
end
endPlushie.Type.Integer serves as a minimal reference.
Plushie.Type.Color shows how to handle many input forms.
Callbacks
Required:
cast/1- validates input, returns{:ok, normalized}or:error. Defines what values are accepted and what canonical form they take.typespec/0- returns a quoted typespec (e.g.quote(do: float())). Used in generated@typeand@specattributes.
Optional:
castable/0- returns a quoted typespec for the valuescast/1accepts. Default: same astypespec/0. Implement when the input forms are broader than the canonical type (e.g., Color accepts atoms, strings, and maps but stores a hex string).decode/1- decodes a wire-format value (JSON-decoded strings, numbers, maps with string keys) into the canonical type. Default: delegates tocast/1. Implement when wire and Elixir representations differ (e.g., enums receive strings but store atoms).encode/1- converts to wire-safe form (atoms to strings, structs to maps). Only needed when the Elixir value isn't directly serializable.guard/1- returns a quoted guard expression. Enables pattern matching in generated setter functions.fields/0- for struct types, returns[{name, type}, ...].field_options/0- declares constraint keys (e.g.[:min, :max]). Validated at compile time.constrain_guard/2- generates guards from field constraints (e.g. range checks from:min/:maxoptions).merge/2- controls how a field default combines with a user override. Default: full replacement. Implement for struct types where partial updates should preserve unset fields.resolve/2- derives the final value from sibling widget props at render time. Default: identity. Implement when your type needs to read other props to compute its value.
Summary
Callbacks
Returns the quoted typespec for values accepted by cast/1.
Decodes a wire-format value into the canonical type.
Merges a default value with a user-provided override.
Resolves derived values after all widget props are set.
Functions
Casts a value according to a composite type constructor.
Casts a value according to a type identifier.
Casts a map or keyword list against a list of {name, type} field specs.
Casts a named field value, treating nil as a valid absent value.
Universal cast entry point that handles both module types and composite types.
Returns true if the given atom is a known composite kind.
Returns the composite module for the given kind atom.
Decodes a wire-format event field value.
Decodes a map against a list of {name, type} field specs using
the decode path. Like cast_named_fields/2 but uses decode_value
for inner types (handling wire representations).
Decodes a named field value from wire format, treating nil as absent.
Decodes a wire-format value through the type's decode path.
Encodes a value to its wire-safe representation.
Merges a default value with a user override using the type's merge semantics.
Returns the map of primitive atom shortcuts to their type modules.
Resolves a type reference to a module or composite descriptor.
Resolves derived values using the type's resolve semantics.
Returns true if the given atom is a known primitive type shortcut.
Returns a human-readable type string for documentation.
Returns true if the given type identifier is valid for event fields.
Callbacks
@callback castable() :: Macro.t()
Returns the quoted typespec for values accepted by cast/1.
For types that normalize input (e.g., Color accepts atoms, strings,
and maps but stores a hex string), this describes the broader input
surface. Widget setter @spec annotations use this so dialyzer
and documentation reflect what users can actually pass.
Defaults to typespec/0 when not implemented, which is correct
for types where the input and canonical forms are the same.
Decodes a wire-format value into the canonical type.
Wire data arrives as JSON-decoded values (strings, numbers, booleans, lists, maps with string keys). This callback handles coercion from those wire representations to the canonical Elixir type.
Defaults to cast/1 when not implemented, which is correct for
types where wire and Elixir representations are the same
(integers, floats, strings, booleans).
Types where wire and Elixir differ should implement this. For example, enums receive strings from the wire but store atoms.
@callback field_options() :: [atom()]
Merges a default value with a user-provided override.
When a widget field has a default value and the user also provides a value, this callback controls how the two combine. The default implementation replaces the default entirely with the override, which is correct for most types.
Struct types with many optional fields can implement field-level
merge instead. Consider a %Config{theme: nil, locale: nil} type.
A widget declares default: %Config{theme: :dark} and the user
sets config: %Config{locale: :en}. A field-level merge produces
%Config{theme: :dark, locale: :en} rather than discarding the
default theme.
Implement this callback when your type is a struct and partial
overrides should preserve unset defaults. See Plushie.Type.A11y
for a real-world example.
Resolves derived values after all widget props are set.
Called during tree normalization with the current field value and the full props map of the widget. This lets a type compute its final value based on sibling props that aren't known at declaration time.
For example, imagine a type with a derive_from field that names
another prop. A widget declares field :tooltip, MyType, default: %MyType{derive_from: :label}. When resolve/2 runs, it looks up
the :label prop from the props map and uses it as the tooltip
value. The derive_from directive is cleared since it only exists
to guide resolution, not to appear on the wire.
Most types don't need this. Implement it only when your type must
read other widget props to compute its final value. See
Plushie.Type.A11y for a real-world example.
@callback typespec() :: Macro.t()
Functions
Casts a value according to a composite type constructor.
Dispatches to the appropriate Plushie.Type.Composite module
based on the composite kind.
Casts a value according to a type identifier.
Resolves the type via resolve/1 and delegates to the type module's
cast/1. The :string type accepts nil (wire-format absent fields).
Returns {:ok, value} on success, :error on failure.
Casts a map or keyword list against a list of {name, type} field specs.
Looks up each field by atom key first, then by string key as a
fallback. Casts through the field's type and returns {:ok, map}
or :error. Missing fields become nil.
Casts a named field value, treating nil as a valid absent value.
Used by map record composites and struct type cast.
Universal cast entry point that handles both module types and composite types.
Resolves the type via resolve/1 and delegates to either
cast_composite/2 or the type module's cast/1.
Returns true if the given atom is a known composite kind.
Returns the composite module for the given kind atom.
Decodes a wire-format event field value.
Like cast_field/2 but uses the type's decode/1 callback,
which handles wire representations (strings for enums, lists for
tuples, etc.).
Decodes a map against a list of {name, type} field specs using
the decode path. Like cast_named_fields/2 but uses decode_value
for inner types (handling wire representations).
Decodes a named field value from wire format, treating nil as absent.
Decodes a wire-format value through the type's decode path.
Like cast_value/2 but uses decode/1 for module types and
decode/2 for composite types.
Encodes a value to its wire-safe representation.
Handles primitives (atoms become strings, tuples become lists) and
structs (delegates to module.encode/1 when available, otherwise
strips __struct__ and recursively encodes the map).
Merges a default value with a user override using the type's merge semantics.
Falls back to simple replacement if the type module doesn't implement merge/2.
Returns the map of primitive atom shortcuts to their type modules.
Resolves a type reference to a module or composite descriptor.
Accepts primitive atom shortcuts (:integer, :string, etc.),
module names, and composite tuple forms ({:enum, [...]},
{:list, type}, {:map, spec}, {:tuple, [types]},
{:union, [types]}).
Resolves derived values using the type's resolve semantics.
Falls back to identity if the type module doesn't implement resolve/2.
Returns true if the given atom is a known primitive type shortcut.
Returns a human-readable type string for documentation.
Returns true if the given type identifier is valid for event fields.
Valid types are primitive shortcuts, modules implementing Plushie.Type
(with cast/1), or composite type tuples.