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