Copy‑paste patterns aligned with Reflaxe.Elixir v1.0+ and the repo’s No‑Dynamic policy.
When Phoenix/Ecto hand you “raw params” maps, model them as typed
typedefs (preferred) or useelixir.types.Termas the explicit boundary type (neverDynamic).
package live;
import elixir.types.Term;
import phoenix.Phoenix.HandleEventResult;
import phoenix.Phoenix.MountResult;
import phoenix.Phoenix.Socket;
typedef Product = {
var id: Int;
var title: String;
}
typedef ProductAssigns = {
var products: Array<Product>;
var query: String;
var selectedId: Null<Int>;
}
typedef ProductEventParams = {
var ?query: String;
var ?id: Int;
}
@:native("MyAppWeb.ProductLive")
@:liveview
class ProductLive {
public static function mount(params: Term, session: Term, socket: Socket<ProductAssigns>): MountResult<ProductAssigns> {
socket = socket.assign({
products: [],
query: "",
selectedId: null
});
return Ok(socket);
}
@:native("handle_event")
public static function handle_event(event: String, params: ProductEventParams, socket: Socket<ProductAssigns>): HandleEventResult<ProductAssigns> {
return switch (event) {
case "search":
var query = params.query != null ? params.query : "";
NoReply(socket.assign({query: query}));
case "select":
NoReply(socket.assign({selectedId: params.id}));
case _:
NoReply(socket);
};
}
}Compiles to:
defmodule ProductLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, %{products: [], query: "", selected_id: nil})}
end
def handle_event("search", params, socket) do
query = Map.get(params, "query", "")
{:noreply, assign(socket, %{query: query})}
end
def handle_event("select", params, socket) do
{:noreply, assign(socket, %{selected_id: Map.get(params, "id")})}
end
def handle_event(_, _params, socket), do: {:noreply, socket}
endNotes
- Use
Socket<TAssigns>in callback signatures (what Phoenix expects). - Use socket helpers directly (
socket.assign(...),socket.update(...)).LiveSocket<TAssigns>is optional for wrapper-style helper APIs. - Keep params typed (
typedef ProductEventParams) so you can doparams.querywithout reflection. - If
params/sessionare unused in Haxe, generated Elixir still emits_params/_session.
Use named @:changeset(cast([...]), validate([...])) for the common case (cast + validate_required).
package schemas;
import ecto.Changeset;
import elixir.types.Term;
typedef UserParams = {
?email: String,
?name: String,
?role: String,
?active: Bool
}
/**
* If you need to accept “raw” form maps, keep a broader param type around.
* Prefer a `typedef` with optional fields and only use `Term` where truly polymorphic.
*/
typedef UserChangesetParams = {
?email: String,
?name: String,
?role: String,
?active: Bool
}
@:native("MyApp.User")
@:schema("users")
@:timestamps
@:changeset(cast(["email", "name", "role", "active"]), validate(["email", "name"]))
class User {
@:field @:primary_key public var id: Int;
@:field public var email: String;
@:field public var name: String;
@:field public var role: String;
@:field public var active: Bool = true;
}No manual extern is required here: @:schema auto-injects a typed
changeset<Params>(schema, params): Changeset<Schema, Params> declaration for Haxe calls.
An explicit extern declaration is still accepted as an optional compatibility path.
Legacy positional form remains supported: @:changeset(["email", ...], ["email", ...]).
Compiles to:
defmodule MyApp.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :name, :string
field :role, :string
field :active, :boolean, default: true
timestamps()
end
def changeset(user, params) do
user
|> cast(params, [:email, :name, :role, :active])
|> validate_required([:email, :name])
end
endpackage infrastructure;
import ecto.Changeset;
import ecto.DatabaseAdapter.*;
import ecto.Query.EctoQuery;
import haxe.functional.Result;
@:native("MyApp.Repo")
@:repo({
adapter: Postgres,
json: Jason,
extensions: [],
poolSize: 10
})
extern class Repo {
@:overload(function<T>(query: EctoQuery<T>): Array<T> {})
public static function all<T>(queryable: Class<T>): Array<T>;
public static function get<T>(queryable: Class<T>, id: Int): Null<T>;
public static function insert<T, P>(changeset: Changeset<T, P>): Result<T, Changeset<T, P>>;
public static function update<T, P>(changeset: Changeset<T, P>): Result<T, Changeset<T, P>>;
public static function delete<T>(struct: T): Result<T, Changeset<T, {}>>;
}import ecto.Query;
using reflaxe.elixir.macros.TypedQueryLambda;
var pattern = "%" + query + "%";
var q = from(u in User)
.where(ilike(u.email, ^pattern))
.orderBy(u -> u.name, :asc);
var users = Repo.all(q);Compiles to:
pattern = "%" <> query <> "%"
q =
from u in User,
where: ilike(u.email, ^pattern),
order_by: [asc: u.name]
users = Repo.all(q) import HXX.*;
import phoenix.types.Assigns;
typedef PageAssigns = {
title: String
}
class Page {
public static function render(assigns: Assigns<PageAssigns>): String {
return hxx('<h1>${assigns.title}</h1>');
}
}- Gradual adoption into an existing Phoenix app:
docs/06-guides/PHOENIX_GRADUAL_ADOPTION.md - Todo‑app (end‑to‑end reference):
examples/todo-app/README.md - Example index:
examples/README.md