VoidhashDocs
DevelopersHelp centerGuidesAPI reference
Dashboard
Get started
Introduction
Quickstart
How Voidhash works
Glossary
Versions and releases
Concepts
Teams, projects and environments
API keys
Products and perks
Offers
Subscription lifecycle
People and identity
Groups
Events
Features
Paywalls in your app
Feature flags and A/B tests
Paywall components
Push notifications
Ad attribution in your app
Client SDKs
React Native
Swift
Kotlin
Web
Your backend
Node.js
PHP
Go
Rust
Other languages
Payment sources
Forward store notifications
Developer tools
CLI
MCP server
Query analytics with VoidQL
Components
Develop a component
Editor panels
Panel handlers and state
Component definition
Component props
Publishing
Installing and capabilities

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.

CallRecords
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

  • Editor panels
  • Test a panel
  • Panel limits

Something wrong or missing on this page? Tell us by email or on Discord.

PreviousEditor panelsNextComponent definition

On this page

HandlersListsUI stateNext steps