Demos /

Editing, permissions & proposals

Real module, child, row and approval controls. Changes belong only to this browser session.

Your browser-session sandbox is temporary, not published, and expires after 30 idle minutes. Simulated personas and OWNER/USER modes are not authentication. Editing: 64 rows, three modules per row. Forum: 128 topics, 512 comments. At most 32 visitor sandboxes; no rate-protection or production-readiness guarantee.

Live editing sandbox

EditableModule wraps each live module below. OWNER saves directly; USER proposes content for explicit approval. Nested child edits replace prior proposals; positional child IDs describe current visible items, not permanent identities.

EditableModule composition
EditableModule.wrap(displayModule)
    .withEditUrl("/editing-demo/edit/" + module.id)
    .withDeleteUrl("/editing-demo/delete/" + module.id)
    .withDeleteTarget("#page-content").withDeleteSwap("none")
    .withCanEdit(module.canEdit).withCanDelete(module.canDelete)
    .withEditMode(module.editMode);
Temporary browser-session sandbox, not published. Flags and selectable OWNER/USER modes are simulations, not authentication. Approval is a sandbox action. Same-origin and CSRF checks are enforced; applications still own authentication, authorization, storage and resource policy.
Try editing modules, adding modules, inserting rows, and approving pending edits.

Welcome to the Editing Demo

Request-owned editing

This is your temporary browser-session editing sandbox.

  • Single modal container
  • OOB swaps for save/delete
  • Row insert opens add-module modal

Edit this module to see changes apply immediately.

Approval Workflow (User Edit)

USER_EDIT Mode

Edits to this module are staged for approval.

Open the Pending Edits modal to approve or reject.

Fully Locked Module

Locked

No edit or delete buttons appear for this module.

Edit-Only Module

Edit Only

Can be edited but not deleted.

Delete-Only Module

Delete Only

Can be deleted but not edited.

Row Locked (No Add Module)

Row Locking

This row disables the add-module control.

Modules can still be edited or deleted as allowed.

Width Controls

Resize Me

Use the width dropdown in the edit modal to change this column.

Markdown Toggle

This module currently renders plain text. Toggle **Render as Markdown** to see the difference.

Nested Editing Demo

  • Item 1
  • Item 2
  • Item 3

EditablePage & EditableRow

A real wrapper composition. Its insertion control uses a presentation adapter for the existing first-row insertion action, returning the live sandbox page and its canonical modal host through OOB output. The wrapper row is a read-only variant, not a second datastore.

Read-only wrapper specimen

This row intentionally has no edit controls. Use the live sandbox above to mutate content.

Java composition
Row rowContent = new Row().withChild(new Column().withWidth(12)
    .withChild(ContentModule.create().withTitle("Read-only wrapper specimen")
        .withContent("This row intentionally has no edit controls. Use the live sandbox above to mutate content.")));
EditableRow row = EditableRow.wrap(rowContent, "wrapper-row", "wrapper-page")
    .withCanAddModule(false);
EditablePage.create("wrapper-page")
    .withInsertRowUrlTemplate("/demos/features/editing/insert-wrapper-row")
    .addEditableRow(row);

Editable ยท EditableChild ยท EditModalBuilder ยท EditMode

The live sandbox above reconstructs Editable modules from this session. Open Nested Editing Demo to edit its positional children; choose USER mode to stage a content proposal, then use Pending Edits above to approve or reject it. The listing is the module-9 modal composition with its resolved URLs. The controller supplies editable from the current session's SimpleListModule and combinedForm from its property fields, sandbox notice and width selector; neither is a second datastore. Reset the sandbox if you deleted that module.

