Compare commits

..

2 Commits

Author SHA1 Message Date
edc9c4d371 form now has a style for the entire form 2024-06-07 14:42:47 -05:00
31b24a23a1 impl groups 2024-06-07 14:21:14 -05:00
5 changed files with 211 additions and 140 deletions

View File

@ -1,113 +1,127 @@
// form component as a grid
.form_grid {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
row-gap: 1.5rem;
column-gap: 1rem;
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
row-gap: 1.5rem;
column-gap: 1rem;
}
// size up to 12 columns on small or bigger devices
@media (min-width: 640px) {
.form_grid {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.form_grid {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
}
.form_heading {
font-size: 1.875rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
text-align: center;
font-weight: bold;
color: #374151;
border-bottom: 2px solid #374151;
margin-bottom: 2rem;
grid-column: span 12;
font-size: 1.875rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
text-align: center;
font-weight: bold;
color: #374151;
border-bottom: 2px solid #374151;
margin-bottom: 2rem;
grid-column: span 12;
display: grid;
}
.form_group {
background-color: #0295f744;
border-radius: 25px;
padding: 20px;
}
.form_label {
font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
text-transform: uppercase;
letter-spacing: 0.05em;
text-align: left;
color: #4a5568;
font-size: 1rem;
font-weight: bold;
margin-left: 0.5rem;
margin-bottom: 0.25rem;
display: inline;
font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
text-transform: uppercase;
letter-spacing: 0.05em;
text-align: left;
color: #4a5568;
font-size: 1rem;
font-weight: bold;
margin-left: 0.5rem;
margin-bottom: 0.25rem;
display: inline;
}
.form_input {
display: block;
box-sizing: border-box;
width: 100%;
background-color: #f7fafc;
border-width: 2px;
border-color: #e2e8f0;
border-style: solid;
color: #4a5568;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 1rem;
padding-right: 1rem;
border-radius: 0.375rem;
line-height: 1.25;
outline: none;
display: block;
box-sizing: border-box;
width: 100%;
background-color: #f7fafc;
border-width: 2px;
border-color: #e2e8f0;
border-style: solid;
color: #4a5568;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 1rem;
padding-right: 1rem;
border-radius: 0.375rem;
line-height: 1.25;
outline: none;
}
.form_input:focus {
background-color: #ffffff;
border-color: #90cdf4;
background-color: #ffffff;
border-color: #90cdf4;
}
.form_input_invalid {
border: 2px solid #ef4444;
background-color: #ffd4d4;
border: 2px solid #ef4444;
background-color: #ffd4d4;
}
.form_error {
display: inline;
padding-left: 0.25rem;
color: #ef4444;
display: inline;
padding-left: 0.25rem;
color: #ef4444;
}
.form_button {
display: block;
outline: none;
border: none;
/* width: 100%; */
border-radius: 1rem;
background-color: #0477d6;
color: #fff;
font-weight: bold;
cursor: pointer;
margin: 0 auto;
padding: 0.75rem 1.25rem;
font-size: 1rem;
appearance: none;
min-height: 40px;
display: block;
outline: none;
border: none;
/* width: 100%; */
border-radius: 1rem;
background-color: #0477d6;
color: #fff;
font-weight: bold;
cursor: pointer;
margin: 0 auto auto auto;
padding: 0.75rem 1.25rem;
font-size: 1rem;
appearance: none;
min-height: 40px;
}
.form_submit {
@extend .form_button;
@extend .col-span-full;
margin: 0 auto;
}
.form_submit:hover {
background-color: #005fb3;
background-color: #005fb3;
}
// column widths
.col-span-full {
grid-column: 1 / -1;
grid-column: 1 / -1;
}
.col-span-12 {
grid-column: span 12 / span 12;
grid-column: span 12 / span 12;
}
.col-span-6 {
grid-column: span 6 / span 6;
grid-column: span 6 / span 6;
}
.col-span-4 {
grid-column: span 4 / span 4;
grid-column: span 4 / span 4;
}
.col-span-3 {
grid-column: span 3 / span 3;
grid-column: span 3 / span 3;
}
.col-span-2 {
grid-column: span 2 / span 2;
grid-column: span 2 / span 2;
}
.col-span-1 {
grid-column: span 1 / span 1;
grid-column: span 1 / span 1;
}

View File

@ -1,36 +1,38 @@
use super::{ControlRenderData, VanityControlBuilder, VanityControlData};
use super::ValidationCb;
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::{Signal, View};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct GroupData {
pub(crate) title: Option<String>,
}
impl VanityControlData for GroupData {
fn build_control<FS: FormStyle>(
fs: &FS,
control: ControlRenderData<FS, Self>,
_value_getter: Option<Signal<String>>,
) -> View {
fs.group(control)
}
}
use leptos::{CollectView, RwSignal};
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
pub fn group(
self,
builder: impl Fn(
VanityControlBuilder<FD, FS, GroupData>,
) -> VanityControlBuilder<FD, FS, GroupData>,
) -> Self {
self.new_vanity(builder)
}
}
pub fn group(mut self, builder: impl Fn(FormBuilder<FD, FS>) -> FormBuilder<FD, FS>) -> Self {
let mut group_builder = FormBuilder::new_group(self.fd, self.fs);
group_builder = builder(group_builder);
self.fs = group_builder.fs; // take the style back
impl<FD: FormToolData, FS: FormStyle> VanityControlBuilder<FD, FS, GroupData> {
pub fn title(mut self, title: impl ToString) -> Self {
self.data.title = Some(title.to_string());
for validation in group_builder.validations {
self.validations.push(validation);
}
let render_fn = move |fs: &FS, fd: RwSignal<FD>| {
let (views, validation_cbs): (Vec<_>, Vec<_>) = group_builder
.render_fns
.into_iter()
.map(|r_fn| r_fn(&fs, fd))
.unzip();
let view = fs.group(views.collect_view(), group_builder.styles);
let validation_cb = move || {
let mut success = true;
for validation in validation_cbs.iter().flatten() {
if !validation() {
success = false;
}
}
success
};
(view, Some(Box::new(validation_cb) as Box<dyn ValidationCb>))
};
self.render_fns.push(Box::new(render_fn));
self
}
}

View File

@ -23,11 +23,13 @@ pub struct FormBuilder<FD: FormToolData, FS: FormStyle> {
/// The [`ToolFormData`] signal.
pub(crate) fd: RwSignal<FD>,
/// The [`FormStyle`].
fs: FS,
pub(crate) fs: FS,
/// The list of [`ValidationFn`]s.
validations: Vec<Rc<dyn ValidationFn<FD>>>,
pub(crate) validations: Vec<Rc<dyn ValidationFn<FD>>>,
/// The list of functions that will render the form.
render_fns: Vec<Box<dyn RenderFn<FS, FD>>>,
pub(crate) render_fns: Vec<Box<dyn RenderFn<FS, FD>>>,
/// The list of styling attributes applied on the form level
pub(crate) styles: Vec<FS::StylingAttributes>,
}
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
@ -39,9 +41,25 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
fs: form_style,
validations: Vec::new(),
render_fns: Vec::new(),
styles: Vec::new(),
}
}
pub(crate) fn new_group(fd: RwSignal<FD>, fs: FS) -> FormBuilder<FD, FS> {
FormBuilder {
fd,
fs,
validations: Vec::new(),
render_fns: Vec::new(),
styles: Vec::new(),
}
}
pub fn style(mut self, style: FS::StylingAttributes) -> Self {
self.styles.push(style);
self
}
pub(crate) fn new_vanity<C: VanityControlData + Default>(
mut self,
builder: impl Fn(VanityControlBuilder<FD, FS, C>) -> VanityControlBuilder<FD, FS, C>,
@ -119,7 +137,7 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
self.render_fns.push(Box::new(render_fn));
}
fn build_control_view<C: ControlData, FDT: Clone + PartialEq + 'static>(
fn build_control_view<C: ControlData, FDT: 'static>(
fd: RwSignal<FD>,
fs: &FS,
getter: Rc<dyn FieldGetter<FD, FDT>>,
@ -130,11 +148,21 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
render_data: crate::controls::ControlRenderData<FS, C>,
) -> (View, Box<dyn ValidationCb>) {
let (validation_signal, validation_signal_set) = create_signal(Ok(()));
let validation_fn_clone = validation_fn.clone();
let value_getter = move || {
let getter = getter.clone();
// memoize so that updating one field doesn't cause a re-render for all fields
let field = create_memo(move |_| getter(fd.get()));
unparse_fn(field.get())
let fd = fd.get();
// rerun validation if it is failing
if validation_signal.get_untracked().is_err() {
if let Some(ref validation_fn) = validation_fn_clone {
let validation_result = validation_fn(&fd);
// if validation succeeds this time, resolve the validation error
if validation_result.is_ok() {
validation_signal_set.set(Ok(()));
}
}
}
unparse_fn(getter(fd))
};
let value_getter = value_getter.into_signal();
@ -153,7 +181,31 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
};
let validation_cb = Box::new(validation_cb);
let validation_cb2 = validation_cb.clone();
let value_setter = Self::create_value_setter(
validation_cb.clone(),
validation_signal_set,
parse_fn,
setter,
fd,
);
let view = C::build_control(
fs,
render_data,
value_getter,
value_setter,
validation_signal.into(),
);
(view, validation_cb)
}
fn create_value_setter<CRT: 'static, FDT: 'static>(
validation_cb: Box<dyn Fn() -> bool + 'static>,
validation_signal_set: WriteSignal<Result<(), String>>,
parse_fn: Box<dyn ParseFn<CRT, FDT>>,
setter: Rc<dyn FieldSetter<FD, FDT>>,
fd: RwSignal<FD>,
) -> Box<dyn Fn(CRT) + 'static> {
let value_setter = move |value| {
let parsed = match parse_fn(value) {
Ok(p) => {
@ -172,18 +224,9 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
});
// run validation
(validation_cb2)();
(validation_cb)();
};
let value_setter = Box::new(value_setter);
let view = C::build_control(
fs,
render_data,
value_getter,
value_setter,
validation_signal.into(),
);
(view, validation_cb)
Box::new(value_setter)
}
pub(crate) fn build_action_form<ServFn>(
@ -201,7 +244,7 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
.map(|r_fn| r_fn(&self.fs, self.fd))
.unzip();
let elements = self.fs.form_frame(views.into_view());
let elements = self.fs.form_frame(views.into_view(), self.styles);
let on_submit = move |ev: SubmitEvent| {
let mut failed = false;
@ -235,7 +278,7 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
.map(|r_fn| r_fn(&self.fs, self.fd))
.unzip();
let elements = self.fs.form_frame(views.into_view());
let elements = self.fs.form_frame(views.into_view(), self.styles);
let on_submit = move |ev: SubmitEvent| {
let mut failed = false;

View File

@ -18,7 +18,7 @@ pub struct GridFormStyle;
impl FormStyle for GridFormStyle {
type StylingAttributes = GridFormStylingAttributes;
fn form_frame(&self, children: View) -> View {
fn form_frame(&self, children: View, _styles: Vec<Self::StylingAttributes>) -> View {
view! { <div class="form_grid">{children}</div> }.into_view()
}
@ -79,12 +79,11 @@ impl FormStyle for GridFormStyle {
.options
.into_iter()
.map(|value| {
// let value = value;
let cloned_value = value.clone();
view! {
<option
value=value.clone()
selected=move || value_getter.get() == *cloned_value
selected=move || {value_getter.get() == *cloned_value}
>
{value.clone()}
</option>
@ -100,7 +99,7 @@ impl FormStyle for GridFormStyle {
id=&control.data.name
name=control.data.name
class="form_input"
on:change=move |ev| {
on:input=move |ev| {
value_setter(event_target_value(&ev));
}
>
@ -114,7 +113,7 @@ impl FormStyle for GridFormStyle {
fn submit(&self, control: ControlRenderData<Self, SubmitData>) -> View {
view! {
<input type="submit" value=control.data.text class="col-span-full form_button"/>
<input type="submit" value=control.data.text class="form_submit"/>
}
.into_view()
}
@ -353,10 +352,6 @@ impl FormStyle for GridFormStyle {
.into_view()
}
fn group(&self, _control: ControlRenderData<Self, crate::controls::group::GroupData>) -> View {
todo!()
}
fn button<FD: crate::FormToolData>(
&self,
control: ControlRenderData<Self, crate::controls::button::ButtonData<FD>>,
@ -377,10 +372,27 @@ impl FormStyle for GridFormStyle {
};
view! {
<button type="button" class="form_button" on:click=on_click style:grid-column=format!("span {}", width) class="form_button">
<button type="button" class="form_button" on:click=on_click style:grid-column=format!("span {}", width)>
{control.data.text}
</button>
}
.into_view()
}
// TODO: change this and form frame to use ControlRenderData
fn group(&self, inner: View, styles: Vec<Self::StylingAttributes>) -> View {
let mut width = 12;
for style in styles {
match style {
GridFormStylingAttributes::Width(w) => width = w,
}
}
view! {
<div class="form_group form_grid" style:grid-column=format!("span {}", width)>
{inner}
</div>
}
.into_view()
}
}

View File

@ -1,17 +1,17 @@
mod grid_form;
pub use grid_form::{GridFormStyle, GridFormStylingAttributes};
use crate::{
controls::{
button::ButtonData, checkbox::CheckboxData, group::GroupData, heading::HeadingData,
hidden::HiddenData, output::OutputData, radio_buttons::RadioButtonsData,
select::SelectData, slider::SliderData, stepper::StepperData, submit::SubmitData,
text_area::TextAreaData, text_input::TextInputData, ControlData, ControlRenderData,
button::ButtonData, checkbox::CheckboxData, heading::HeadingData, hidden::HiddenData,
output::OutputData, radio_buttons::RadioButtonsData, select::SelectData,
slider::SliderData, stepper::StepperData, submit::SubmitData, text_area::TextAreaData,
text_input::TextInputData, ControlData, ControlRenderData,
},
FormToolData,
};
use leptos::{Signal, View};
pub use grid_form::{GridFormStyle, GridFormStylingAttributes};
pub trait FormStyle: Default + 'static {
type StylingAttributes;
@ -22,7 +22,7 @@ pub trait FormStyle: Default + 'static {
///
/// Do NOT wrap it in an actual `form` element; any
/// wrapping should be done with `div` or similar elements.
fn form_frame(&self, children: View) -> View;
fn form_frame(&self, children: View, styles: Vec<Self::StylingAttributes>) -> View;
fn heading(&self, control: ControlRenderData<Self, HeadingData>) -> View;
fn hidden(
&self,
@ -86,6 +86,6 @@ pub trait FormStyle: Default + 'static {
fn submit(&self, control: ControlRenderData<Self, SubmitData>) -> View;
// TODO: test custom component
fn custom_component(&self, view: View) -> View;
// TODO: add group
fn group(&self, control: ControlRenderData<Self, GroupData>) -> View;
// TODO: test group
fn group(&self, inner: View, styles: Vec<Self::StylingAttributes>) -> View;
}