Skip to main content

tern_sdk/ui/
html.rs

1//! `el` builders, one per allowed tag.
2//!
3//! Plain HTML elements a program styles with its own sheet, and checkbox
4//! and radio controls whose flips come back as `change` events; a form's
5//! values ride on every event from inside it.
6//!
7//! # Example
8//! ```
9//! use tern_sdk::ui::{Actions, Node, html};
10//!
11//! let form: Node = html::form()
12//! 	.class("ask")
13//! 	.child(html::p("Which size?"))
14//! 	.child(
15//! 		html::label()
16//! 			.child(html::radio("size", "s"))
17//! 			.child(html::span("Small")),
18//! 	)
19//! 	.child(html::button("Create").actions(Actions::new().click("submit")))
20//! 	.into();
21//! assert_eq!(form.child_nodes().len(), 3);
22//! ```
23
24use serde::Serialize;
25use serde_json::{Map, Value};
26
27use super::{
28	InputType, Node,
29	node::{
30		actions_methods, base_methods, children_methods, event_methods, kind_type, max_method, props,
31		title_method,
32	},
33};
34
35kind_type!(
36	/// `el`: one HTML element of an allowed tag, its `text`, then its
37	/// children.
38	El
39);
40
41impl<M> El<M> {
42	base_methods!();
43
44	title_method!();
45
46	max_method!();
47
48	actions_methods!();
49
50	event_methods!();
51
52	children_methods!();
53
54	props! {
55		/// The element's tag.
56		tag(String) = "tag";
57		/// Space-separated class names for your stylesheets.
58		class(String) = "class";
59		/// Plain text drawn before the children.
60		text(String) = "text";
61		/// `input` only: checkbox or radio.
62		input_type(InputType) = "type";
63		/// `input` only: the name in its form's values and its radio group.
64		name(String) = "name";
65		/// `input` only: the value it reports (default `"on"`).
66		value(String) = "value";
67		/// `input` only: the checked state you set.
68		checked(bool) = "checked";
69		/// `input` only: dimmed, never flips, left out of values.
70		disabled(bool) = "disabled";
71	}
72
73	/// Sets all extra attributes (`data-*`, `aria-*`, `role`, `colspan`,
74	/// `rowspan`).
75	pub fn attrs(mut self, attrs: Map<String, Value>) -> Self {
76		self.0.props.insert("attrs".into(), Value::Object(attrs));
77		self
78	}
79
80	/// Sets one extra attribute (a string, number or boolean).
81	pub fn attr(mut self, name: impl Into<String>, value: impl Serialize) -> Self {
82		let Ok(value) = serde_json::to_value(value) else {
83			return self;
84		};
85		if let Some(Value::Object(attrs)) = self.0.props.get_mut("attrs") {
86			attrs.insert(name.into(), value);
87		} else {
88			let mut attrs = Map::new();
89			attrs.insert(name.into(), value);
90			self.0.props.insert("attrs".into(), Value::Object(attrs));
91		}
92		self
93	}
94}
95
96/// An `el` of any tag.
97pub fn el<M>(tag: impl Into<String>) -> El<M> {
98	El(Node::new("el")).tag(tag)
99}
100
101/// Defines builders for tags without text.
102macro_rules! bare_tags {
103	($($(#[$m:meta])* $f:ident = $tag:literal;)*) => {
104		$($(#[$m])* pub fn $f<M>() -> El<M> {
105			el($tag)
106		})*
107	};
108}
109
110/// Defines builders for tags that take their text.
111macro_rules! text_tags {
112	($($(#[$m:meta])* $f:ident = $tag:literal;)*) => {
113		$($(#[$m])* pub fn $f<M>(text: impl Into<String>) -> El<M> {
114			el($tag).text(text)
115		})*
116	};
117}
118
119bare_tags! {
120	/// `div`: a block.
121	div = "div";
122	/// `section`.
123	section = "section";
124	/// `header`.
125	header = "header";
126	/// `footer`.
127	footer = "footer";
128	/// `nav`.
129	nav = "nav";
130	/// `aside`.
131	aside = "aside";
132	/// `main`.
133	main = "main";
134	/// `article`.
135	article = "article";
136	/// `figure`.
137	figure = "figure";
138	/// `blockquote`.
139	blockquote = "blockquote";
140	/// `ul`: a bulleted list.
141	ul = "ul";
142	/// `ol`: a numbered list.
143	ol = "ol";
144	/// `dl`: a description list.
145	dl = "dl";
146	/// `hr`: a rule (no children).
147	hr = "hr";
148	/// `table`.
149	table = "table";
150	/// `thead`.
151	thead = "thead";
152	/// `tbody`.
153	tbody = "tbody";
154	/// `tr`: a table row.
155	tr = "tr";
156	/// `label`: names the controls inside it; a click flips the first.
157	label = "label";
158	/// `form`: groups controls; events from inside carry its values.
159	form = "form";
160}
161
162text_tags! {
163	/// `span`: inline text.
164	span = "span";
165	/// `p`: a paragraph.
166	p = "p";
167	/// `li`: a list item.
168	li = "li";
169	/// `dt`: a term.
170	dt = "dt";
171	/// `dd`: a definition.
172	dd = "dd";
173	/// `h1`.
174	h1 = "h1";
175	/// `h2`.
176	h2 = "h2";
177	/// `h3`.
178	h3 = "h3";
179	/// `h4`.
180	h4 = "h4";
181	/// `pre`: preformatted text.
182	pre = "pre";
183	/// `code`: inline code.
184	code = "code";
185	/// `kbd`: a key.
186	kbd = "kbd";
187	/// `strong`.
188	strong = "strong";
189	/// `b`.
190	b = "b";
191	/// `em`.
192	em = "em";
193	/// `i`.
194	i = "i";
195	/// `del`: deleted text.
196	del = "del";
197	/// `mark`: highlighted text.
198	mark = "mark";
199	/// `th`: a header cell.
200	th = "th";
201	/// `td`: a cell.
202	td = "td";
203	/// `button`: give it `actions` (or `on_click`) to make it do something.
204	button = "button";
205}
206
207/// `input`: a control of `kind`.
208pub fn input<M>(kind: impl Into<InputType>) -> El<M> {
209	el("input").input_type(kind)
210}
211
212/// A checkbox named `name` reporting `value` (a lone box reports
213/// `true`/`false` in its form's values).
214pub fn checkbox<M>(name: impl Into<String>, value: impl Into<String>) -> El<M> {
215	input(InputType::Checkbox).name(name).value(value)
216}
217
218/// A radio of group `name` reporting `value`.
219pub fn radio<M>(name: impl Into<String>, value: impl Into<String>) -> El<M> {
220	input(InputType::Radio).name(name).value(value)
221}