Java composition
public static Component nestedTriggers() {
    return new Div().withChild(Button.create("Open live nested edit")
        .withAttribute("hx-get", "/editing-demo/edit/module-9?editMode=OWNER_EDIT")
        .withAttribute("hx-target", "#edit-modal-container"))
        .withChild(Button.create("Open live nested proposal")
            .withAttribute("hx-get", "/editing-demo/edit/module-9?editMode=USER_EDIT")
            .withAttribute("hx-target", "#edit-modal-container"));
}
// Corresponding module-9 composition: arguments come from the live controller.
// EditModalBuilder consumes editable.getEditableChildren(): List<EditableChild>.
public static String nestedModal(Editable<?> editable, Component combinedForm, EditMode mode) {
    return EditModalBuilder.create().withTitle("Edit Module").withModuleId("module-9")
        .withEditView(combinedForm).withEditable(editable)
        .withSaveUrl("/editing-demo/save/module-9?editMode=" + mode.name())
        .withDeleteUrl("/editing-demo/delete/module-9?editMode=" + mode.name())
        .withChildEditUrl("/editing-demo/edit-child/module-9/{id}?editMode=" + mode.name())
        .withChildDeleteUrl("/editing-demo/delete-child/module-9/{id}?editMode=" + mode.name())
        .withPageContainerId("page-content").withModalContainerId("edit-modal-container").build().render();
}
// OWNER_EDIT applies to live session content; USER_EDIT stages a proposal.

Slot-driven edit fields & validation

EditFormBuilder derives controls using FormFieldHelper and SlotKeyMap. Submit a nonblank title of at most 80 characters. This is a stateless preview, separate from the mutable sandbox above; invalid input retains its draft.

Validated preview

A slot-driven draft
Java composition
public static String editFieldsResult(String title, boolean markdown, String error) {
    SlotKey<String> key = SlotKey.of("draftTitle");
    SlotKey<Boolean> enabled = SlotKey.of("markdown");
    Component fields = EditFormBuilder.fromSlots(java.util.Map.of("draftTitle", key, "markdown", enabled),
        java.util.Map.of("draftTitle", String.class, "markdown", Boolean.class),
        SlotKeyMap.create().putString("draftTitle", title).putBoolean("markdown", markdown));
    Form form = Form.create().withAction("/demos/api/edit-fields").withMethod(Form.Method.POST)
        .withHxPost("/demos/api/edit-fields").withHxTarget("#edit-fields-result").withHxSwap("outerHTML")
        .withChild(fields).withChild(Button.submit("Validate draft preview"));
    Div preview = new Div().withId("edit-fields-result").withAttribute("aria-live", "polite");
    if (error != null) preview.withChild(Alert.danger(error).withAttribute("role", "alert"));
    ContentModule result = ContentModule.create().withTitle(error == null ? "Validated preview" : "Draft ยท not accepted").withContent(title);
    if (!markdown) result.disableMarkdown();
    return preview.withChild(form).withChild(result).render();
}
// POST /demos/api/edit-fields validates before returning this same draft:
// ValidationResult validation = draftTitle.isBlank() || draftTitle.length() > 80
//     ? ValidationResult.invalid("Use a nonblank title of at most 80 characters.")
//     : ValidationResult.valid();
// return editFieldsResult(draftTitle, markdown, validation.isValid() ? null : validation.getErrorsAsString());
// EditFormBuilder delegates String/Boolean controls to FormFieldHelper.
// SlotKeyMap stores TypedValue entries; it is not shared between requests.

ModuleEditHandler: request-owned preview

Each submission creates a new ContentModule. Update renders a copy; delete omits that copy. USER mode renders a proposal preview only. Nothing is saved, queued or deleted in the sandbox above.

Request-owned title

