Settings
Tabs, switches and buttons working together, the same screen in every framework.
// Vanilla: the create* factories the elements are built on.
import createButton from "mtrl/components/button";
import createSwitch from "mtrl/components/switch";
import createTabs from "mtrl/components/tabs";
import { DEFAULTS, summary, type Settings } from "./shared";
const s: Settings = { ...DEFAULTS };
const app = document.getElementById("app")!;
const section = Object.assign(document.createElement("section"), { className: "settings" });
const panel = (): HTMLDivElement => Object.assign(document.createElement("div"), { className: "settings__panel" });
const status = Object.assign(document.createElement("output"), { className: "settings__status" });
status.setAttribute("role", "status");
const tabs = createTabs({
tabs: [
{ text: "Connectivity", value: "connectivity", state: "active" },
{ text: "Notifications", value: "notifications" },
],
});
const connectivity = panel();
const notifications = panel();
notifications.hidden = true;
tabs.on("change", ({ value }: { value: string }) => {
connectivity.hidden = value !== "connectivity";
notifications.hidden = value !== "notifications";
});
const make = (key: keyof Settings, label: string, supportingText?: string) => {
const control = createSwitch({ label, checked: Boolean(s[key]), supportingText });
control.on("change", ({ checked }) => {
(s as Record<string, unknown>)[key] = checked;
if (key === "airplane") applyAirplane();
});
return control;
};
const wifi = make("wifi", "Wi-Fi");
const bluetooth = make("bluetooth", "Bluetooth");
const airplane = make("airplane", "Airplane mode", "Turns off Wi-Fi and Bluetooth");
const allow = make("notifications", "Allow notifications");
const sounds = make("sounds", "Sounds");
const applyAirplane = (): void => {
for (const control of [wifi, bluetooth]) s.airplane ? control.disable() : control.enable();
};
connectivity.append(wifi.element, bluetooth.element, airplane.element);
notifications.append(allow.element, sounds.element);
const actions = Object.assign(document.createElement("footer"), { className: "settings__actions" });
const reset = createButton({ text: "Reset", variant: "outlined" });
const save = createButton({ text: "Save" });
reset.on("click", () => {
Object.assign(s, { ...DEFAULTS, tab: s.tab });
wifi.setValue(s.wifi); bluetooth.setValue(s.bluetooth); airplane.setValue(s.airplane);
allow.setValue(s.notifications); sounds.setValue(s.sounds);
applyAirplane();
status.textContent = "Reset to the defaults.";
});
save.on("click", () => (status.textContent = summary(s)));
actions.append(reset.element, save.element);
section.append(tabs.element, connectivity, notifications, actions, status);
app.append(section);
// The state every variant of this example shares, and how it is described.
export interface Settings {
tab: string;
wifi: boolean;
bluetooth: boolean;
airplane: boolean;
notifications: boolean;
sounds: boolean;
}
export const DEFAULTS: Settings = {
tab: "connectivity",
wifi: true,
bluetooth: false,
airplane: false,
notifications: true,
sounds: false,
};
const on = (value: boolean): string => (value ? "on" : "off");
export const summary = (s: Settings): string =>
`Saved: Wi-Fi ${on(s.wifi)}, Bluetooth ${on(s.bluetooth)}, airplane mode ${on(s.airplane)}, ` +
`notifications ${on(s.notifications)}, sounds ${on(s.sounds)}.`;
// Web components: the elements themselves, no framework.
import "mtrl/elements/css";
import { defineAll } from "mtrl/elements";
import { DEFAULTS, summary } from "./shared";
defineAll();
const app = document.getElementById("app")!;
app.innerHTML = `
<section class="settings">
<m-tabs id="tabs" value="connectivity">
<m-tab value="connectivity">Connectivity</m-tab>
<m-tab value="notifications">Notifications</m-tab>
</m-tabs>
<div class="settings__panel" id="connectivity">
<m-switch id="wifi" checked>Wi-Fi</m-switch>
<m-switch id="bluetooth">Bluetooth</m-switch>
<m-switch id="airplane" supporting-text="Turns off Wi-Fi and Bluetooth">Airplane mode</m-switch>
</div>
<div class="settings__panel" id="notifications" hidden>
<m-switch id="notifications-switch" checked>Allow notifications</m-switch>
<m-switch id="sounds">Sounds</m-switch>
</div>
<footer class="settings__actions">
<m-button id="reset" variant="outlined">Reset</m-button>
<m-button id="save">Save</m-button>
</footer>
<output class="settings__status" role="status"></output>
</section>`;
type SwitchElement = HTMLElement & { checked: boolean; disabled: boolean };
const $ = <T extends HTMLElement = SwitchElement>(id: string) => document.getElementById(id) as T;
const tabs = $<HTMLElement & { value: string }>("tabs");
const status = app.querySelector("output")!;
const switches = {
wifi: $("wifi"), bluetooth: $("bluetooth"), airplane: $("airplane"),
notifications: $("notifications-switch"), sounds: $("sounds"),
};
const showTab = (tab: string): void => {
$("connectivity").hidden = tab !== "connectivity";
$("notifications").hidden = tab !== "notifications";
};
const applyAirplane = (): void => {
switches.wifi.disabled = switches.bluetooth.disabled = switches.airplane.checked;
};
tabs.addEventListener("change", (e) => showTab((e as CustomEvent<{ value: string }>).detail.value));
switches.airplane.addEventListener("change", applyAirplane);
$("save").addEventListener("click", () => {
status.textContent = summary({
tab: tabs.value,
wifi: switches.wifi.checked, bluetooth: switches.bluetooth.checked, airplane: switches.airplane.checked,
notifications: switches.notifications.checked, sounds: switches.sounds.checked,
});
});
$("reset").addEventListener("click", () => {
switches.wifi.checked = DEFAULTS.wifi;
switches.bluetooth.checked = DEFAULTS.bluetooth;
switches.airplane.checked = DEFAULTS.airplane;
switches.notifications.checked = DEFAULTS.notifications;
switches.sounds.checked = DEFAULTS.sounds;
applyAirplane();
status.textContent = "Reset to the defaults.";
});
// The state every variant of this example shares, and how it is described.
export interface Settings {
tab: string;
wifi: boolean;
bluetooth: boolean;
airplane: boolean;
notifications: boolean;
sounds: boolean;
}
export const DEFAULTS: Settings = {
tab: "connectivity",
wifi: true,
bluetooth: false,
airplane: false,
notifications: true,
sounds: false,
};
const on = (value: boolean): string => (value ? "on" : "off");
export const summary = (s: Settings): string =>
`Saved: Wi-Fi ${on(s.wifi)}, Bluetooth ${on(s.bluetooth)}, airplane mode ${on(s.airplane)}, ` +
`notifications ${on(s.notifications)}, sounds ${on(s.sounds)}.`;
// React: mtrl/react wraps the same elements.
import { useState } from "react";
import { Button, Switch, Tab, Tabs } from "mtrl/react";
import { DEFAULTS, summary, type Settings as State } from "./shared";
export default function Settings() {
const [s, setS] = useState<State>(DEFAULTS);
const [status, setStatus] = useState("");
const toggle = (key: keyof State) => (e: CustomEvent<{ checked: boolean }>) =>
setS((prev) => ({ ...prev, [key]: e.detail.checked }));
return (
<section className="settings">
<Tabs value={s.tab} onChange={(e) => setS((prev) => ({ ...prev, tab: e.detail.value ?? prev.tab }))}>
<Tab value="connectivity">Connectivity</Tab>
<Tab value="notifications">Notifications</Tab>
</Tabs>
<div className="settings__panel" hidden={s.tab !== "connectivity"}>
<Switch checked={s.wifi} disabled={s.airplane} onChange={toggle("wifi")}>Wi-Fi</Switch>
<Switch checked={s.bluetooth} disabled={s.airplane} onChange={toggle("bluetooth")}>Bluetooth</Switch>
<Switch checked={s.airplane} supportingText="Turns off Wi-Fi and Bluetooth" onChange={toggle("airplane")}>
Airplane mode
</Switch>
</div>
<div className="settings__panel" hidden={s.tab !== "notifications"}>
<Switch checked={s.notifications} onChange={toggle("notifications")}>Allow notifications</Switch>
<Switch checked={s.sounds} onChange={toggle("sounds")}>Sounds</Switch>
</div>
<footer className="settings__actions">
<Button variant="outlined" onClick={() => { setS((prev) => ({ ...DEFAULTS, tab: prev.tab })); setStatus("Reset to the defaults."); }}>
Reset
</Button>
<Button onClick={() => setStatus(summary(s))}>Save</Button>
</footer>
<output className="settings__status" role="status">{status}</output>
</section>
);
}
// The state every variant of this example shares, and how it is described.
export interface Settings {
tab: string;
wifi: boolean;
bluetooth: boolean;
airplane: boolean;
notifications: boolean;
sounds: boolean;
}
export const DEFAULTS: Settings = {
tab: "connectivity",
wifi: true,
bluetooth: false,
airplane: false,
notifications: true,
sounds: false,
};
const on = (value: boolean): string => (value ? "on" : "off");
export const summary = (s: Settings): string =>
`Saved: Wi-Fi ${on(s.wifi)}, Bluetooth ${on(s.bluetooth)}, airplane mode ${on(s.airplane)}, ` +
`notifications ${on(s.notifications)}, sounds ${on(s.sounds)}.`;
// Vue: mtrl/vue wraps the same elements; v-model binds their live state.
import { defineComponent, reactive, ref } from "vue";
import { MButton, MSwitch, MTab, MTabs } from "mtrl/vue";
import { DEFAULTS, summary } from "./shared";
export default defineComponent({
components: { MButton, MSwitch, MTab, MTabs },
setup() {
const s = reactive({ ...DEFAULTS });
const status = ref("");
const save = () => (status.value = summary(s));
const reset = () => {
Object.assign(s, { ...DEFAULTS, tab: s.tab });
status.value = "Reset to the defaults.";
};
return { s, status, save, reset };
},
template: `
<section class="settings">
<MTabs v-model="s.tab">
<MTab value="connectivity">Connectivity</MTab>
<MTab value="notifications">Notifications</MTab>
</MTabs>
<div class="settings__panel" :hidden="s.tab !== 'connectivity'">
<MSwitch v-model="s.wifi" :disabled="s.airplane">Wi-Fi</MSwitch>
<MSwitch v-model="s.bluetooth" :disabled="s.airplane">Bluetooth</MSwitch>
<MSwitch v-model="s.airplane" supporting-text="Turns off Wi-Fi and Bluetooth">Airplane mode</MSwitch>
</div>
<div class="settings__panel" :hidden="s.tab !== 'notifications'">
<MSwitch v-model="s.notifications">Allow notifications</MSwitch>
<MSwitch v-model="s.sounds">Sounds</MSwitch>
</div>
<footer class="settings__actions">
<MButton variant="outlined" @click="reset">Reset</MButton>
<MButton @click="save">Save</MButton>
</footer>
<output class="settings__status" role="status">{{ status }}</output>
</section>`,
});
// The state every variant of this example shares, and how it is described.
export interface Settings {
tab: string;
wifi: boolean;
bluetooth: boolean;
airplane: boolean;
notifications: boolean;
sounds: boolean;
}
export const DEFAULTS: Settings = {
tab: "connectivity",
wifi: true,
bluetooth: false,
airplane: false,
notifications: true,
sounds: false,
};
const on = (value: boolean): string => (value ? "on" : "off");
export const summary = (s: Settings): string =>
`Saved: Wi-Fi ${on(s.wifi)}, Bluetooth ${on(s.bluetooth)}, airplane mode ${on(s.airplane)}, ` +
`notifications ${on(s.notifications)}, sounds ${on(s.sounds)}.`;
<!-- Svelte: mtrl/svelte wraps the same elements; bind: keeps their live state. -->
<script lang="ts">
import { Button, Switch, Tab, Tabs } from "mtrl/svelte";
import { DEFAULTS, summary } from "./shared";
let s = $state({ ...DEFAULTS });
let status = $state("");
const reset = () => {
s = { ...DEFAULTS, tab: s.tab };
status = "Reset to the defaults.";
};
</script>
<section class="settings">
<Tabs bind:value={s.tab}>
<Tab value="connectivity">Connectivity</Tab>
<Tab value="notifications">Notifications</Tab>
</Tabs>
<div class="settings__panel" hidden={s.tab !== "connectivity"}>
<Switch bind:checked={s.wifi} disabled={s.airplane}>Wi-Fi</Switch>
<Switch bind:checked={s.bluetooth} disabled={s.airplane}>Bluetooth</Switch>
<Switch bind:checked={s.airplane} supportingText="Turns off Wi-Fi and Bluetooth">Airplane mode</Switch>
</div>
<div class="settings__panel" hidden={s.tab !== "notifications"}>
<Switch bind:checked={s.notifications}>Allow notifications</Switch>
<Switch bind:checked={s.sounds}>Sounds</Switch>
</div>
<footer class="settings__actions">
<Button variant="outlined" onclick={reset}>Reset</Button>
<Button onclick={() => (status = summary(s))}>Save</Button>
</footer>
<output class="settings__status" role="status">{status}</output>
</section>
// The state every variant of this example shares, and how it is described.
export interface Settings {
tab: string;
wifi: boolean;
bluetooth: boolean;
airplane: boolean;
notifications: boolean;
sounds: boolean;
}
export const DEFAULTS: Settings = {
tab: "connectivity",
wifi: true,
bluetooth: false,
airplane: false,
notifications: true,
sounds: false,
};
const on = (value: boolean): string => (value ? "on" : "off");
export const summary = (s: Settings): string =>
`Saved: Wi-Fi ${on(s.wifi)}, Bluetooth ${on(s.bluetooth)}, airplane mode ${on(s.airplane)}, ` +
`notifications ${on(s.notifications)}, sounds ${on(s.sounds)}.`;
// Solid: mtrl/solid wraps the same elements.
import { createSignal } from "solid-js";
import { createStore } from "solid-js/store";
import { Button, Switch, Tab, Tabs } from "mtrl/solid";
import { DEFAULTS, summary, type Settings as State } from "./shared";
export default function Settings() {
const [s, setS] = createStore<State>({ ...DEFAULTS });
const [status, setStatus] = createSignal("");
const toggle = (key: keyof State) => (e: CustomEvent<{ checked: boolean }>) => setS(key, e.detail.checked);
return (
<section class="settings">
<Tabs value={s.tab} onChange={(e) => setS("tab", e.detail.value ?? s.tab)}>
<Tab value="connectivity">Connectivity</Tab>
<Tab value="notifications">Notifications</Tab>
</Tabs>
<div class="settings__panel" hidden={s.tab !== "connectivity"}>
<Switch checked={s.wifi} disabled={s.airplane} onChange={toggle("wifi")}>Wi-Fi</Switch>
<Switch checked={s.bluetooth} disabled={s.airplane} onChange={toggle("bluetooth")}>Bluetooth</Switch>
<Switch checked={s.airplane} supportingText="Turns off Wi-Fi and Bluetooth" onChange={toggle("airplane")}>
Airplane mode
</Switch>
</div>
<div class="settings__panel" hidden={s.tab !== "notifications"}>
<Switch checked={s.notifications} onChange={toggle("notifications")}>Allow notifications</Switch>
<Switch checked={s.sounds} onChange={toggle("sounds")}>Sounds</Switch>
</div>
<footer class="settings__actions">
<Button variant="outlined" onClick={() => { setS({ ...DEFAULTS, tab: s.tab }); setStatus("Reset to the defaults."); }}>
Reset
</Button>
<Button onClick={() => setStatus(summary(s))}>Save</Button>
</footer>
<output class="settings__status" role="status">{status()}</output>
</section>
);
}
// The state every variant of this example shares, and how it is described.
export interface Settings {
tab: string;
wifi: boolean;
bluetooth: boolean;
airplane: boolean;
notifications: boolean;
sounds: boolean;
}
export const DEFAULTS: Settings = {
tab: "connectivity",
wifi: true,
bluetooth: false,
airplane: false,
notifications: true,
sounds: false,
};
const on = (value: boolean): string => (value ? "on" : "off");
export const summary = (s: Settings): string =>
`Saved: Wi-Fi ${on(s.wifi)}, Bluetooth ${on(s.bluetooth)}, airplane mode ${on(s.airplane)}, ` +
`notifications ${on(s.notifications)}, sounds ${on(s.sounds)}.`;