This page explains the two assign authoring styles and when to use each one.
Both styles generate normal Phoenix calls at runtime. The choice is about Haxe authoring ergonomics:
- default macro selector style: least code to write
- typed-key style: explicit key tokens and stronger key/value coupling
| Goal | Haxe API | Phoenix runtime call |
|---|---|---|
| Shortest single-field update | assign(_.field, value) |
assign(socket, :field, value) |
| Phoenix-style multi-field update | assign({field_a: a, field_b: b}) |
assign(socket, %{field_a: a, field_b: b}) |
| Typed key-token style (optional) | assignKey(keys.field, value) |
assign(socket, :field, value) |
| Set default-if-missing | assignNew(_.field, fn) or assignNewKey(keys.field, fn) |
assign_new(socket, :field, fn -> ... end) |
| Update from previous value | update(_.field, fn) or updateKey(keys.field, fn) |
update(socket, :field, fn prev -> ... end) |
socket = socket.assign(_.count, 0);Why this exists:
- Haxe has no built-in typed field-reference literal for typedef fields.
_.fieldis a compact compile-time selector read by the assign macro.
What you get:
- compile-time field-name validation
- automatic
camelCase -> snake_caseatom conversion - idiomatic Phoenix
assign/3output
socket = socket.assign({
count: 0,
search_query: ""
});Why this exists:
- It maps directly to Phoenix
assign(socket, %{...}). - It keeps the API close to Phoenix
assign/2shape.
What you get:
- compile-time field-name validation for each object field
- snake_case atom-key rewrite in generated Elixir
import phoenix.AssignKeys;
typedef CounterAssigns = { count: Int };
var keys = AssignKeys.of(CounterAssigns);
socket = socket.assignKey(keys.count, 0);
socket = socket.updateKey(keys.count, (n) -> n + 1);Why this exists:
- Some teams want explicit key tokens in function signatures.
- It binds key and value types at the type-token level.
What you get:
- key-specific value typing (
keys.countenforcesInt) - optional better field completion from
keys.<field>
| Aspect | assign(_.field, value) / assign({...}) |
assignKey(keys.field, value) |
|---|---|---|
| Setup required | none | one var keys = AssignKeys.of(MyAssigns) |
| Code size | shortest | slightly more explicit |
| Runtime mapping | Phoenix-faithful | Phoenix-faithful |
| Key/value typing | field name validated; value typed by call context | explicit key token carries value type |
| Good default for most apps | yes | optional advanced mode |
Usually, no.
- If you use default assign APIs (
assign,assignNew,update, bulkassign({...})), you do not need@:build(...). - If you use typed keys, prefer
AssignKeys.of(MyAssigns)first. @:build(phoenix.macros.AssignKeysBuilder.build(MyAssigns))is still supported when you explicitly want a dedicated static key class.
Usually, no.
- In callbacks,
socket: Socket<TAssigns>can call assign helpers directly. LiveSocket<TAssigns>remains available as an explicit wrapper if you prefer wrapper-style helper signatures or pipe-oriented code.
assign is implemented as one macro entrypoint with an optional value:
assign(fieldOrUpdates, ?value)
Dispatch rules:
valueomitted + object literal -> validate fields + emit bulk assign mapvalueomitted + non-literal map value -> pass through tophoenix.Component.assign(socket, updates)valueprovided + first arg is_.field-> validate field + emit atom keyvalueprovided + first arg is not_.field-> pass through tophoenix.Component.assign(socket, key, value)
Source:
std/phoenix/SocketAssignExtensions.hxstd/phoenix/LiveSocket.hxstd/phoenix/macros/AssignMacro.hx
This API needs syntax-shape dispatch (_.field vs object literal), not only type-based overload selection. Macro dispatch is the right fit here.
@:overload is still used where it fits (for example phoenix.Component.assign extern signatures).
merge({...}) remains as a backward-compatible alias. Prefer assign({...}) for clearer Phoenix parity.
assignKey / assignNewKey / updateKey are macro-based too.
- When the key expression is field-shaped (
keys.count,MyKeys.count), the compiler emits a direct atom key (:count) for clean Phoenix output. - When the key is a computed token expression, the compiler passes that token expression through.
Typed-key mode has two generation options:
- preferred:
phoenix.AssignKeys.of(MyAssigns)
- optional static-class mode:
@:build(phoenix.macros.AssignKeysBuilder.build(MyAssigns))
Both use the same key normalization (camelCase -> snake_case) and produce AssignKey<TAssigns, TValue> tokens.
Source:
std/phoenix/AssignKeys.hxstd/phoenix/types/AssignKey.hxstd/phoenix/macros/AssignKeysBuilder.hxstd/phoenix/macros/AssignKeysSupport.hx
assign(_.missingField, value)-> compile-time error (unknown assigns field)assign({missing_field: value})-> compile-time error (unknown assigns field)assignKey(keys.count, "x")whencountisInt-> compile-time type errorassign(nonLiteralMap)-> forwarded to Phoenix assign/2 runtime call
docs/02-user-guide/haxe-for-phoenix.mddocs/07-patterns/PHOENIX_LIVEVIEW_PATTERNS.mddocs/04-api-reference/PHOENIX_API_REFERENCE.md