Edit this temporary copy.
Java composition
public static final class PreviewHandler implements ModuleEditHandler<ContentModule> {
    private final String title;
    private final String content;
    private final EditMode mode;
    public PreviewHandler(String title, String content, EditMode mode) {
        this.title = title;
        this.content = content;
        this.mode = mode;
    }
    private ContentModule copy(String id) {
        return ContentModule.create().withModuleId(id).withTitle(title).withContent(content).disableMarkdown();
    }
    public Component renderEditForm(String id) {
        return Form.create().withAction("/demos/api/module-preview").withMethod(Form.Method.POST)
            .withHxPost("/demos/api/module-preview").withHxTarget("#handler-result").withHxSwap("outerHTML")
            .withChild(FormFieldHelper.textField("Preview title", "title", title))
            .withChild(FormFieldHelper.textAreaField("Preview content", "content", content, 3))
            .addField("Operation", Select.create("operation").addOption("edit", "Render edit preview")
                .addOption("update", "Update copy", true).addOption("delete", "Delete copy"))
            .addField("Preview mode", Select.create("mode").addOption("OWNER_EDIT", "OWNER_EDIT", mode == EditMode.OWNER_EDIT)
                .addOption("USER_EDIT", "USER_EDIT", mode == EditMode.USER_EDIT))
            .withChild(Button.submit("Apply preview request"));
    }
    public Component handleUpdate(String id, Map<String, String> data, EditMode editMode) {
        ContentModule candidate = copy(id);
        ValidationResult validation = candidate.validate(data);
        if (!validation.isValid()) return Alert.danger(validation.getErrorsAsString()).withAttribute("role", "alert");
        candidate.applyEdits(data);
        return new Div().withChild(new Paragraph(editMode == EditMode.USER_EDIT
            ? "Proposal preview only โ€” not queued." : "Updated request-owned copy โ€” not saved."))
            .withChild(candidate);
    }
    public Component handleDelete(String id, EditMode editMode) {
        return editMode == EditMode.USER_EDIT
            ? new Div().withChild(new Paragraph("Delete proposal preview only โ€” not queued.")).withChild(copy(id))
            : new Paragraph("Deleted request-owned preview copy โ€” no stored content changed.");
    }
}
public static String handlerResult(String operation, String title, String content, EditMode mode) {
    ModuleEditHandler<ContentModule> handler = new PreviewHandler(title, content, mode);
    Component result = switch (operation) {
        case "edit" -> ContentModule.create().withTitle(title).withContent(content).disableMarkdown();
        case "update" -> handler.handleUpdate("request-preview", Map.of("title", title, "content", content), mode);
        case "delete" -> handler.handleDelete("request-preview", mode);
        default -> throw new IllegalArgumentException("Unknown preview operation");
    };
    return new Div().withId("handler-result").withAttribute("aria-live", "polite")
        .withChild(handler.renderEditForm("request-preview")).withChild(new Div().withId("handler-copy").withChild(result)).render();
}
// Protected POST /demos/api/module-preview parses operation/title/content/mode,
// then calls handlerResult. No handler or module instance is retained between requests.

AuthWrapper: simulated policy

Toggle the simulated check. This demonstrates action gating, not login or real authorization.

Simulated action denied

Reviewer mode: USER_EDIT

Reviewer may delete: false

Owner may delete: false

Java composition
public static AuthorizationChecker policy(boolean allow) {
    return new AuthorizationChecker() {
        public boolean canEdit(String moduleId, String userId) { return allow && "specimen".equals(moduleId) && "reviewer".equals(userId); }
        public boolean canDelete(String moduleId, String userId) { return allow && "specimen".equals(moduleId) && "owner".equals(userId); }
        public EditMode getEditMode(String moduleId, String userId) {
            return canDelete(moduleId, userId) ? EditMode.OWNER_EDIT : EditMode.USER_EDIT;
        }
    };
}
public static String auth(boolean allow) {
    AuthorizationChecker checker = policy(allow);
    return new Div().withId("auth-result").withAttribute("role", "status")
        .withChild(new RawHtml(AuthWrapper.require(() -> checker.canEdit("specimen", "reviewer"),
            () -> Alert.success("Simulated action allowed").render(), () -> Alert.warning("Simulated action denied").render())))
        .withChild(new Paragraph("Reviewer mode: " + checker.getEditMode("specimen", "reviewer")))
        .withChild(new Paragraph("Reviewer may delete: " + checker.canDelete("specimen", "reviewer")))
        .withChild(new Paragraph("Owner may delete: " + checker.canDelete("specimen", "owner"))).render();
}
public static String authPreview() {
    return new Div().withChild(Form.create().withAction("/demos/api/auth").withMethod(Form.Method.POST)
        .withHxPost("/demos/api/auth").withHxTarget("#auth-result").withHxSwap("outerHTML")
        .withChild(Checkbox.create("allow", "true").withLabel("Simulate permission granted"))
        .withChild(Button.submit("Evaluate policy"))).withChild(new RawHtml(auth(false))).render();
}
// The checkbox POSTs allow to /demos/api/auth; the response replaces #auth-result.