MembershipApplication
A co-op membership application in steps: personal details, ID, beneficiaries adding up to 100%, the share pledge, and a review.
Demo
- 1. Personal details
- 2. ID
- 3. Beneficiaries
- 4. Share pledge
- 5. Review
Step 1 of 5: Personal details
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.qwemain.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>}Related
AssemblyNoticeA general assembly notice: date and time, the venue or online link, the quorum, a numbered agenda and an RSVP form.6 views · 6 downloadsCoopBallotA board election ballot: candidates as cards with a short statement, up to N ticked and no more, sent as a plain form post.7 views · 8 downloadsPatronageRefundCalcWorks out a co-op member's dividend on shares and patronage refund on interest paid or purchases, to the total, as they type.7 views · 4 downloadsTransferFormA transfer between your accounts or to a payee: from, to, amount, date and memo, checked, then a review with the fee and the balance after.7 views · 9 downloadsAccountPickerChoose the account to pay from as radio cards with each one's masked number and available balance, those short of the amount marked.7 views · 7 downloadsCardControlsA card's controls beside its masked face: freeze it, set daily spending and cash limits, and allow online, ATM, contactless and abroad use.7 views · 6 downloads
Thread
Thread 0 posts
On MembershipApplication: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.