Create / Edit Form
A full-page form for creating or editing a record. Includes field grouping, validation errors, loading state on submit, and cancel/save actions.
Components used​
Layout · Header · Card · FormField · Input · Select · Textarea · Toggle · Button
Code​
- HTML · @primus/ui-core
- React
- Angular
<div style="background:var(--background);width:100%;padding:1.25rem;box-sizing:border-box">
<!-- Page header -->
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.25rem">
<div>
<h1 style="margin:0;font-size:1.25rem;font-weight:700">Create Tenant</h1>
<p style="margin:0;font-size:0.8rem;color:var(--muted-foreground)">Fill in the details below to onboard a new tenant</p>
</div>
<div class="hstack">
<button class="outline small">Cancel</button>
<button class="small">Save Tenant</button>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 320px;gap:1rem;align-items:start">
<!-- Main form -->
<div class="vstack">
<!-- Basic info card -->
<div class="card" style="padding:1.5rem">
<h3 style="margin:0 0 1.25rem;font-size:1rem;font-weight:600;padding-bottom:0.75rem;border-bottom:1px solid var(--border)">Basic Information</h3>
<div class="vstack" style="gap:1rem">
<div data-field>
<label for="t-name">Tenant name <span style="color:var(--danger)">*</span></label>
<input id="t-name" type="text" placeholder="Acme Corp" value="Acme Corp" />
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.75rem">
<div data-field>
<label for="t-slug">Slug</label>
<input id="t-slug" type="text" placeholder="acme-corp" value="acme-corp" />
<span data-hint>Used in URLs and API keys</span>
</div>
<div data-field>
<label for="t-domain">Domain</label>
<input id="t-domain" type="text" placeholder="acme.com" />
</div>
</div>
<div data-field="error">
<label for="t-email">Admin email <span style="color:var(--danger)">*</span></label>
<input id="t-email" type="email" placeholder="admin@acme.com" aria-invalid="true" />
<span class="error">Please enter a valid email address.</span>
</div>
</div>
</div>
<!-- Plan & billing card -->
<div class="card" style="padding:1.5rem">
<h3 style="margin:0 0 1.25rem;font-size:1rem;font-weight:600;padding-bottom:0.75rem;border-bottom:1px solid var(--border)">Plan & Billing</h3>
<div class="vstack" style="gap:1rem">
<div data-field>
<label for="t-plan">Plan <span style="color:var(--danger)">*</span></label>
<select id="t-plan">
<option>Starter</option>
<option selected>Pro</option>
<option>Enterprise</option>
</select>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.75rem">
<div data-field>
<label for="t-billing">Billing cycle</label>
<select id="t-billing"><option>Monthly</option><option selected>Annual</option></select>
</div>
<div data-field>
<label for="t-seats">Max seats</label>
<input id="t-seats" type="number" value="50" min="1" />
</div>
</div>
</div>
</div>
<!-- Notes card -->
<div class="card" style="padding:1.5rem">
<h3 style="margin:0 0 1.25rem;font-size:1rem;font-weight:600;padding-bottom:0.75rem;border-bottom:1px solid var(--border)">Notes</h3>
<textarea rows="3" placeholder="Internal notes about this tenant..."></textarea>
</div>
</div>
<!-- Sidebar settings -->
<div class="vstack">
<div class="card" style="padding:1.25rem">
<h3 style="margin:0 0 1rem;font-size:0.9rem;font-weight:600">Settings</h3>
<div class="vstack" style="gap:0.875rem">
<label style="display:flex;align-items:center;justify-content:space-between;font-size:0.875rem">
Active
<input type="checkbox" role="switch" checked />
</label>
<label style="display:flex;align-items:center;justify-content:space-between;font-size:0.875rem">
Email notifications
<input type="checkbox" role="switch" checked />
</label>
<label style="display:flex;align-items:center;justify-content:space-between;font-size:0.875rem">
MFA required
<input type="checkbox" role="switch" />
</label>
<label style="display:flex;align-items:center;justify-content:space-between;font-size:0.875rem">
SSO only
<input type="checkbox" role="switch" />
</label>
</div>
</div>
<div class="card" style="padding:1.25rem">
<h3 style="margin:0 0 0.75rem;font-size:0.9rem;font-weight:600">Region</h3>
<div data-field style="margin:0">
<select>
<option>EU West</option>
<option>US East</option>
<option>APAC</option>
</select>
</div>
</div>
</div>
</div>
</div>
import {
PrimusLayout, PrimusHeader,
PrimusInput, PrimusSelect, PrimusTextarea,
PrimusToggle, PrimusButton,
} from 'primus-react-ui';
import { useState } from 'react';
export function CreateTenantPage() {
const [form, setForm] = useState({
name: '', slug: '', domain: '', email: '',
plan: 'Pro', billing: 'Annual', seats: 50,
notes: '', active: true,
emailNotifs: true, mfaRequired: false, ssoOnly: false,
region: 'EU West',
});
const [errors, setErrors] = useState({});
const [loading, setLoading] = useState(false);
const set = (key, val) => setForm(f => ({ ...f, [key]: val }));
const handleSave = async () => {
setLoading(true);
try {
await createTenant(form);
navigate('/tenants');
} catch (e) {
setErrors(e.fields);
} finally {
setLoading(false);
}
};
return (
<PrimusLayout>
<PrimusHeader title="Create Tenant" subtitle="Fill in the details to onboard a new tenant">
<PrimusButton variant="outline" onClick={() => navigate(-1)}>Cancel</PrimusButton>
<PrimusButton loading={loading} onClick={handleSave}>Save Tenant</PrimusButton>
</PrimusHeader>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 300px', gap: '1rem', alignItems: 'start' }}>
<div className="vstack">
<div className="card" style={{ padding: '1.5rem' }}>
<h3>Basic Information</h3>
<PrimusInput label="Tenant name" required value={form.name} onChange={v => set('name', v)} />
<PrimusInput label="Admin email" type="email" required
value={form.email} onChange={v => set('email', v)} error={errors.email} />
<PrimusInput label="Domain" value={form.domain} onChange={v => set('domain', v)} />
</div>
<div className="card" style={{ padding: '1.5rem' }}>
<h3>Plan & Billing</h3>
<PrimusSelect label="Plan" value={form.plan} onChange={v => set('plan', v)}
options={['Starter','Pro','Enterprise'].map(v => ({ value: v, label: v }))} />
<PrimusInput label="Max seats" type="number"
value={form.seats} onChange={v => set('seats', Number(v))} />
</div>
<div className="card" style={{ padding: '1.5rem' }}>
<h3>Notes</h3>
<PrimusTextarea value={form.notes} onChange={v => set('notes', v)} rows={3}
placeholder="Internal notes about this tenant..." />
</div>
</div>
<div className="vstack">
<div className="card" style={{ padding: '1.25rem' }}>
<h3>Settings</h3>
<PrimusToggle label="Active" checked={form.active} onChange={v => set('active', v)} />
<PrimusToggle label="Email notifications" checked={form.emailNotifs} onChange={v => set('emailNotifs', v)} />
<PrimusToggle label="MFA required" checked={form.mfaRequired} onChange={v => set('mfaRequired', v)} />
<PrimusToggle label="SSO only" checked={form.ssoOnly} onChange={v => set('ssoOnly', v)} />
</div>
<div className="card" style={{ padding: '1.25rem' }}>
<h3>Region</h3>
<PrimusSelect label="" value={form.region} onChange={v => set('region', v)}
options={['EU West','US East','APAC'].map(v => ({ value: v, label: v }))} />
</div>
</div>
</div>
</PrimusLayout>
);
}
<primus-layout>
<primus-header title="Create Tenant" subtitle="Fill in the details to onboard a new tenant">
<div header-actions>
<primus-button variant="outline" (clicked)="onCancel()">Cancel</primus-button>
<primus-button [loading]="loading" (clicked)="onSave()">Save Tenant</primus-button>
</div>
</primus-header>
<div style="display:grid;grid-template-columns:1fr 300px;gap:1rem;align-items:start">
<div class="vstack">
<div class="card" style="padding:1.5rem">
<h3>Basic Information</h3>
<primus-input label="Tenant name" [required]="true"
[(value)]="form.name"></primus-input>
<primus-input label="Admin email" type="email" [required]="true"
[(value)]="form.email" [error]="errors.email"></primus-input>
<primus-input label="Domain"
[(value)]="form.domain"></primus-input>
</div>
<div class="card" style="padding:1.5rem">
<h3>Plan & Billing</h3>
<primus-select label="Plan"
[options]="planOptions" [(value)]="form.plan"></primus-select>
<primus-input label="Max seats" type="number"
[(value)]="form.seats"></primus-input>
</div>
<div class="card" style="padding:1.5rem">
<h3>Notes</h3>
<primus-textarea [rows]="3"
placeholder="Internal notes..."
[(value)]="form.notes"></primus-textarea>
</div>
</div>
<div class="vstack">
<div class="card" style="padding:1.25rem">
<h3>Settings</h3>
<primus-toggle label="Active" [(checked)]="form.active"></primus-toggle>
<primus-toggle label="Email notifications" [(checked)]="form.emailNotifs"></primus-toggle>
<primus-toggle label="MFA required" [(checked)]="form.mfaRequired"></primus-toggle>
<primus-toggle label="SSO only" [(checked)]="form.ssoOnly"></primus-toggle>
</div>
<div class="card" style="padding:1.25rem">
<h3>Region</h3>
<primus-select [options]="regionOptions" [(value)]="form.region"></primus-select>
</div>
</div>
</div>
</primus-layout>
Variations​
- Edit mode — Pre-populate all fields from the existing record. Change the header title to "Edit Tenant". Add a "Delete" button (danger, outline) in the header.
- Unsaved changes guard — Show a confirmation modal when navigating away with unsaved changes. Use the
Modalcomponent.