Panel handlers and state
Change a paywall from an editor panel's buttons, lists and unbound controls, and keep state such as an open tab.
A bound field edits its value without calling your editor panel. Buttons, lists and other unbound controls change the paywall through handlers instead.
Handlers
Unbound controls take handlers, such as .on_click(…), .on_change(…) or
.on_commit(…). A handler never edits the paywall directly. It records what should change, and
the editor checks each change against the panel's limits and applies it.
| Call | Records |
|---|---|
cx.set_prop(name, value) | A new value for a prop of the selected instances. |
cx.reset_prop(name) | The prop's default. |
cx.bind_prop(name, variable) | A binding of the prop to a variable. |
cx.set_style(key, value) | A style value, for a key the panel declared. |
cx.reset_style(&[keys]) | Removes a state's override of those keys. |
cx.transaction(label, |intents| …) | Several changes as one undo step named label. |
cx.in_draft(…), cx.commit_draft_as(label) | A live preview that lands as one undo step. |
cx.toast_error(message) | An error toast. |
A button that resets two props in one undo step:
button("Reset rows").variant("ghost").on_click(|cx, ()| {
cx.transaction("Reset rows", |intents| {
intents.reset_prop("rowPadding");
intents.reset_prop("rowRadius");
});
})Lists
A list_editor shows a list prop with add and remove buttons; each child is one item. Its
handlers write the whole list:
fn features(cx: &PanelCx) -> Node {
let field = cx.prop("features");
let list: Vec<Value> = field.value().items().to_vec();
let add = {
let list = list.clone();
move |cx: &mut EventCx, ()| {
let mut next = list.clone();
next.push(Value::from("New feature"));
cx.transaction("Add feature", |intents| {
intents.set_prop("features", next);
});
}
};
let remove = {
let list = list.clone();
move |cx: &mut EventCx, index: f64| {
let mut next = list.clone();
if (index as usize) < next.len() {
next.remove(index as usize);
cx.transaction("Remove feature", |intents| {
intents.set_prop("features", next);
});
}
}
};
let items = list.iter().enumerate().map(|(index, item)| {
let list = list.clone();
text_field()
.value(item.as_str().unwrap_or(""))
.on_commit(move |cx, text: String| {
let mut next = list.clone();
next[index] = Value::from(text);
cx.transaction("Edit feature", |intents| {
intents.set_prop("features", next);
});
})
});
list_editor("Features")
.add_label("Add feature")
.can_add(!field.mixed() && list.len() < 6)
.mixed(field.mixed())
.on_add(add)
.on_remove(remove)
.children(items)
.into()
}UI state
Some state belongs to the panel rather than the paywall, such as the open tab.
cx.ui_state(|| initial) keeps a value while the panel is shown. Read it with .get() and change
it with .set(…); the panel renders again right away.
#[derive(Clone, Copy, PartialEq)]
enum Tab {
Content,
Style,
}
#[panel(for = "project/feature-list", title = "Feature list", order = 13, multi_selectable)]
pub fn settings(cx: &mut PanelCx) -> Panel {
let tab = cx.ui_state(|| Tab::Content);
let tabs = toggle_group()
.width("full")
.options([opt("content", "Content"), opt("style", "Style")])
.value(if tab.get() == Tab::Content { "content" } else { "style" })
.on_change(move |_cx, value: String| {
tab.set(if value == "style" { Tab::Style } else { Tab::Content });
});
let body: Vec<Node> = match tab.get() {
Tab::Content => content(cx),
Tab::Style => style(cx),
};
panel().child(section("Feature list").child(tabs).children(body))
}Here content and style are functions of your own that return each tab's fields as a
Vec<Node>. Call ui_state on every render, in the same order, as you would a React hook.
Next steps
Something wrong or missing on this page? Tell us by email or on Discord.