tablo_core/schema/fields/
custom.rs1use topcoat::{context::Cx, view::*};
5
6pub trait Control: Send + Sync {
26 fn render<'a>(&self, cx: &'a Cx, input: ControlInput) -> BoxView<'a>;
27
28 fn display<'a>(&self, cx: &'a Cx, value: &str) -> BoxView<'a> {
30 let text = value.to_string();
31 view! {
32 cx =>
33 <div class="text-sm text-foreground wrap-anywhere whitespace-pre-wrap">
34 (text)
35 </div>
36 }
37 .boxed()
38 }
39}
40
41#[derive(Debug, Clone)]
44pub struct ControlInput {
45 name: String,
46 value: Option<String>,
47 required: bool,
48 invalid: bool,
49 described_by: Option<String>,
50}
51
52impl ControlInput {
53 pub(crate) fn new(
54 name: &str,
55 value: Option<&str>,
56 required: bool,
57 invalid: bool,
58 described_by: Option<String>,
59 ) -> Self {
60 Self {
61 name: name.to_string(),
62 value: value.map(str::to_string),
63 required,
64 invalid,
65 described_by,
66 }
67 }
68
69 pub fn name(&self) -> &str {
70 &self.name
71 }
72
73 pub fn value(&self) -> Option<&str> {
74 self.value.as_deref()
75 }
76
77 pub fn required(&self) -> bool {
78 self.required
79 }
80
81 pub fn invalid(&self) -> bool {
82 self.invalid
83 }
84
85 pub fn described_by(&self) -> Option<&str> {
86 self.described_by.as_deref()
87 }
88
89 pub fn attributes(&self, cx: &Cx) -> Attributes {
90 let required = self.required;
91 attributes! {
92 cx =>
93 id=(self.name.clone())
94 name=(self.name.clone())
95 value=(self.value.clone())
96 required=(required)
97 aria-required=(required.then_some("true"))
98 aria-invalid=(if self.invalid { "true" } else { "false" })
99 aria-describedby=(self.described_by.clone())
100 }
101 }
102}
103
104#[derive(Debug, Clone, Copy, Default)]
106pub struct Toggle;
107
108impl Control for Toggle {
109 fn render<'a>(&self, cx: &'a Cx, input: ControlInput) -> BoxView<'a> {
110 let name = input.name().to_string();
111 let checked = input.value().is_some_and(|v| v.trim() == "true");
112 let invalid = if input.invalid() { "true" } else { "false" };
113 let described_by = input.described_by().map(str::to_string);
114 view! {
115 cx =>
116 <input type="hidden" name=(name.clone()) value="false">
117 <input
118 type="checkbox"
119 id=(name.clone())
120 name=(name)
121 value="true"
122 checked=(checked)
123 aria-invalid=(invalid)
124 aria-describedby=(described_by)
125 class="size-4 accent-primary"
126 >
127 }
128 .boxed()
129 }
130
131 fn display<'a>(&self, cx: &'a Cx, value: &str) -> BoxView<'a> {
132 let text = if value.trim() == "true" { "Yes" } else { "No" };
133 view! { cx => <div class="text-sm text-foreground">(text)</div> }.boxed()
134 }
135}