Finance

MembershipApplication

A co-op membership application in steps: personal details, ID, beneficiaries adding up to 100%, the share pledge, and a review.

#finance#coop#forms#wizard#interactive

Demo

Preview
  1. 1. Personal details
  2. 2. ID
  3. 3. Beneficiaries
  4. 4. Share pledge
  5. 5. Review

Step 1 of 5: Personal details

Personal details

Step 1 of 5

on_step: 0

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/membership_application.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/membership_application.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/membership_application.qwe
component MembershipApplicationDemoLive() {    state step = 0    <div.demo-stack>        <MembershipApplication id="membership-application-demo" action="#membership-application" coop="Bayanihan Multi-Purpose Cooperative"                               currency="PHP" par={100} min_shares={50} initial_percent={25} hidden={{ branch: "poblacion" }}                               on_step={(i) => { step = i }}/>        <p.demo-note>on_step: {step}</p>    </div>}view MembershipApplicationDemo() = <MembershipApplicationDemoLive/>

Source

ui/membership_application.qwe
// MembershipApplication: the application to join a co-op or credit union, in// steps: personal details, an ID, the beneficiaries (as many rows as are needed,// their shares adding up to 100%), the share pledge (how many shares, at the par// value, what is paid now and what is left), and a review of it all with Edit// buttons and the applicant's agreement, before it is sent. Every step's fields// are real inputs in one <form method="post">, so the route gets the whole// application at once; Next checks the step with the browser's own validation and// points to what to fix.////   <MembershipApplication id="join" action="/membership/apply" coop="Bayanihan Multi-Purpose Cooperative"//                          currency="PHP" par={100} min_shares={50} initial_percent={25}/>//// The fields sent: first_name, middle_name, last_name, birth_date, sex,// civil_status, mobile, email, address, occupation, employer; id_type, id_number,// id_expiry, tin; for each beneficiary beneficiary_name, beneficiary_relation and// beneficiary_share (in order, so a route reads them as lists); shares,// initial_payment, payment_plan; and agree ("yes"), with any `hidden` fields.//// id: a prefix for its ids, unique on the page ("membership-application"). action:// where it posts. coop: the co-op's name, in the agreement. par: the par value of// one share (100), in `currency` (ISO code, "USD") written for `locale` (the// program's). min_shares: the fewest shares to subscribe (1). initial_percent: what// part of the subscription is paid on joining, in percent (25). plans: the ways to// pay the rest (["Salary deduction", "Monthly cash payment", "Paid in full now"]).// id_types: the IDs taken (national ID, passport, driver's license, ...).// relations: the relationships a beneficiary may have. max_beneficiaries (4).// agreement: the text the applicant agrees to. values: what to fill in to start// with, by field name. hidden: other fields to send, as { name: value }. submit:// the last button's text ("Send my application"). on_step(index): inside one of// your components, called with the step shown (4 for the review).import ui/theme.qwe use ui_clslet membership_application_css = css`.ui-membership-app { display: grid; gap: 18px; min-width: 0; max-width: 640px; font: 14px/1.45 var(--ui-font); color: var(--ui-fg); }.ui-membership-app-steps { display: flex; gap: 6px; margin: 0; padding: 0; list-style: none; }.ui-membership-app-step { flex: 1 1 0; min-width: 0; display: grid; gap: 6px; }.ui-membership-app-step::before { content: ""; height: 4px; border-radius: 2px; background: var(--ui-border); }.ui-membership-app-done::before, .ui-membership-app-current::before { background: var(--ui-accent); }.ui-membership-app-step-label { display: block; padding: 0; border: 0; background: none; color: var(--ui-muted); font: 500 12px var(--ui-font);  text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border-radius: 3px; }.ui-membership-app-current .ui-membership-app-step-label { color: var(--ui-fg); }button.ui-membership-app-step-label { color: var(--ui-accent); cursor: pointer; }button.ui-membership-app-step-label:hover { text-decoration: underline; }button.ui-membership-app-step-label:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-membership-app-form { display: grid; gap: 18px; padding: 22px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); }.ui-membership-app-panel { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; margin: 0; padding: 0; border: 0; min-width: 0; }.ui-membership-app-panel[hidden] { display: none; }.ui-membership-app .ui-membership-app-title { display: block; grid-column: 1 / -1; margin: 0; font-size: 17px; font-weight: 650; outline: none; }.ui-membership-app-text { grid-column: 1 / -1; margin: -8px 0 0; color: var(--ui-muted); }.ui-membership-app-field { display: grid; gap: 5px; align-content: start; min-width: 0; }.ui-membership-app-wide { grid-column: 1 / -1; }.ui-membership-app-label { font-size: 13px; font-weight: 500; }.ui-membership-app-req { margin-left: 2px; color: var(--ui-danger); }.ui-membership-app-input { box-sizing: border-box; width: 100%; height: 38px; margin: 0; padding: 0 12px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.5 var(--ui-font); transition: border-color .15s, box-shadow .15s; }textarea.ui-membership-app-input { height: auto; padding: 8px 12px; resize: vertical; }.ui-membership-app-input:hover { border-color: var(--ui-strong); }.ui-membership-app-input:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-membership-app-input:user-invalid { border-color: var(--ui-danger); }.ui-membership-app-hint { margin: 0; color: var(--ui-muted); font-size: 12px; }.ui-membership-app-check { display: flex; align-items: flex-start; gap: 8px; cursor: pointer; }.ui-membership-app-check input { margin: 3px 0 0; accent-color: var(--ui-accent); }.ui-membership-app-bens { grid-column: 1 / -1; display: grid; gap: 10px; }.ui-membership-app-ben { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1.3fr) 90px auto; align-items: end; gap: 10px;  padding: 12px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); }.ui-membership-app-ben-num { grid-column: 1 / -1; margin: 0; color: var(--ui-muted); font-size: 12px; font-weight: 600; }.ui-membership-app-icon { display: grid; place-items: center; width: 38px; height: 38px; padding: 0; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm);  background: var(--ui-surface); color: var(--ui-muted); cursor: pointer; }.ui-membership-app-icon:hover:not(:disabled) { color: var(--ui-danger); border-color: var(--ui-danger); }.ui-membership-app-icon:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-membership-app-icon:disabled { opacity: .4; cursor: not-allowed; }.ui-membership-app-add { justify-self: start; height: 34px; padding: 0 14px; border: 1px dashed var(--ui-border); border-radius: var(--ui-radius-sm);  background: none; color: var(--ui-accent); font: 500 13px var(--ui-font); cursor: pointer; }.ui-membership-app-add:hover:not(:disabled) { border-color: var(--ui-accent); }.ui-membership-app-add:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-membership-app-add:disabled { opacity: .5; cursor: not-allowed; }.ui-membership-app-total { margin: 0; font-weight: 500; font-variant-numeric: tabular-nums; }.ui-membership-app-total-ok { color: var(--ui-success); }.ui-membership-app-error { grid-column: 1 / -1; margin: 0; padding: 8px 12px; border-radius: var(--ui-radius-sm); background: var(--ui-danger-soft); color: var(--ui-danger); font-size: 13px; }.ui-membership-app-pledge { grid-column: 1 / -1; display: grid; gap: 0; margin: 0; padding: 4px 14px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); }.ui-membership-app-pledge div { display: flex; justify-content: space-between; gap: 16px; padding: 8px 0; border-bottom: 1px solid var(--ui-border); }.ui-membership-app-pledge div:last-child { border-bottom: 0; }.ui-membership-app-pledge dt { color: var(--ui-muted); }.ui-membership-app-pledge dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; text-align: right; }.ui-membership-app-review { grid-column: 1 / -1; display: grid; gap: 12px; }.ui-membership-app-review-step { padding: 12px 14px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); }.ui-membership-app-review-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-bottom: 6px; }.ui-membership-app-review-head h4 { margin: 0; font-size: 14px; font-weight: 600; }.ui-membership-app-edit { padding: 2px 8px; border: 0; border-radius: 4px; background: none; color: var(--ui-accent); font: 500 13px var(--ui-font); cursor: pointer; }.ui-membership-app-edit:hover { background: var(--ui-accent-soft); }.ui-membership-app-edit:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-membership-app-dl { display: grid; gap: 4px; margin: 0; }.ui-membership-app-dl div { display: grid; grid-template-columns: 150px 1fr; gap: 12px; }.ui-membership-app-dl dt { color: var(--ui-muted); }.ui-membership-app-dl dd { margin: 0; overflow-wrap: anywhere; }.ui-membership-app-actions { display: flex; align-items: center; gap: 10px; padding-top: 14px; border-top: 1px solid var(--ui-border); }.ui-membership-app-gap { flex: 1; }.ui-membership-app-count { color: var(--ui-muted); font-size: 13px; }.ui-membership-app-button { height: 38px; padding: 0 18px; border: 1px solid transparent; border-radius: var(--ui-radius-sm); background: var(--ui-accent);  color: var(--ui-accent-fg); font: 500 14px var(--ui-font); cursor: pointer; }.ui-membership-app-button:hover { filter: brightness(1.08); }.ui-membership-app-button:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-membership-app-back { background: var(--ui-surface); border-color: var(--ui-border); color: var(--ui-fg); }@media (max-width: 560px) {  .ui-membership-app-panel { grid-template-columns: 1fr; }  .ui-membership-app-ben { grid-template-columns: 1fr 1fr; }  .ui-membership-app-dl div { grid-template-columns: 1fr; gap: 0; }}@media (prefers-reduced-motion: reduce) { .ui-membership-app-input { transition: none; } }`let membership_application_titles = ["Personal details", "ID", "Beneficiaries", "Share pledge", "Review"]// The fields of the first two steps.fn membership_application_personal() -> list = [    { name: "first_name", label: "First name", required: true, autocomplete: "given-name" },    { name: "middle_name", label: "Middle name", autocomplete: "additional-name" },    { name: "last_name", label: "Last name", required: true, autocomplete: "family-name" },    { name: "birth_date", label: "Date of birth", type: "date", required: true, autocomplete: "bday" },    { name: "sex", label: "Sex", type: "select", options: ["Female", "Male"] },    { name: "civil_status", label: "Civil status", type: "select", options: ["Single", "Married", "Widowed", "Separated"] },    { name: "mobile", label: "Mobile number", type: "tel", required: true, autocomplete: "tel" },    { name: "email", label: "Email", type: "email", autocomplete: "email" },    { name: "address", label: "Home address", type: "textarea", required: true, wide: true, autocomplete: "street-address" },    { name: "occupation", label: "Occupation" },    { name: "employer", label: "Employer or business" },]fn membership_application_id_fields(id_types: list) -> list = [    { name: "id_type", label: "ID", type: "select", required: true, options: id_types },    { name: "id_number", label: "ID number", required: true, autocomplete: "off" },    { name: "id_expiry", label: "Valid until", type: "date", hint: "Leave empty if it does not expire." },    { name: "tin", label: "Tax ID number", hint: "Optional.", autocomplete: "off" },]// What the review shows for a field's value.fn membership_application_shown(v) -> str = v == null || str(v) == "" ? "—" : str(v)// A beneficiary's share as a number (0 when it is not one).fn membership_application_share(b) -> int = int(trim(str(b.share ?? ""))) ?? 0// One field: its label, the control, its hint.view MembershipApplicationField(id: str, f: map, value = null, on_value = null) {    style global membership_application_css    let t = f.type ?? "text"    let fid = `${id}-${f.name}`    let hint_id = f.hint != null ? `${fid}-hint` : null    let req = f.required ?? false    let keep = (x) => { on_value && on_value(x) }    <div class={ui_cls(["ui-membership-app-field", (f.wide ?? false) && "ui-membership-app-wide"])}>        {t != "checkbox" && <label.ui-membership-app-label for={fid}>{f.label}{req && <span.ui-membership-app-req aria-hidden="true">*</span>}</label>}        {match t {            "textarea" => <textarea.ui-membership-app-input id={fid} name={f.name} rows="2" required={req} autocomplete={f.autocomplete}                                    aria-describedby={hint_id} @input={(e) => keep(e.value)}>{value ?? ""}</textarea>            "select" => <select.ui-membership-app-input id={fid} name={f.name} required={req} aria-describedby={hint_id} @change={(e) => keep(e.value)}>                <option value="" selected={value == null || value == ""}>{req ? "Choose one" : "—"}</option>                {for o in f.options ?? [] => <option value={o} selected={o == value}>{o}</option>}            </select>            "checkbox" => <label.ui-membership-app-check>                <input type="checkbox" id={fid} name={f.name} value="yes" checked={value == true || value == "yes"} required={req}                       aria-describedby={hint_id} @change={(e) => keep(e.checked)}/>                <span>{f.label}{req && <span.ui-membership-app-req aria-hidden="true">*</span>}</span>            </label>            _ => <input.ui-membership-app-input type={t} id={fid} name={f.name} value={value} required={req} min={f.min} max={f.max}                       step={f.step} inputmode={f.inputmode} autocomplete={f.autocomplete} aria-describedby={hint_id}                       @input={(e) => keep(e.value)}/>        }}        {f.hint != null && <p.ui-membership-app-hint id={hint_id}>{f.hint}</p>}    </div>}component MembershipApplication(id: str = "membership-application", action: str = "/", coop: str? = null, par = 100, currency: str = "USD",                                locale: str? = null, min_shares: int = 1, initial_percent = 25,                                plans: list = ["Salary deduction", "Monthly cash payment", "Paid in full now"],                                id_types: list = ["National ID", "Passport", "Driver's license", "Social security ID", "Voter's ID"],                                relations: list = ["Spouse", "Child", "Parent", "Sibling", "Other relative", "Other"],                                max_beneficiaries: int = 4, agreement: str? = null, values: map = {}, hidden: map = {},                                submit: str = "Send my application", on_step = null, class: str? = null) {    style global membership_application_css    state at = 0    state vals = values    state bens = [{ k: 1, name: "", relation: "", share: "100" }]    state next_k = 2    state ben_error = null    let code = upper(currency)    let shown = (m) => locale != null ? money.format(m, locale) : money.format(m)    let par_m = money.of(dec(par), code, "half_up")    let shares = int(trim(str(vals.shares ?? ""))) ?? 0    let subscribed = money.mul(par_m, shares)    let least = money.mul(subscribed, dec(initial_percent) / 100, "ceiling")    let paid_d = dec(trim(str(vals.initial_payment ?? "")))    let paid = is_err(paid_d) ? money.zero(code) : money.of(paid_d, code, "half_up") ?? money.zero(code)    let left = money.compare(paid, subscribed) >= 0 ? money.zero(code) : money.sub(subscribed, paid)    let total_share = sum(bens | map(membership_application_share))    let keep = (name: str, v) => { vals = set(vals, name, v) }    let personal = membership_application_personal()    let ids = membership_application_id_fields(id_types)    let pledge = [        { name: "shares", label: "Shares to subscribe", type: "number", required: true, min: str(min_shares), step: "1", inputmode: "numeric",          hint: `At least ${min_shares}, at ${shown(par_m)} each.` },        { name: "initial_payment", label: "Paid on joining", type: "number", required: true, min: str(least.amount),          max: shares > 0 ? str(subscribed.amount) : null, step: "0.01", inputmode: "decimal",          hint: `At least ${shown(least)} (${initial_percent}% of the subscription).` },        { name: "payment_plan", label: "The rest paid by", type: "select", required: true, options: plans, wide: true },    ]    let go = (e, i: int) => {        at = i        ben_error = null        on_step && on_step(i)        let root = e.target.closest(".ui-membership-app")        js("requestAnimationFrame", (t = null) => {            let h = root.querySelector(`#${id}-title-${i}`)            h && h.focus()        })    }    let forward = (e) => {        let panel = e.target.closest("form").querySelector(`[data-step="${at}"]`)        let wrong = panel.querySelector("input:invalid, select:invalid, textarea:invalid")        if wrong != null { wrong.reportValidity() }        else if at == 2 && total_share != 100 { ben_error = `The beneficiaries' shares add up to ${total_share}%; they must make 100%.` }        else { go(e, at + 1) }    }    let set_ben = (k: int, field: str, v) => {        bens = bens | map((b) => b.k == k ? set(b, field, v) : b)        ben_error = null    }    let add_ben = () => {        bens = [...bens, { k: next_k, name: "", relation: "", share: "" }]        next_k = next_k + 1    }    let drop_ben = (k: int) => { bens = bens | filter((b) => b.k != k) }    let review_rows = (fields: list) => for f in fields => <div><dt>{f.label}</dt><dd>{membership_application_shown(vals[f.name])}</dd></div>    let review_head = (e_i: int) =>        <div.ui-membership-app-review-head>            <h4>{membership_application_titles[e_i]}</h4>            <button.ui-membership-app-edit type="button" aria-label={`Edit ${membership_application_titles[e_i]}`} @click={(e) => go(e, e_i)}>Edit</button>        </div>    <div class={ui_cls(["ui-membership-app", class])}>        <ol.ui-membership-app-steps aria-label="Steps">            {for [i, t] in enumerate(membership_application_titles) => {                let phase = i < at ? "done" : i == at ? "current" : "upcoming"                let label = [`${i + 1}. ${t}`, phase == "done" && <span.ui-sr> (done)</span>]                <li key={i} class={`ui-membership-app-step ui-membership-app-${phase}`} aria-current={phase == "current" ? "step" : null}>                    {phase == "done" ? <button.ui-membership-app-step-label type="button" @click={(e) => go(e, i)}>{label}</button>                                     : <span.ui-membership-app-step-label>{label}</span>}                </li>            }}        </ol>        <p.ui-sr aria-live="polite">Step {at + 1} of 5: {membership_application_titles[at]}</p>        <form.ui-membership-app-form method="post" action={action}>            {for k in keys(hidden) => <input type="hidden" name={k} value={str(hidden[k])}/>}            <fieldset.ui-membership-app-panel data-step="0" hidden={at != 0} aria-labelledby={`${id}-title-0`}>                <h3.ui-membership-app-title id={`${id}-title-0`} tabindex="-1">Personal details</h3>                {for f in personal => <MembershipApplicationField key={f.name} id={id} f={f} value={vals[f.name]} on_value={(x) => keep(f.name, x)}/>}            </fieldset>            <fieldset.ui-membership-app-panel data-step="1" hidden={at != 1} aria-labelledby={`${id}-title-1`}>                <h3.ui-membership-app-title id={`${id}-title-1`} tabindex="-1">ID</h3>                <p.ui-membership-app-text>A valid government ID with your photo. Bring it when you come to sign.</p>                {for f in ids => <MembershipApplicationField key={f.name} id={id} f={f} value={vals[f.name]} on_value={(x) => keep(f.name, x)}/>}            </fieldset>            <fieldset.ui-membership-app-panel data-step="2" hidden={at != 2} aria-labelledby={`${id}-title-2`}>                <h3.ui-membership-app-title id={`${id}-title-2`} tabindex="-1">Beneficiaries</h3>                <p.ui-membership-app-text>Who receives your share capital and savings if you die. Their shares add up to 100%.</p>                <div.ui-membership-app-bens>                    {for [i, b] in enumerate(bens) => {                        let bid = `${id}-ben-${b.k}`                        <div.ui-membership-app-ben key={b.k} role="group" aria-label={`Beneficiary ${i + 1}`}>                            <p.ui-membership-app-ben-num aria-hidden="true">Beneficiary {i + 1}</p>                            <div.ui-membership-app-field>                                <label.ui-membership-app-label for={`${bid}-name`}>Full name<span.ui-membership-app-req aria-hidden="true">*</span></label>                                <input.ui-membership-app-input id={`${bid}-name`} name="beneficiary_name" value={b.name} required={true} autocomplete="off"                                       @input={(e) => set_ben(b.k, "name", e.value)}/>                            </div>                            <div.ui-membership-app-field>                                <label.ui-membership-app-label for={`${bid}-relation`}>Relationship<span.ui-membership-app-req aria-hidden="true">*</span></label>                                <select.ui-membership-app-input id={`${bid}-relation`} name="beneficiary_relation" required={true}                                        @change={(e) => set_ben(b.k, "relation", e.value)}>                                    <option value="" selected={b.relation == ""}>Choose one</option>                                    {for r in relations => <option value={r} selected={r == b.relation}>{r}</option>}                                </select>                            </div>                            <div.ui-membership-app-field>                                <label.ui-membership-app-label for={`${bid}-share`}>Share %<span.ui-membership-app-req aria-hidden="true">*</span></label>                                <input.ui-membership-app-input type="number" id={`${bid}-share`} name="beneficiary_share" value={b.share} required={true}                                       min="1" max="100" step="1" inputmode="numeric" @input={(e) => set_ben(b.k, "share", e.value)}/>                            </div>                            <button.ui-membership-app-icon type="button" aria-label={`Remove beneficiary ${i + 1}`} disabled={len(bens) == 1}                                    @click={() => drop_ben(b.k)}>                                <svg viewBox="0 0 14 14" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M3 3l8 8M11 3l-8 8"/></svg>                            </button>                        </div>                    }}                    <button.ui-membership-app-add type="button" disabled={len(bens) >= max_beneficiaries} @click={() => add_ben()}>+ Add a beneficiary</button>                    <p class={ui_cls(["ui-membership-app-total", total_share == 100 && "ui-membership-app-total-ok"])} aria-live="polite">Total: {total_share}%</p>                </div>                {ben_error != null && <p.ui-membership-app-error role="alert">{ben_error}</p>}            </fieldset>            <fieldset.ui-membership-app-panel data-step="3" hidden={at != 3} aria-labelledby={`${id}-title-3`}>                <h3.ui-membership-app-title id={`${id}-title-3`} tabindex="-1">Share pledge</h3>                <p.ui-membership-app-text>The shares you subscribe are your capital in the co-op; part is paid now and the rest over time.</p>                {for f in pledge => <MembershipApplicationField key={f.name} id={id} f={f} value={vals[f.name]} on_value={(x) => keep(f.name, x)}/>}                <dl.ui-membership-app-pledge aria-live="polite">                    <div.ui-membership-app-subscribed><dt>Subscribed</dt><dd>{shares} × {shown(par_m)} = {shown(subscribed)}</dd></div>                    <div.ui-membership-app-paid><dt>Paid on joining</dt><dd>{shown(paid)}</dd></div>                    <div.ui-membership-app-left><dt>Left to pay</dt><dd>{shown(left)}</dd></div>                </dl>            </fieldset>            <section.ui-membership-app-panel data-step="4" hidden={at != 4} aria-labelledby={`${id}-title-4`}>                <h3.ui-membership-app-title id={`${id}-title-4`} tabindex="-1">Review</h3>                <div.ui-membership-app-review>                    <div.ui-membership-app-review-step>{review_head(0)}<dl.ui-membership-app-dl>{review_rows(personal)}</dl></div>                    <div.ui-membership-app-review-step>{review_head(1)}<dl.ui-membership-app-dl>{review_rows(ids)}</dl></div>                    <div.ui-membership-app-review-step>{review_head(2)}<dl.ui-membership-app-dl>                        {for b in bens => <div key={b.k}><dt>{b.share}%</dt><dd>{membership_application_shown(b.name)}{b.relation != "" && `, ${lower(b.relation)}`}</dd></div>}                    </dl></div>                    <div.ui-membership-app-review-step>{review_head(3)}<dl.ui-membership-app-dl>                        <div><dt>Subscribed</dt><dd>{shares} shares, {shown(subscribed)}</dd></div>                        <div><dt>Paid on joining</dt><dd>{shown(paid)}</dd></div>                        <div><dt>The rest</dt><dd>{shown(left)}, {lower(membership_application_shown(vals.payment_plan))}</dd></div>                    </dl></div>                </div>                <div.ui-membership-app-wide>                    <MembershipApplicationField id={id} f={{ name: "agree", type: "checkbox", required: true, wide: true,                        label: agreement ?? `I have attended the pre-membership seminar, I agree to abide by the by-laws of ${coop ?? "the cooperative"}, and what I have written here is true.` }}                        value={vals.agree} on_value={(x) => keep("agree", x)}/>                </div>            </section>            <div.ui-membership-app-actions>                {at > 0 && <button.ui-membership-app-button.ui-membership-app-back type="button" @click={(e) => go(e, at - 1)}>Back</button>}                <span.ui-membership-app-gap></span>                <span.ui-membership-app-count>Step {at + 1} of 5</span>                {at < 4 ? <button.ui-membership-app-button type="button" @click={forward}>Next</button>                        : <button.ui-membership-app-button type="submit">{submit}</button>}            </div>        </form>    </div>}
Thread
76Useful? Like it, or add to its thread below.

Thread 0 posts

On MembershipApplication: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.