1use crossterm::event::KeyCode;
9use telex::prelude::*;
10use telex::Color;
11
12telex::require_api!(0, 2);
13
14fn main() {
15 telex::run_with_theme(App, telex::theme::Theme::nord()).unwrap();
16}
17
18struct App;
19
20impl Component for App {
21 fn render(&self, cx: Scope) -> View {
22 let show_help = state!(cx, || false);
23
24 cx.use_command(
26 KeyBinding::key(KeyCode::F(1)),
27 with!(show_help => move || show_help.update(|v| *v = !*v)),
28 );
29
30 let form = state!(cx, || {
32 FormState::new()
33 .field(
34 FieldBuilder::new("email")
35 .required()
36 .email()
37 .error_message("Please enter a valid email address")
38 .build(),
39 )
40 .field(
41 FieldBuilder::new("password")
42 .required()
43 .min_length(8)
44 .error_message("Password must be at least 8 characters")
45 .build(),
46 )
47 .field(
48 FieldBuilder::new("username")
49 .required()
50 .min_length(3)
51 .max_length(20)
52 .custom(|v| {
53 if v.contains(' ') {
54 Some("Username cannot contain spaces".to_string())
55 } else if !v.chars().all(|c| c.is_alphanumeric() || c == '_') {
56 Some(
57 "Username can only contain letters, numbers, and underscores"
58 .to_string(),
59 )
60 } else {
61 None
62 }
63 })
64 .build(),
65 )
66 .field(
67 FieldBuilder::new("age")
68 .integer()
69 .custom(|v| {
70 if v.is_empty() {
71 return None; }
73 match v.parse::<i32>() {
74 Ok(age) if age < 0 => Some("Age cannot be negative".to_string()),
75 Ok(age) if age > 150 => {
76 Some("Please enter a valid age".to_string())
77 }
78 Ok(_) => None,
79 Err(_) => Some("Please enter a valid number".to_string()),
80 }
81 })
82 .build(),
83 )
84 });
85
86 let submit_message = state!(cx, String::new);
87
88 let email = form.get().get_value("email");
90 let password = form.get().get_value("password");
91 let username = form.get().get_value("username");
92 let age = form.get().get_value("age");
93
94 let email_error = form.get().get_error("email");
95 let password_error = form.get().get_error("password");
96 let username_error = form.get().get_error("username");
97 let age_error = form.get().get_error("age");
98
99 let on_submit = with!(form, submit_message => move || {
101 if form.get().validate() {
102 let values = form.get().values();
103 submit_message.set(format!(
104 "Form submitted! Email: {}, Username: {}",
105 values.get("email").unwrap_or(&String::new()),
106 values.get("username").unwrap_or(&String::new())
107 ));
108 } else {
109 submit_message.set("Please fix the errors above".to_string());
110 }
111 });
112
113 let on_reset = with!(form, submit_message => move || {
114 form.get().reset();
115 submit_message.set(String::new());
116 });
117
118 View::vstack()
119 .spacing(1)
120 .child(
121 View::boxed()
123 .border(true)
124 .padding(1)
125 .child(
126 View::vstack()
127 .child(View::styled_text("Form Validation Demo").bold().build())
128 .child(
129 View::styled_text("Tab between fields, type to enter values")
130 .dim()
131 .build(),
132 )
133 .build(),
134 )
135 .build(),
136 )
137 .child(
138 View::boxed()
140 .flex(1)
141 .border(true)
142 .padding(1)
143 .child(
144 View::form()
145 .spacing(1)
146 .child(
147 View::form_field("email")
148 .label("Email Address *")
149 .value(email.clone())
150 .placeholder("you@example.com")
151 .error(email_error)
152 .on_change(with!(form => move |v: String| {
153 form.get().set_value("email", v);
154 }))
155 .on_blur(with!(form => move || {
156 form.get().touch("email");
157 }))
158 .build(),
159 )
160 .child(
161 View::form_field("username")
162 .label("Username *")
163 .value(username.clone())
164 .placeholder("johndoe")
165 .error(username_error)
166 .on_change(with!(form => move |v: String| {
167 form.get().set_value("username", v);
168 }))
169 .on_blur(with!(form => move || {
170 form.get().touch("username");
171 }))
172 .build(),
173 )
174 .child(
175 View::form_field("password")
176 .label("Password * (min 8 chars)")
177 .value(password.clone())
178 .placeholder("Enter password")
179 .password(true)
180 .error(password_error)
181 .on_change(with!(form => move |v: String| {
182 form.get().set_value("password", v);
183 }))
184 .on_blur(with!(form => move || {
185 form.get().touch("password");
186 }))
187 .build(),
188 )
189 .child(
190 View::form_field("age")
191 .label("Age (optional)")
192 .value(age.clone())
193 .placeholder("25")
194 .error(age_error)
195 .on_change(with!(form => move |v: String| {
196 form.get().set_value("age", v);
197 }))
198 .on_blur(with!(form => move || {
199 form.get().touch("age");
200 }))
201 .build(),
202 )
203 .build(),
204 )
205 .build(),
206 )
207 .child(
208 View::hstack()
210 .spacing(2)
211 .child(View::button().label("Submit").on_press(on_submit).build())
212 .child(View::button().label("Reset").on_press(on_reset).build())
213 .build(),
214 )
215 .child(
216 View::boxed()
218 .border(true)
219 .padding(1)
220 .child(
221 View::vstack()
222 .child(View::text(if submit_message.get().is_empty() {
223 "Fill in the form and click Submit".to_string()
224 } else {
225 submit_message.get()
226 }))
227 .child(
228 View::styled_text(format!(
229 "Form valid: {}",
230 if form.get().is_valid() { "Yes" } else { "No" }
231 ))
232 .color(if form.get().is_valid() {
233 Color::Green
234 } else {
235 Color::Red
236 })
237 .build(),
238 )
239 .build(),
240 )
241 .build(),
242 )
243 .child(View::styled_text("F1 help • Ctrl+Q quit").dim().build())
244 .child(
245 View::modal()
246 .visible(show_help.get())
247 .title("Example 22: Forms")
248 .on_dismiss(with!(show_help => move || show_help.set(false)))
249 .child(
250 View::vstack()
251 .child(View::styled_text("What you're seeing").bold().build())
252 .child(View::text("• Declarative form validation"))
253 .child(View::text("• Required, email, length validators"))
254 .child(View::text("• Custom validation functions"))
255 .child(View::gap(1))
256 .child(View::styled_text("Key concepts").bold().build())
257 .child(View::text("• FormState manages all fields"))
258 .child(View::text("• FieldBuilder defines validation"))
259 .child(View::text("• View::form_field() renders inputs"))
260 .child(View::text("• on_blur triggers validation"))
261 .child(View::gap(1))
262 .child(View::styled_text("Try this").bold().build())
263 .child(View::text("• Enter invalid email, see error"))
264 .child(View::text("• Try short password (<8 chars)"))
265 .child(View::text("• Username with spaces shows error"))
266 .child(View::gap(1))
267 .child(View::styled_text("Next up").bold().build())
268 .child(View::text("→ 23_modal: modal dialogs"))
269 .child(View::gap(1))
270 .child(View::styled_text("Press Escape to close").dim().build())
271 .build(),
272 )
273 .build(),
274 )
275 .build()
276 }
277}