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