The Elm architecture — guide
Class:
guide· audience: a techie writing or driving a facett component. The what is inelm-design.md; the why inelm-reasoning.md. This file is the workflow.
f0 — zero to a tested component (the whole workflow)
Four moves turn a struct into a harness-testable facett component:
- Name the state. One struct = your
Model:#[derive(Serialize, Deserialize, Clone, PartialEq)]. Everything you'd assert on goes here; caches/animation do not (put those inCell/RefCellfields on the view, not inModel). - Name the inputs. One
enum Msg: Clone + Debug. Every click, key, drag = a variant. impl Elm.update(&mut self, Msg)is your only mutation;view(&self, ui)paints and returns theMsgs the frame produced. No I/O inupdate— returnEffectdata (or useenum Effect {}if there is none).- One macro line.
facett_core::impl_facet_via_elm!(MyView);— now it's aFacet.
Then it is automatically testable headless (see f3). That's the payoff: no window in the test.
f1 — filling the trait
struct MyView { state: MyModel, /* + non-Model caches: */ layout: RefCell<Option<Layout>> }
impl facett_core::Elm for MyView {
type Model = MyModel;
type Msg = MyMsg;
type Effect = (); // or a real enum; or `enum NoFx {}` for "pure"
fn title(&self) -> &str { "my view" }
fn state(&self) -> &MyModel { &self.state }
fn update(&mut self, m: MyMsg) -> Vec<()> {
match m { MyMsg::Pick(id) => self.state.selected = Some(id), /* … */ }
vec![]
}
fn view(&self, ui: &mut egui::Ui) -> Vec<MyMsg> {
let mut out = vec![];
if ui.button("pick").clicked() { out.push(MyMsg::Pick(some_id)); }
out // never mutate self here
}
}
Rule of thumb: if you're about to write &mut self anywhere but update, stop — that state change is a Msg.
f2 — choosing the macro form
| you need… | use |
just a Facet from your Elm |
impl_facet_via_elm!(MyView) |
that plus extra Facet methods (but keep default state_json) |
impl_facet_via_elm!(MyView, { fn foo(&self){…} }) |
a richer state_json than serde(state()) |
impl_facet_via_elm!(MyView, custom_state_json, { fn state_json(&self)->Value {…} }) |
Most components use form 1 or the custom_state_json form (a fuller introspection json).
f3 — driving it in a test (the whole point)
use facett_core::harness;
let mut v = MyView::new();
// Apply a Msg script and inspect the resulting Model — NO window, NO GPU:
let model = harness::snapshot(&mut v, [MyMsg::Pick(id_a), MyMsg::Pick(id_b)]);
assert_eq!(model.selected, Some(id_b));
// Golden-file style:
let json = harness::snapshot_json(&mut v, [MyMsg::Pick(id_a)]);
// Assert it actually paints (offscreen), for the render-report path:
assert!(harness::headless_render(&mut v).drew());
drive returns the Effects so you can assert "this Msg requested effect X" without X happening. This is the same seam nornir's test-matrix and robot functions consume at scale — see nornir test-function design.
Look & feel
A component inherits facett's look automatically: paint through the egui::Ui you're given and use facett_core::look/Theme (the harness has render_themed to snapshot a component under a specific Theme). Don't hard-code colours; pull from the theme so every component reads as one product.