diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala index 9091760..f5200eb 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala @@ -26,7 +26,7 @@ object FieldBuilder: // TODO: use validation codec with A => raw string and raw string => Validted[A] - def requiredInput[A: InputCodec](using + def requiredInput[A: InputSchema](using fctx: FormBuilderContext ): FieldBuilder[A] = new FieldBuilder[A]: @@ -35,14 +35,15 @@ fieldDescriptor: FieldDescriptor, initialValue: Option[A] ): FormComponent[A] = - val codec = summon[InputCodec[A]] + val codec = summon[InputSchema[A]] Input( fieldDescriptor, initialValue.map(codec.encode), - Validations.required(fieldDescriptor.label)(_).flatMap(codec.decode) + Validations.required(fieldDescriptor.label)(_).flatMap(codec.decode), + codec.inputType ) - def optionalInput[A: InputCodec](using + def optionalInput[A: InputSchema](using fctx: FormBuilderContext ): FieldBuilder[Option[A]] = new FieldBuilder[Option[A]]: @@ -51,7 +52,7 @@ fieldDescriptor: FieldDescriptor, initialValue: Option[Option[A]] ): FormComponent[Option[A]] = - val codec = summon[InputCodec[A]] + val codec = summon[InputSchema[A]] Input[Option[A]]( fieldDescriptor, initialValue.flatten.map(codec.encode), @@ -59,14 +60,16 @@ v match case Some(s) if s.trim.nonEmpty => codec.decode(s).map(Some(_)) case _ => Validation.succeed(None) + , + codec.inputType ) - given [A: InputCodec](using + given [A: InputSchema](using fctx: FormBuilderContext, ev: NotGiven[A <:< Option[_]] ): FieldBuilder[A] = requiredInput[A] - given [A, B: InputCodec](using + given [A, B: InputSchema](using fctx: FormBuilderContext, ev: A <:< Option[B] ): FieldBuilder[Option[B]] = optionalInput[B] @@ -144,7 +147,8 @@ class Input[A]( desc: FieldDescriptor, initialValue: Option[String] = None, - validation: Option[String] => Validated[A] + validation: Option[String] => Validated[A], + inputType: InputSchema.InputType )(using fctx: FormBuilderContext) extends FormComponent[A]: private val rawValue: Var[Option[String]] = Var(initialValue) @@ -157,7 +161,8 @@ desc, initialValue, validated, - rawValue.writer + rawValue.writer, + inputType ).elements class FileField[A]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala index 9091760..f5200eb 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala @@ -26,7 +26,7 @@ object FieldBuilder: // TODO: use validation codec with A => raw string and raw string => Validted[A] - def requiredInput[A: InputCodec](using + def requiredInput[A: InputSchema](using fctx: FormBuilderContext ): FieldBuilder[A] = new FieldBuilder[A]: @@ -35,14 +35,15 @@ fieldDescriptor: FieldDescriptor, initialValue: Option[A] ): FormComponent[A] = - val codec = summon[InputCodec[A]] + val codec = summon[InputSchema[A]] Input( fieldDescriptor, initialValue.map(codec.encode), - Validations.required(fieldDescriptor.label)(_).flatMap(codec.decode) + Validations.required(fieldDescriptor.label)(_).flatMap(codec.decode), + codec.inputType ) - def optionalInput[A: InputCodec](using + def optionalInput[A: InputSchema](using fctx: FormBuilderContext ): FieldBuilder[Option[A]] = new FieldBuilder[Option[A]]: @@ -51,7 +52,7 @@ fieldDescriptor: FieldDescriptor, initialValue: Option[Option[A]] ): FormComponent[Option[A]] = - val codec = summon[InputCodec[A]] + val codec = summon[InputSchema[A]] Input[Option[A]]( fieldDescriptor, initialValue.flatten.map(codec.encode), @@ -59,14 +60,16 @@ v match case Some(s) if s.trim.nonEmpty => codec.decode(s).map(Some(_)) case _ => Validation.succeed(None) + , + codec.inputType ) - given [A: InputCodec](using + given [A: InputSchema](using fctx: FormBuilderContext, ev: NotGiven[A <:< Option[_]] ): FieldBuilder[A] = requiredInput[A] - given [A, B: InputCodec](using + given [A, B: InputSchema](using fctx: FormBuilderContext, ev: A <:< Option[B] ): FieldBuilder[Option[B]] = optionalInput[B] @@ -144,7 +147,8 @@ class Input[A]( desc: FieldDescriptor, initialValue: Option[String] = None, - validation: Option[String] => Validated[A] + validation: Option[String] => Validated[A], + inputType: InputSchema.InputType )(using fctx: FormBuilderContext) extends FormComponent[A]: private val rawValue: Var[Option[String]] = Var(initialValue) @@ -157,7 +161,8 @@ desc, initialValue, validated, - rawValue.writer + rawValue.writer, + inputType ).elements class FileField[A]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala index cd01954..bdc0cec 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala @@ -91,13 +91,14 @@ fctx.formUIFactory .section(desc.title, desc.subtitle.map(textToTextNode))(_*) ) - case Control(name, required, decode, validation) => + case Control(name, required, decode, validation, inputType) => val desc = FieldDescriptor(name) FieldBuilder .Input( desc, initialValue.map(decode(_)), - validation + validation, + inputType ) .wrap( fctx.formUIFactory.field( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala index 9091760..f5200eb 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala @@ -26,7 +26,7 @@ object FieldBuilder: // TODO: use validation codec with A => raw string and raw string => Validted[A] - def requiredInput[A: InputCodec](using + def requiredInput[A: InputSchema](using fctx: FormBuilderContext ): FieldBuilder[A] = new FieldBuilder[A]: @@ -35,14 +35,15 @@ fieldDescriptor: FieldDescriptor, initialValue: Option[A] ): FormComponent[A] = - val codec = summon[InputCodec[A]] + val codec = summon[InputSchema[A]] Input( fieldDescriptor, initialValue.map(codec.encode), - Validations.required(fieldDescriptor.label)(_).flatMap(codec.decode) + Validations.required(fieldDescriptor.label)(_).flatMap(codec.decode), + codec.inputType ) - def optionalInput[A: InputCodec](using + def optionalInput[A: InputSchema](using fctx: FormBuilderContext ): FieldBuilder[Option[A]] = new FieldBuilder[Option[A]]: @@ -51,7 +52,7 @@ fieldDescriptor: FieldDescriptor, initialValue: Option[Option[A]] ): FormComponent[Option[A]] = - val codec = summon[InputCodec[A]] + val codec = summon[InputSchema[A]] Input[Option[A]]( fieldDescriptor, initialValue.flatten.map(codec.encode), @@ -59,14 +60,16 @@ v match case Some(s) if s.trim.nonEmpty => codec.decode(s).map(Some(_)) case _ => Validation.succeed(None) + , + codec.inputType ) - given [A: InputCodec](using + given [A: InputSchema](using fctx: FormBuilderContext, ev: NotGiven[A <:< Option[_]] ): FieldBuilder[A] = requiredInput[A] - given [A, B: InputCodec](using + given [A, B: InputSchema](using fctx: FormBuilderContext, ev: A <:< Option[B] ): FieldBuilder[Option[B]] = optionalInput[B] @@ -144,7 +147,8 @@ class Input[A]( desc: FieldDescriptor, initialValue: Option[String] = None, - validation: Option[String] => Validated[A] + validation: Option[String] => Validated[A], + inputType: InputSchema.InputType )(using fctx: FormBuilderContext) extends FormComponent[A]: private val rawValue: Var[Option[String]] = Var(initialValue) @@ -157,7 +161,8 @@ desc, initialValue, validated, - rawValue.writer + rawValue.writer, + inputType ).elements class FileField[A]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala index cd01954..bdc0cec 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala @@ -91,13 +91,14 @@ fctx.formUIFactory .section(desc.title, desc.subtitle.map(textToTextNode))(_*) ) - case Control(name, required, decode, validation) => + case Control(name, required, decode, validation, inputType) => val desc = FieldDescriptor(name) FieldBuilder .Input( desc, initialValue.map(decode(_)), - validation + validation, + inputType ) .wrap( fctx.formUIFactory.field( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala index 65de5e2..cfdb8fc 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala @@ -16,11 +16,18 @@ name: String, required: Boolean, decode: A => String, - validation: Option[String] => Validated[A] + validation: Option[String] => Validated[A], + inputType: InputSchema.InputType ) extends FormSchema[A] object Control: - def apply[A](name: String)(using ic: InputCodec[A]): Control[A] = - Control(name, ic.required, ic.encode, ic.decodeOptional(name)) + def apply[A](name: String)(using ic: InputSchema[A]): Control[A] = + Control( + name, + ic.required, + ic.encode, + ic.decodeOptional(name), + ic.inputType + ) case class Section[A](name: String, content: FormSchema[A]) extends FormSchema[A] case class Zip[A, B <: Tuple](left: FormSchema[A], right: FormSchema[B]) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala index 9091760..f5200eb 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala @@ -26,7 +26,7 @@ object FieldBuilder: // TODO: use validation codec with A => raw string and raw string => Validted[A] - def requiredInput[A: InputCodec](using + def requiredInput[A: InputSchema](using fctx: FormBuilderContext ): FieldBuilder[A] = new FieldBuilder[A]: @@ -35,14 +35,15 @@ fieldDescriptor: FieldDescriptor, initialValue: Option[A] ): FormComponent[A] = - val codec = summon[InputCodec[A]] + val codec = summon[InputSchema[A]] Input( fieldDescriptor, initialValue.map(codec.encode), - Validations.required(fieldDescriptor.label)(_).flatMap(codec.decode) + Validations.required(fieldDescriptor.label)(_).flatMap(codec.decode), + codec.inputType ) - def optionalInput[A: InputCodec](using + def optionalInput[A: InputSchema](using fctx: FormBuilderContext ): FieldBuilder[Option[A]] = new FieldBuilder[Option[A]]: @@ -51,7 +52,7 @@ fieldDescriptor: FieldDescriptor, initialValue: Option[Option[A]] ): FormComponent[Option[A]] = - val codec = summon[InputCodec[A]] + val codec = summon[InputSchema[A]] Input[Option[A]]( fieldDescriptor, initialValue.flatten.map(codec.encode), @@ -59,14 +60,16 @@ v match case Some(s) if s.trim.nonEmpty => codec.decode(s).map(Some(_)) case _ => Validation.succeed(None) + , + codec.inputType ) - given [A: InputCodec](using + given [A: InputSchema](using fctx: FormBuilderContext, ev: NotGiven[A <:< Option[_]] ): FieldBuilder[A] = requiredInput[A] - given [A, B: InputCodec](using + given [A, B: InputSchema](using fctx: FormBuilderContext, ev: A <:< Option[B] ): FieldBuilder[Option[B]] = optionalInput[B] @@ -144,7 +147,8 @@ class Input[A]( desc: FieldDescriptor, initialValue: Option[String] = None, - validation: Option[String] => Validated[A] + validation: Option[String] => Validated[A], + inputType: InputSchema.InputType )(using fctx: FormBuilderContext) extends FormComponent[A]: private val rawValue: Var[Option[String]] = Var(initialValue) @@ -157,7 +161,8 @@ desc, initialValue, validated, - rawValue.writer + rawValue.writer, + inputType ).elements class FileField[A]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala index cd01954..bdc0cec 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala @@ -91,13 +91,14 @@ fctx.formUIFactory .section(desc.title, desc.subtitle.map(textToTextNode))(_*) ) - case Control(name, required, decode, validation) => + case Control(name, required, decode, validation, inputType) => val desc = FieldDescriptor(name) FieldBuilder .Input( desc, initialValue.map(decode(_)), - validation + validation, + inputType ) .wrap( fctx.formUIFactory.field( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala index 65de5e2..cfdb8fc 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala @@ -16,11 +16,18 @@ name: String, required: Boolean, decode: A => String, - validation: Option[String] => Validated[A] + validation: Option[String] => Validated[A], + inputType: InputSchema.InputType ) extends FormSchema[A] object Control: - def apply[A](name: String)(using ic: InputCodec[A]): Control[A] = - Control(name, ic.required, ic.encode, ic.decodeOptional(name)) + def apply[A](name: String)(using ic: InputSchema[A]): Control[A] = + Control( + name, + ic.required, + ic.encode, + ic.decodeOptional(name), + ic.inputType + ) case class Section[A](name: String, content: FormSchema[A]) extends FormSchema[A] case class Zip[A, B <: Tuple](left: FormSchema[A], right: FormSchema[B]) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala index 680f4e9..03b22b9 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala @@ -37,6 +37,8 @@ def input(inError: Signal[Boolean])(mods: HtmlMod*): HtmlElement + def textarea(inError: Signal[Boolean])(mods: HtmlMod*): HtmlElement + def fileInput(title: String)( buttonMods: HtmlMod* )( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala index 9091760..f5200eb 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala @@ -26,7 +26,7 @@ object FieldBuilder: // TODO: use validation codec with A => raw string and raw string => Validted[A] - def requiredInput[A: InputCodec](using + def requiredInput[A: InputSchema](using fctx: FormBuilderContext ): FieldBuilder[A] = new FieldBuilder[A]: @@ -35,14 +35,15 @@ fieldDescriptor: FieldDescriptor, initialValue: Option[A] ): FormComponent[A] = - val codec = summon[InputCodec[A]] + val codec = summon[InputSchema[A]] Input( fieldDescriptor, initialValue.map(codec.encode), - Validations.required(fieldDescriptor.label)(_).flatMap(codec.decode) + Validations.required(fieldDescriptor.label)(_).flatMap(codec.decode), + codec.inputType ) - def optionalInput[A: InputCodec](using + def optionalInput[A: InputSchema](using fctx: FormBuilderContext ): FieldBuilder[Option[A]] = new FieldBuilder[Option[A]]: @@ -51,7 +52,7 @@ fieldDescriptor: FieldDescriptor, initialValue: Option[Option[A]] ): FormComponent[Option[A]] = - val codec = summon[InputCodec[A]] + val codec = summon[InputSchema[A]] Input[Option[A]]( fieldDescriptor, initialValue.flatten.map(codec.encode), @@ -59,14 +60,16 @@ v match case Some(s) if s.trim.nonEmpty => codec.decode(s).map(Some(_)) case _ => Validation.succeed(None) + , + codec.inputType ) - given [A: InputCodec](using + given [A: InputSchema](using fctx: FormBuilderContext, ev: NotGiven[A <:< Option[_]] ): FieldBuilder[A] = requiredInput[A] - given [A, B: InputCodec](using + given [A, B: InputSchema](using fctx: FormBuilderContext, ev: A <:< Option[B] ): FieldBuilder[Option[B]] = optionalInput[B] @@ -144,7 +147,8 @@ class Input[A]( desc: FieldDescriptor, initialValue: Option[String] = None, - validation: Option[String] => Validated[A] + validation: Option[String] => Validated[A], + inputType: InputSchema.InputType )(using fctx: FormBuilderContext) extends FormComponent[A]: private val rawValue: Var[Option[String]] = Var(initialValue) @@ -157,7 +161,8 @@ desc, initialValue, validated, - rawValue.writer + rawValue.writer, + inputType ).elements class FileField[A]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala index cd01954..bdc0cec 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala @@ -91,13 +91,14 @@ fctx.formUIFactory .section(desc.title, desc.subtitle.map(textToTextNode))(_*) ) - case Control(name, required, decode, validation) => + case Control(name, required, decode, validation, inputType) => val desc = FieldDescriptor(name) FieldBuilder .Input( desc, initialValue.map(decode(_)), - validation + validation, + inputType ) .wrap( fctx.formUIFactory.field( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala index 65de5e2..cfdb8fc 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala @@ -16,11 +16,18 @@ name: String, required: Boolean, decode: A => String, - validation: Option[String] => Validated[A] + validation: Option[String] => Validated[A], + inputType: InputSchema.InputType ) extends FormSchema[A] object Control: - def apply[A](name: String)(using ic: InputCodec[A]): Control[A] = - Control(name, ic.required, ic.encode, ic.decodeOptional(name)) + def apply[A](name: String)(using ic: InputSchema[A]): Control[A] = + Control( + name, + ic.required, + ic.encode, + ic.decodeOptional(name), + ic.inputType + ) case class Section[A](name: String, content: FormSchema[A]) extends FormSchema[A] case class Zip[A, B <: Tuple](left: FormSchema[A], right: FormSchema[B]) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala index 680f4e9..03b22b9 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala @@ -37,6 +37,8 @@ def input(inError: Signal[Boolean])(mods: HtmlMod*): HtmlElement + def textarea(inError: Signal[Boolean])(mods: HtmlMod*): HtmlElement + def fileInput(title: String)( buttonMods: HtmlMod* )( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputCodec.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputCodec.scala deleted file mode 100644 index e4cd029..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputCodec.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar.forms - -import works.iterative.core.* -import zio.prelude.Validation -import works.iterative.core.UserMessage - -trait InputCodec[A]: - def encode(a: A): String - def decode(s: String): Validated[A] - def decodeOptional(label: String)(s: Option[String]): Validated[A] = - Validation.fail(UserMessage("error.value.required", label)) - def required: Boolean = true - -object InputCodec: - def apply[A]( - encodeF: A => String, - decodeF: String => Validated[A] - ): InputCodec[A] = - new InputCodec[A]: - override def encode(a: A): String = encodeF(a) - override def decode(s: String): Validated[A] = decodeF(s) - - def fromValidatedString[A]( - factory: ValidatedStringFactory[A] - ): InputCodec[A] = new InputCodec: - override def encode(a: A): String = factory.getter(a) - override def decode(s: String): Validated[A] = factory(s) - - given withValidatedStringFactory[A](using - factory: ValidatedStringFactory[A] - ): InputCodec[A] = fromValidatedString[A](factory) - - given validatedStringToInputCodec[A] - : Conversion[ValidatedStringFactory[A], InputCodec[A]] = - fromValidatedString(_) - - given string: InputCodec[String] with - override def encode(a: String): String = a - override def decode(s: String): Validated[String] = Validation.succeed(s) - - given plainOneLine: InputCodec[PlainOneLine] with - override def encode(a: PlainOneLine): String = a.asString - override def decode(s: String): Validated[PlainOneLine] = - PlainOneLine(s) - - given plainMultiLine: InputCodec[PlainMultiLine] with - override def encode(a: PlainMultiLine): String = a.asString - override def decode(s: String): Validated[PlainMultiLine] = - PlainMultiLine(s) - - given email: InputCodec[Email] with - def encode(a: Email): String = a.value - def decode(s: String): Validated[Email] = Email(s) - - given optionalInputCodec[A](using codec: InputCodec[A]): InputCodec[Option[A]] - with - def encode(a: Option[A]): String = a.map(codec.encode).getOrElse("") - def decode(s: String): Validated[Option[A]] = - if s.isEmpty then Validation.succeed(None) - else codec.decode(s).map(Some(_)) - override def decodeOptional(label: String)( - s: Option[String] - ): Validated[Option[A]] = - Validation.succeed(None) - override def required: Boolean = false diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala index 9091760..f5200eb 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala @@ -26,7 +26,7 @@ object FieldBuilder: // TODO: use validation codec with A => raw string and raw string => Validted[A] - def requiredInput[A: InputCodec](using + def requiredInput[A: InputSchema](using fctx: FormBuilderContext ): FieldBuilder[A] = new FieldBuilder[A]: @@ -35,14 +35,15 @@ fieldDescriptor: FieldDescriptor, initialValue: Option[A] ): FormComponent[A] = - val codec = summon[InputCodec[A]] + val codec = summon[InputSchema[A]] Input( fieldDescriptor, initialValue.map(codec.encode), - Validations.required(fieldDescriptor.label)(_).flatMap(codec.decode) + Validations.required(fieldDescriptor.label)(_).flatMap(codec.decode), + codec.inputType ) - def optionalInput[A: InputCodec](using + def optionalInput[A: InputSchema](using fctx: FormBuilderContext ): FieldBuilder[Option[A]] = new FieldBuilder[Option[A]]: @@ -51,7 +52,7 @@ fieldDescriptor: FieldDescriptor, initialValue: Option[Option[A]] ): FormComponent[Option[A]] = - val codec = summon[InputCodec[A]] + val codec = summon[InputSchema[A]] Input[Option[A]]( fieldDescriptor, initialValue.flatten.map(codec.encode), @@ -59,14 +60,16 @@ v match case Some(s) if s.trim.nonEmpty => codec.decode(s).map(Some(_)) case _ => Validation.succeed(None) + , + codec.inputType ) - given [A: InputCodec](using + given [A: InputSchema](using fctx: FormBuilderContext, ev: NotGiven[A <:< Option[_]] ): FieldBuilder[A] = requiredInput[A] - given [A, B: InputCodec](using + given [A, B: InputSchema](using fctx: FormBuilderContext, ev: A <:< Option[B] ): FieldBuilder[Option[B]] = optionalInput[B] @@ -144,7 +147,8 @@ class Input[A]( desc: FieldDescriptor, initialValue: Option[String] = None, - validation: Option[String] => Validated[A] + validation: Option[String] => Validated[A], + inputType: InputSchema.InputType )(using fctx: FormBuilderContext) extends FormComponent[A]: private val rawValue: Var[Option[String]] = Var(initialValue) @@ -157,7 +161,8 @@ desc, initialValue, validated, - rawValue.writer + rawValue.writer, + inputType ).elements class FileField[A]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala index cd01954..bdc0cec 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala @@ -91,13 +91,14 @@ fctx.formUIFactory .section(desc.title, desc.subtitle.map(textToTextNode))(_*) ) - case Control(name, required, decode, validation) => + case Control(name, required, decode, validation, inputType) => val desc = FieldDescriptor(name) FieldBuilder .Input( desc, initialValue.map(decode(_)), - validation + validation, + inputType ) .wrap( fctx.formUIFactory.field( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala index 65de5e2..cfdb8fc 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala @@ -16,11 +16,18 @@ name: String, required: Boolean, decode: A => String, - validation: Option[String] => Validated[A] + validation: Option[String] => Validated[A], + inputType: InputSchema.InputType ) extends FormSchema[A] object Control: - def apply[A](name: String)(using ic: InputCodec[A]): Control[A] = - Control(name, ic.required, ic.encode, ic.decodeOptional(name)) + def apply[A](name: String)(using ic: InputSchema[A]): Control[A] = + Control( + name, + ic.required, + ic.encode, + ic.decodeOptional(name), + ic.inputType + ) case class Section[A](name: String, content: FormSchema[A]) extends FormSchema[A] case class Zip[A, B <: Tuple](left: FormSchema[A], right: FormSchema[B]) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala index 680f4e9..03b22b9 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala @@ -37,6 +37,8 @@ def input(inError: Signal[Boolean])(mods: HtmlMod*): HtmlElement + def textarea(inError: Signal[Boolean])(mods: HtmlMod*): HtmlElement + def fileInput(title: String)( buttonMods: HtmlMod* )( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputCodec.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputCodec.scala deleted file mode 100644 index e4cd029..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputCodec.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar.forms - -import works.iterative.core.* -import zio.prelude.Validation -import works.iterative.core.UserMessage - -trait InputCodec[A]: - def encode(a: A): String - def decode(s: String): Validated[A] - def decodeOptional(label: String)(s: Option[String]): Validated[A] = - Validation.fail(UserMessage("error.value.required", label)) - def required: Boolean = true - -object InputCodec: - def apply[A]( - encodeF: A => String, - decodeF: String => Validated[A] - ): InputCodec[A] = - new InputCodec[A]: - override def encode(a: A): String = encodeF(a) - override def decode(s: String): Validated[A] = decodeF(s) - - def fromValidatedString[A]( - factory: ValidatedStringFactory[A] - ): InputCodec[A] = new InputCodec: - override def encode(a: A): String = factory.getter(a) - override def decode(s: String): Validated[A] = factory(s) - - given withValidatedStringFactory[A](using - factory: ValidatedStringFactory[A] - ): InputCodec[A] = fromValidatedString[A](factory) - - given validatedStringToInputCodec[A] - : Conversion[ValidatedStringFactory[A], InputCodec[A]] = - fromValidatedString(_) - - given string: InputCodec[String] with - override def encode(a: String): String = a - override def decode(s: String): Validated[String] = Validation.succeed(s) - - given plainOneLine: InputCodec[PlainOneLine] with - override def encode(a: PlainOneLine): String = a.asString - override def decode(s: String): Validated[PlainOneLine] = - PlainOneLine(s) - - given plainMultiLine: InputCodec[PlainMultiLine] with - override def encode(a: PlainMultiLine): String = a.asString - override def decode(s: String): Validated[PlainMultiLine] = - PlainMultiLine(s) - - given email: InputCodec[Email] with - def encode(a: Email): String = a.value - def decode(s: String): Validated[Email] = Email(s) - - given optionalInputCodec[A](using codec: InputCodec[A]): InputCodec[Option[A]] - with - def encode(a: Option[A]): String = a.map(codec.encode).getOrElse("") - def decode(s: String): Validated[Option[A]] = - if s.isEmpty then Validation.succeed(None) - else codec.decode(s).map(Some(_)) - override def decodeOptional(label: String)( - s: Option[String] - ): Validated[Option[A]] = - Validation.succeed(None) - override def required: Boolean = false diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputField.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputField.scala index 7faa9eb..9486238 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputField.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputField.scala @@ -2,12 +2,14 @@ import com.raquo.laminar.api.L.* import works.iterative.core.* +import io.laminext.syntax.core.* case class InputField( desc: FieldDescriptor, initialValue: Option[String], validated: Signal[Validated[_]], - observer: Observer[Option[String]] + observer: Observer[Option[String]], + inputType: InputSchema.InputType )(using fctx: FormBuilderContext): val hadFocus: Var[Boolean] = Var(false) @@ -23,20 +25,28 @@ if t then v.fold(_.toList, _ => List.empty) else Nil ) + def makeField: HtmlElement = + val mods = nodeSeq( + idAttr(desc.idString), + nameAttr(desc.name), + desc.placeholder.map(placeholder(_)), + initialValue.map(value(_)), + onInput.mapToValue.setAsValue --> observer.contramap { (v: String) => + Option(v).map(_.trim).filter(_.nonEmpty) + }, + onFocus.mapTo(true) --> hadFocus.writer, + onBlur.mapTo(true) --> touched.writer + ) + inputType match + case InputSchema.InputType.Input(typeValue) => + fctx.formUIFactory.input(hasError)(tpe(typeValue), mods) + case InputSchema.InputType.Textarea => + fctx.formUIFactory.textarea(hasError)(mods) + val elements: Seq[HtmlElement] = Seq( div( - fctx.formUIFactory.input(hasError)( - idAttr(desc.idString), - nameAttr(desc.name), - desc.placeholder.map(placeholder(_)), - initialValue.map(value(_)), - onInput.mapToValue.setAsValue --> observer.contramap { (v: String) => - Option(v).map(_.trim).filter(_.nonEmpty) - }, - onFocus.mapTo(true) --> hadFocus.writer, - onBlur.mapTo(true) --> touched.writer - ), + makeField, children <-- errors .map( _.map[HtmlElement](msg => diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala index 9091760..f5200eb 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FieldBuilder.scala @@ -26,7 +26,7 @@ object FieldBuilder: // TODO: use validation codec with A => raw string and raw string => Validted[A] - def requiredInput[A: InputCodec](using + def requiredInput[A: InputSchema](using fctx: FormBuilderContext ): FieldBuilder[A] = new FieldBuilder[A]: @@ -35,14 +35,15 @@ fieldDescriptor: FieldDescriptor, initialValue: Option[A] ): FormComponent[A] = - val codec = summon[InputCodec[A]] + val codec = summon[InputSchema[A]] Input( fieldDescriptor, initialValue.map(codec.encode), - Validations.required(fieldDescriptor.label)(_).flatMap(codec.decode) + Validations.required(fieldDescriptor.label)(_).flatMap(codec.decode), + codec.inputType ) - def optionalInput[A: InputCodec](using + def optionalInput[A: InputSchema](using fctx: FormBuilderContext ): FieldBuilder[Option[A]] = new FieldBuilder[Option[A]]: @@ -51,7 +52,7 @@ fieldDescriptor: FieldDescriptor, initialValue: Option[Option[A]] ): FormComponent[Option[A]] = - val codec = summon[InputCodec[A]] + val codec = summon[InputSchema[A]] Input[Option[A]]( fieldDescriptor, initialValue.flatten.map(codec.encode), @@ -59,14 +60,16 @@ v match case Some(s) if s.trim.nonEmpty => codec.decode(s).map(Some(_)) case _ => Validation.succeed(None) + , + codec.inputType ) - given [A: InputCodec](using + given [A: InputSchema](using fctx: FormBuilderContext, ev: NotGiven[A <:< Option[_]] ): FieldBuilder[A] = requiredInput[A] - given [A, B: InputCodec](using + given [A, B: InputSchema](using fctx: FormBuilderContext, ev: A <:< Option[B] ): FieldBuilder[Option[B]] = optionalInput[B] @@ -144,7 +147,8 @@ class Input[A]( desc: FieldDescriptor, initialValue: Option[String] = None, - validation: Option[String] => Validated[A] + validation: Option[String] => Validated[A], + inputType: InputSchema.InputType )(using fctx: FormBuilderContext) extends FormComponent[A]: private val rawValue: Var[Option[String]] = Var(initialValue) @@ -157,7 +161,8 @@ desc, initialValue, validated, - rawValue.writer + rawValue.writer, + inputType ).elements class FileField[A]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala index cd01954..bdc0cec 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala @@ -91,13 +91,14 @@ fctx.formUIFactory .section(desc.title, desc.subtitle.map(textToTextNode))(_*) ) - case Control(name, required, decode, validation) => + case Control(name, required, decode, validation, inputType) => val desc = FieldDescriptor(name) FieldBuilder .Input( desc, initialValue.map(decode(_)), - validation + validation, + inputType ) .wrap( fctx.formUIFactory.field( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala index 65de5e2..cfdb8fc 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormSchema.scala @@ -16,11 +16,18 @@ name: String, required: Boolean, decode: A => String, - validation: Option[String] => Validated[A] + validation: Option[String] => Validated[A], + inputType: InputSchema.InputType ) extends FormSchema[A] object Control: - def apply[A](name: String)(using ic: InputCodec[A]): Control[A] = - Control(name, ic.required, ic.encode, ic.decodeOptional(name)) + def apply[A](name: String)(using ic: InputSchema[A]): Control[A] = + Control( + name, + ic.required, + ic.encode, + ic.decodeOptional(name), + ic.inputType + ) case class Section[A](name: String, content: FormSchema[A]) extends FormSchema[A] case class Zip[A, B <: Tuple](left: FormSchema[A], right: FormSchema[B]) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala index 680f4e9..03b22b9 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala @@ -37,6 +37,8 @@ def input(inError: Signal[Boolean])(mods: HtmlMod*): HtmlElement + def textarea(inError: Signal[Boolean])(mods: HtmlMod*): HtmlElement + def fileInput(title: String)( buttonMods: HtmlMod* )( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputCodec.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputCodec.scala deleted file mode 100644 index e4cd029..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputCodec.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar.forms - -import works.iterative.core.* -import zio.prelude.Validation -import works.iterative.core.UserMessage - -trait InputCodec[A]: - def encode(a: A): String - def decode(s: String): Validated[A] - def decodeOptional(label: String)(s: Option[String]): Validated[A] = - Validation.fail(UserMessage("error.value.required", label)) - def required: Boolean = true - -object InputCodec: - def apply[A]( - encodeF: A => String, - decodeF: String => Validated[A] - ): InputCodec[A] = - new InputCodec[A]: - override def encode(a: A): String = encodeF(a) - override def decode(s: String): Validated[A] = decodeF(s) - - def fromValidatedString[A]( - factory: ValidatedStringFactory[A] - ): InputCodec[A] = new InputCodec: - override def encode(a: A): String = factory.getter(a) - override def decode(s: String): Validated[A] = factory(s) - - given withValidatedStringFactory[A](using - factory: ValidatedStringFactory[A] - ): InputCodec[A] = fromValidatedString[A](factory) - - given validatedStringToInputCodec[A] - : Conversion[ValidatedStringFactory[A], InputCodec[A]] = - fromValidatedString(_) - - given string: InputCodec[String] with - override def encode(a: String): String = a - override def decode(s: String): Validated[String] = Validation.succeed(s) - - given plainOneLine: InputCodec[PlainOneLine] with - override def encode(a: PlainOneLine): String = a.asString - override def decode(s: String): Validated[PlainOneLine] = - PlainOneLine(s) - - given plainMultiLine: InputCodec[PlainMultiLine] with - override def encode(a: PlainMultiLine): String = a.asString - override def decode(s: String): Validated[PlainMultiLine] = - PlainMultiLine(s) - - given email: InputCodec[Email] with - def encode(a: Email): String = a.value - def decode(s: String): Validated[Email] = Email(s) - - given optionalInputCodec[A](using codec: InputCodec[A]): InputCodec[Option[A]] - with - def encode(a: Option[A]): String = a.map(codec.encode).getOrElse("") - def decode(s: String): Validated[Option[A]] = - if s.isEmpty then Validation.succeed(None) - else codec.decode(s).map(Some(_)) - override def decodeOptional(label: String)( - s: Option[String] - ): Validated[Option[A]] = - Validation.succeed(None) - override def required: Boolean = false diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputField.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputField.scala index 7faa9eb..9486238 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputField.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputField.scala @@ -2,12 +2,14 @@ import com.raquo.laminar.api.L.* import works.iterative.core.* +import io.laminext.syntax.core.* case class InputField( desc: FieldDescriptor, initialValue: Option[String], validated: Signal[Validated[_]], - observer: Observer[Option[String]] + observer: Observer[Option[String]], + inputType: InputSchema.InputType )(using fctx: FormBuilderContext): val hadFocus: Var[Boolean] = Var(false) @@ -23,20 +25,28 @@ if t then v.fold(_.toList, _ => List.empty) else Nil ) + def makeField: HtmlElement = + val mods = nodeSeq( + idAttr(desc.idString), + nameAttr(desc.name), + desc.placeholder.map(placeholder(_)), + initialValue.map(value(_)), + onInput.mapToValue.setAsValue --> observer.contramap { (v: String) => + Option(v).map(_.trim).filter(_.nonEmpty) + }, + onFocus.mapTo(true) --> hadFocus.writer, + onBlur.mapTo(true) --> touched.writer + ) + inputType match + case InputSchema.InputType.Input(typeValue) => + fctx.formUIFactory.input(hasError)(tpe(typeValue), mods) + case InputSchema.InputType.Textarea => + fctx.formUIFactory.textarea(hasError)(mods) + val elements: Seq[HtmlElement] = Seq( div( - fctx.formUIFactory.input(hasError)( - idAttr(desc.idString), - nameAttr(desc.name), - desc.placeholder.map(placeholder(_)), - initialValue.map(value(_)), - onInput.mapToValue.setAsValue --> observer.contramap { (v: String) => - Option(v).map(_.trim).filter(_.nonEmpty) - }, - onFocus.mapTo(true) --> hadFocus.writer, - onBlur.mapTo(true) --> touched.writer - ), + makeField, children <-- errors .map( _.map[HtmlElement](msg => diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputSchema.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputSchema.scala new file mode 100644 index 0000000..2b97fc1 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/InputSchema.scala @@ -0,0 +1,73 @@ +package works.iterative.ui.components.laminar.forms + +import works.iterative.core.* +import zio.prelude.Validation +import works.iterative.core.UserMessage + +trait InputSchema[A]: + def encode(a: A): String + def decode(s: String): Validated[A] + def decodeOptional(label: String)(s: Option[String]): Validated[A] = + Validation.fail(UserMessage("error.value.required", label)) + def required: Boolean = true + def inputType: InputSchema.InputType = InputSchema.InputType.Input("text") + +object InputSchema: + enum InputType: + case Input(tpe: String) + case Textarea + + def apply[A]( + encodeF: A => String, + decodeF: String => Validated[A] + ): InputSchema[A] = + new InputSchema[A]: + override def encode(a: A): String = encodeF(a) + override def decode(s: String): Validated[A] = decodeF(s) + + def fromValidatedString[A]( + factory: ValidatedStringFactory[A] + ): InputSchema[A] = new InputSchema: + override def encode(a: A): String = factory.getter(a) + override def decode(s: String): Validated[A] = factory(s) + + given withValidatedStringFactory[A](using + factory: ValidatedStringFactory[A] + ): InputSchema[A] = fromValidatedString[A](factory) + + given validatedStringToInputCodec[A] + : Conversion[ValidatedStringFactory[A], InputSchema[A]] = + fromValidatedString(_) + + given string: InputSchema[String] with + override def encode(a: String): String = a + override def decode(s: String): Validated[String] = Validation.succeed(s) + + given plainOneLine: InputSchema[PlainOneLine] with + override def encode(a: PlainOneLine): String = a.asString + override def decode(s: String): Validated[PlainOneLine] = + PlainOneLine(s) + + given plainMultiLine: InputSchema[PlainMultiLine] with + override def encode(a: PlainMultiLine): String = a.asString + override def decode(s: String): Validated[PlainMultiLine] = + PlainMultiLine(s) + override def inputType: InputType = InputType.Textarea + + given email: InputSchema[Email] with + def encode(a: Email): String = a.value + def decode(s: String): Validated[Email] = Email(s) + override def inputType: InputType = InputType.Input("email") + + given optionalInputCodec[A](using + codec: InputSchema[A] + ): InputSchema[Option[A]] with + def encode(a: Option[A]): String = a.map(codec.encode).getOrElse("") + def decode(s: String): Validated[Option[A]] = + if s.isEmpty then Validation.succeed(None) + else codec.decode(s).map(Some(_)) + override def decodeOptional(label: String)( + s: Option[String] + ): Validated[Option[A]] = + Validation.succeed(None) + override def required: Boolean = false