facett-form/examples/form_panel.rs
//! **form_panel** — the [`facett_form`] admin form mounted on the reusable
//! [`facett_wasm_demo`] host: one `FormView<Case>` panel painted as a **native
//! window** and a **wasm / WebGL canvas** from the same source, so it can ship on a
//! Codeberg-Pages site later (the harness recipe in `facett-demo/README.md`). The
//! panel's fixture is a surveillance "Case" record with per-field validation.
//!
//! - native: `cargo run -p facett-form --example form_panel`
//! - wasm: `cargo build -p facett-form --example form_panel --target wasm32-unknown-unknown`
//! then `wasm-bindgen … --target web` it and serve with the generated
//! `index.html` ([`facett_wasm_demo::IndexHtml`]).
use facett_form::{FormModel, FormUi, FormView, ValidationReport};
use facett_wasm_demo::FacetApp;
use serde::{Deserialize, Serialize};
/// The admin record; `garde` rules drive the per-field validation outlines.
#[derive(Clone, PartialEq, Serialize, Deserialize, garde::Validate)]
struct Case {
#[garde(length(min = 1))]
title: String,
#[garde(skip)]
status: String,
#[garde(length(min = 1))]
assignee: String,
#[garde(custom(facett_form::valid_ymd))]
due: String,
#[garde(range(min = 0, max = 5))]
priority: i64,
#[garde(skip)]
open: bool,
#[garde(skip)]
notes: String,
}
impl Default for Case {
fn default() -> Self {
Self {
title: "Investigate vessel Nordkapp".into(),
status: "open".into(),
assignee: "hugin".into(),
due: "2026-06-30".into(),
priority: 2,
open: true,
notes: "Cross-ref AIS gaps with port calls.".into(),
}
}
}
impl FormModel for Case {
fn fields(&mut self, f: &mut FormUi<'_>) {
f.section("Case", |f| {
f.text("title", "Title", &mut self.title);
f.select("status", "Status", &mut self.status, &["open", "closed", "escalated"]);
f.text("assignee", "Assignee", &mut self.assignee);
f.date("due", "Due (YYYY-MM-DD)", &mut self.due);
f.number_i64("priority", "Priority (0-5)", &mut self.priority, Some(0..=5));
f.checkbox("open", "Open", &mut self.open);
});
f.section("Notes", |f| {
f.multiline("notes", "Notes", &mut self.notes);
});
}
fn validate(&self) -> ValidationReport {
ValidationReport::from_garde(garde::Validate::validate(self))
}
fn to_clip(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_default()
}
fn from_clip(&mut self, text: &str) -> bool {
serde_json::from_str::<Case>(text).map(|m| *self = m).is_ok()
}
}
fn panel() -> FormView<Case> {
FormView::new("New Case", Case::default())
}
// ── native host ──────────────────────────────────────────────────────────────
#[cfg(not(target_arch = "wasm32"))]
fn main() -> eframe::Result<()> {
facett_wasm_demo::NativeDemo::new("facett-form — panel demo")
.with_inner_size([560.0, 680.0])
.run(move |_cc| FacetApp::boxed(Box::new(panel())))
}
// On wasm the entry point is `start` (below); `main` still has to exist to link.
#[cfg(target_arch = "wasm32")]
fn main() {}
// ── web host ─────────────────────────────────────────────────────────────────
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn start() {
facett_wasm_demo::WebDemo::new("demo_canvas").mount(move |_cc, _hash| FacetApp::boxed(Box::new(panel())));
}
/// The browser-readable state hook: `window.__form_state()` returns the form's live
/// `state_json` (dirty/valid/errors), so a headless drive verifies the shipped bundle.
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen::prelude::wasm_bindgen(js_name = __form_state)]
pub fn form_state() -> String {
facett_wasm_demo::latest_state()
}