diff --git a/examples/client/src/main/scala/HelloWorld.scala b/examples/client/src/main/scala/HelloWorld.scala index a6e76f7..636e891 100644 --- a/examples/client/src/main/scala/HelloWorld.scala +++ b/examples/client/src/main/scala/HelloWorld.scala @@ -8,7 +8,7 @@ case class Sample(name: String, component: HtmlElement) object App extends App { - val sample = Var(samples.sealedClasses.component) + val sample = Var(samples.either.component) private def item(name: String) = SideNavigation.item( _.text := name, diff --git a/examples/client/src/main/scala/samples/EitherSample.scala b/examples/client/src/main/scala/samples/EitherSample.scala index e2bf7cf..ec26743 100644 --- a/examples/client/src/main/scala/samples/EitherSample.scala +++ b/examples/client/src/main/scala/samples/EitherSample.scala @@ -1,6 +1,6 @@ package samples -import dev.cheleb.scalamigen.{*, given} +import dev.cheleb.scalamigen.* import com.raquo.laminar.api.L.* @@ -10,10 +10,17 @@ val either = { case class EitherSample( either: Either[Cat, Dog], + primitiveEither: Either[Cat, String], optionalInt: Option[Int] - ) + ) derives Form - val eitherVar = Var(EitherSample(Left(Cat("Scala le chat", 6)), Some(1))) + val eitherVar = Var( + EitherSample( + Left(Cat("Scala le chat", 6)), + Right("Forty two"), + Some(1) + ) + ) Sample( "Either", { diff --git a/examples/client/src/main/scala/samples/EnumSample.scala b/examples/client/src/main/scala/samples/EnumSample.scala index a5668d5..9e4087d 100644 --- a/examples/client/src/main/scala/samples/EnumSample.scala +++ b/examples/client/src/main/scala/samples/EnumSample.scala @@ -1,6 +1,6 @@ package samples -import dev.cheleb.scalamigen.{*, given} +import dev.cheleb.scalamigen.* import com.raquo.laminar.api.L.* @@ -9,12 +9,12 @@ import com.raquo.airstream.state.Var import com.raquo.laminar.api.L val enums = { - enum Color(val code: String): + enum Color(val code: String) derives Form: case Black extends Color("000") case White extends Color("FFF") case Isabelle extends Color("???") - case class Basket(color: Color, cat: Cat) + case class Basket(color: Color, cat: Cat) derives Form given colorForm: Form[Color] = enumForm(Color.values, Color.fromOrdinal) @@ -22,7 +22,7 @@ val enums = { name: String, age: Int, color: Color - ) + ) derives Form val eitherVar = Var( Basket(Color.Black, Cat("Scala", 10, Color.White)) diff --git a/examples/client/src/main/scala/samples/ListElement.scala b/examples/client/src/main/scala/samples/ListElement.scala index d19a4a7..a63de7d 100644 --- a/examples/client/src/main/scala/samples/ListElement.scala +++ b/examples/client/src/main/scala/samples/ListElement.scala @@ -1,21 +1,22 @@ package samples -import dev.cheleb.scalamigen.{*, given} +import dev.cheleb.scalamigen.* import com.raquo.laminar.api.L.* val list = { - case class Person2(id: Int, name: String, age: Int) + case class Person2(id: Int, name: String, age: Int) derives Form case class ListElement( ints: List[Person2] - ) + ) derives Form + + given (Person2 => Int) = _.id val listPersonVar = Var( ListElement(List(1, 2, 3).map(id => Person2(id, "Vlad", 20))) ) - given (Person2 => Int) = _.id Sample( "List", div( @@ -27,4 +28,5 @@ val list = { listPersonVar.asForm ) ) + } diff --git a/examples/client/src/main/scala/samples/Persons.scala b/examples/client/src/main/scala/samples/Persons.scala index 921af5c..13a127e 100644 --- a/examples/client/src/main/scala/samples/Persons.scala +++ b/examples/client/src/main/scala/samples/Persons.scala @@ -1,6 +1,6 @@ package samples -import dev.cheleb.scalamigen.{*, given} +import dev.cheleb.scalamigen.* import com.raquo.laminar.api.L.* import magnolia1.* @@ -23,15 +23,15 @@ val person = { email: Option[String], age: BigInt, size: Double - ) + ) derives Form case class Pet( name: String, age: BigInt, House: House, size: Double :| Positive - ) + ) derives Form - case class House(capacity: Int) + case class House(capacity: Int) derives Form // Provide default for optional given Defaultable[Pet] with diff --git a/examples/client/src/main/scala/samples/Sealed.scala b/examples/client/src/main/scala/samples/Sealed.scala index 67ff29c..24f772a 100644 --- a/examples/client/src/main/scala/samples/Sealed.scala +++ b/examples/client/src/main/scala/samples/Sealed.scala @@ -1,6 +1,6 @@ package samples -import dev.cheleb.scalamigen.{*, given} +import dev.cheleb.scalamigen.* import com.raquo.laminar.api.L.* @@ -8,13 +8,14 @@ import com.raquo.airstream.state.Var import com.raquo.laminar.nodes.ReactiveHtmlElement val sealedClasses = { - sealed trait Animal + sealed trait Animal derives Form - case class Horse(name: String, age: Int) extends Animal + case class Horse(name: String, age: Int) extends Animal derives Form case class Lama(name: String, age: Int, splitDistance: Int) extends Animal - case class Otter(name: String, age: Int) extends Animal + derives Form + case class Otter(name: String, age: Int) extends Animal derives Form - case class Owner(name: String, pet: Animal) + case class Owner(name: String, pet: Animal) derives Form Sample( "Sealed", { diff --git a/examples/client/src/main/scala/samples/SimpleSample.scala b/examples/client/src/main/scala/samples/SimpleSample.scala index dfc5d1f..9cbd3c5 100644 --- a/examples/client/src/main/scala/samples/SimpleSample.scala +++ b/examples/client/src/main/scala/samples/SimpleSample.scala @@ -1,6 +1,6 @@ package samples -import dev.cheleb.scalamigen.{*, given} +import dev.cheleb.scalamigen.* import com.raquo.laminar.api.L.* diff --git a/examples/client/src/main/scala/samples/Tree.scala b/examples/client/src/main/scala/samples/Tree.scala index 86ae087..40a4290 100644 --- a/examples/client/src/main/scala/samples/Tree.scala +++ b/examples/client/src/main/scala/samples/Tree.scala @@ -1,14 +1,14 @@ package samples -import dev.cheleb.scalamigen.{*, given} +import dev.cheleb.scalamigen.* import com.raquo.laminar.api.L.* import com.raquo.airstream.state.Var - +import dev.cheleb.scalamigen.FormDerive.autoDerived val tree = { - enum Tree[+T]: + enum Tree[+T] derives Form: case Empty extends Tree[Nothing] case Node(value: T, left: Tree[T], right: Tree[T]) object Tree: diff --git a/examples/client/src/main/scala/samples/Validation.scala b/examples/client/src/main/scala/samples/Validation.scala index b75f203..8b8bf6e 100644 --- a/examples/client/src/main/scala/samples/Validation.scala +++ b/examples/client/src/main/scala/samples/Validation.scala @@ -1,6 +1,6 @@ package samples -import dev.cheleb.scalamigen.{*, given} +import dev.cheleb.scalamigen.* import com.raquo.laminar.api.L.* @@ -19,7 +19,7 @@ val validation = { optionalInt: Option[Int], doubleGreaterThanEight: Double :| GreaterEqual[8.0], optionalDoublePositive: Option[Double :| Positive] - ) + ) derives Form given IronTypeValidator[Double, GreaterEqual[8.0]] = _.toDoubleOption match diff --git a/examples/client/src/main/scala/samples/package.scala b/examples/client/src/main/scala/samples/package.scala index a0c82b7..f314b35 100644 --- a/examples/client/src/main/scala/samples/package.scala +++ b/examples/client/src/main/scala/samples/package.scala @@ -11,10 +11,10 @@ object CurrencyCode: opaque type Password = String object Password: def apply(password: String): Password = password - given Form[Password] = secretForm(apply) + given Form[Password] = Form.secretForm(apply) -case class Cat(name: String, weight: Int, kind: Boolean = true) -case class Dog(name: String, weight: Int) +case class Cat(name: String, weight: Int, kind: Boolean = true) derives Form +case class Dog(name: String, weight: Int) derives Form given Defaultable[Cat] with def default = Cat("", 0) diff --git a/modules/core/src/main/scala/dev/cheleb/scalamigen/Defaultable.scala b/modules/core/src/main/scala/dev/cheleb/scalamigen/Defaultable.scala index 554ba80..e9d22cf 100644 --- a/modules/core/src/main/scala/dev/cheleb/scalamigen/Defaultable.scala +++ b/modules/core/src/main/scala/dev/cheleb/scalamigen/Defaultable.scala @@ -1,5 +1,8 @@ package dev.cheleb.scalamigen +import io.github.iltotore.iron.* +import io.github.iltotore.iron.constraint.all.* + /** Typeclass for default values. * * This typeclass is used to provide default values for a given type. It is @@ -19,3 +22,24 @@ trait Defaultable[A] { */ def label: String = default.getClass.getSimpleName() } + +object Defaultable { + + /** Default value for Int is 0. + */ + inline given Defaultable[Int] with + def default = 0 + + /** Default value for String is "". + */ + inline given Defaultable[String] with + def default = "" + + /** Default value for [Iron type Double + * positive](https://iltotore.github.io/iron/io/github/iltotore/iron/constraint/numeric$.html#Positive-0) + * is 0.0. + */ + inline given Defaultable[IronType[Double, Positive]] with + def default = 1.0.refineUnsafe[Positive] + +} diff --git a/modules/core/src/main/scala/dev/cheleb/scalamigen/Form.scala b/modules/core/src/main/scala/dev/cheleb/scalamigen/Form.scala index d8a16a7..fbddb7c 100644 --- a/modules/core/src/main/scala/dev/cheleb/scalamigen/Form.scala +++ b/modules/core/src/main/scala/dev/cheleb/scalamigen/Form.scala @@ -9,6 +9,9 @@ import org.scalajs.dom.HTMLDivElement import org.scalajs.dom.HTMLElement import com.raquo.laminar.nodes.ReactiveHtmlElement import magnolia1.SealedTrait.Subtype +import scala.deriving.Mirror +import java.time.LocalDate +import io.github.iltotore.iron.* /** A form for a type A. */ @@ -80,8 +83,7 @@ trait Form[A] { self => } -object Form extends AutoDerivation[Form] { - +object Form { def renderVar[A](v: Var[A], syncParent: () => Unit = () => ())(using WidgetFactory )(using @@ -89,6 +91,262 @@ object Form extends AutoDerivation[Form] { ): ReactiveHtmlElement[HTMLElement] = fa.render(v, syncParent) + /** Use this form to render a string that can be converted to A, can be used + * for Opaque types. + */ + + /** Form for an Iron type. This is a form for a type that can be validated + * with an Iron type. + */ + given [T, C](using fv: IronTypeValidator[T, C]): Form[IronType[T, C]] = + new Form[IronType[T, C]] { + + override def render( + variable: Var[IronType[T, C]], + syncParent: () => Unit + )(using factory: WidgetFactory): HtmlElement = + + val errorVar = Var("") + div( + div(child <-- errorVar.signal.map { item => + div( + s"$item" + ) + }), + input( + // _.showClearIcon := true, + backgroundColor <-- errorVar.signal.map { + case "" => "white" + case _ => "red" + }, + value <-- variable.signal.map(toString(_)), + onInput.mapToValue --> { str => + fromString(str, variable, errorVar) + + } + ) + ) + + override def fromString( + str: String, + variable: Var[IronType[T, C]], + errorVar: Var[String] + ): Unit = + fv.validate(str) match + case Left(error) => + errorVar.set(error) + case Right(value) => + errorVar.set("") + variable.set(value) + } + + /** Form for to a string, aka without validation. + */ + given Form[String] with + override def render( + variable: Var[String], + syncParent: () => Unit + )(using factory: WidgetFactory): HtmlElement = + factory.renderText + .amend( + value <-- variable.signal, + onInput.mapToValue --> { v => + variable.set(v) + syncParent() + } + ) + + given Form[Nothing] = new Form[Nothing] { + override def render( + variable: Var[Nothing], + syncParent: () => Unit + )(using factory: WidgetFactory): HtmlElement = + div() + } + + given Form[Boolean] = new Form[Boolean] { + override def render( + variable: Var[Boolean], + syncParent: () => Unit + )(using factory: WidgetFactory): HtmlElement = + div( + factory.renderCheckbox + .amend( + checked <-- variable.signal, + onChange.mapToChecked --> { v => + variable.set(v) + syncParent() + } + ) + ) + } + given Form[Double] = numericForm(_.toDoubleOption, 0) + given Form[Int] = numericForm(_.toIntOption, 0) + given Form[Float] = numericForm(_.toFloatOption, 0) + given Form[BigInt] = + numericForm(str => Try(BigInt(str)).toOption, BigInt(0)) + given Form[BigDecimal] = + numericForm(str => Try(BigDecimal(str)).toOption, BigDecimal(0)) + + // given + + given eitherOf[L, R](using + lf: Form[L], + rf: Form[R], + ld: Defaultable[L], + rd: Defaultable[R] + ): Form[Either[L, R]] = + new Form[Either[L, R]] { + override def render( + variable: Var[Either[L, R]], + syncParent: () => Unit + )(using factory: WidgetFactory): HtmlElement = + + val (vl, vr) = variable.now() match + case Left(l) => + (Var(l), Var(rd.default)) + case Right(r) => + (Var(ld.default), Var(r)) + + div( + span( + factory + .renderLink( + ld.label, + onClick.mapTo(Left(vl.now())) --> variable.writer + ), + "----", + factory.renderLink( + rd.label, + onClick.mapTo( + Right(vr.now()) + ) --> variable.writer + ) + ), + div( + display <-- variable.signal.map { + case Left(_) => "block" + case _ => "none" + }, + lf.render(vl, () => variable.set(Left(vl.now()))) + ), + div( + display <-- variable.signal.map { + case Left(_) => "none" + case _ => "block" + }, + rf.render(vr, () => variable.set(Right(vr.now()))) + ) + ) + + } + + given optionOfA[A](using + d: Defaultable[A], + fa: Form[A] + ): Form[Option[A]] = + new Form[Option[A]] { + override def render( + variable: Var[Option[A]], + syncParent: () => Unit + )(using factory: WidgetFactory): HtmlElement = + val a = variable.zoom { + case Some(a) => + a + case None => d.default + } { case (_, a) => + Some(a) + } + a.now() match + case null => + factory.renderButton.amend( + "Set", + onClick.mapTo(Some(d.default)) --> variable.writer + ) + case _ => + div( + div( + display <-- variable.signal.map { + case Some(_) => "block" + case None => "none" + }, + fa.render(a, syncParent) + ), + div( + factory.renderButton.amend( + display <-- variable.signal.map { + case Some(_) => "none" + case None => "block" + }, + "Set", + onClick.mapTo(Some(d.default)) --> variable.writer + ), + factory.renderButton.amend( + display <-- variable.signal.map { + case Some(_) => "block" + case None => "none" + }, + "Clear", + onClick.mapTo(None) --> variable.writer + ) + ) + ) + } + + given listOfA[A, K](using fa: Form[A], idOf: A => K): Form[List[A]] = + new Form[List[A]] { + + override def render( + variable: Var[List[A]], + syncParent: () => Unit + )(using factory: WidgetFactory): HtmlElement = + div( + children <-- variable.split(idOf)((id, initial, aVar) => { + div( + idAttr := s"list-item-$id", + div( + fa.render(aVar, syncParent) + ) + ) + }) + ) + } + + given Form[LocalDate] = new Form[LocalDate] { + override def render( + variable: Var[LocalDate], + syncParent: () => Unit + )(using factory: WidgetFactory): HtmlElement = + div( + factory.renderDatePicker + .amend( + value <-- variable.signal.map(_.toString), + onChange.mapToValue --> { v => + variable.set(LocalDate.parse(v)) + syncParent() + } + ) + ) + } + + def secretForm[A <: String](to: String => A) = new Form[A]: + override def render( + variable: Var[A], + syncParent: () => Unit + )(using factory: WidgetFactory): HtmlElement = + factory.renderSecret.amend( + value <-- variable.signal, + onInput.mapToValue.map(to) --> { v => + variable.set(v) + syncParent() + } + ) + +} + +object FormDerive extends AutoDerivation[Form] { + + type Typeclass[T] = Form[T] def join[A]( caseClass: CaseClass[Typeclass, A] ): Form[A] = new Form[A] { @@ -203,3 +461,6 @@ object Form extends AutoDerivation[Form] { string.split("(?=[A-Z])").map(_.capitalize).mkString(" ") } + +extension [A]($ : Form.type)(using Mirror.Of[A]) + inline def derived: Form[A] = FormDerive.derived[A] diff --git a/modules/core/src/main/scala/dev/cheleb/scalamigen/IronTypeValidator.scala b/modules/core/src/main/scala/dev/cheleb/scalamigen/IronTypeValidator.scala index a13d34a..fb7301c 100644 --- a/modules/core/src/main/scala/dev/cheleb/scalamigen/IronTypeValidator.scala +++ b/modules/core/src/main/scala/dev/cheleb/scalamigen/IronTypeValidator.scala @@ -1,6 +1,7 @@ package dev.cheleb.scalamigen -import io.github.iltotore.iron.IronType +import io.github.iltotore.iron.* +import io.github.iltotore.iron.constraint.all.* /** Type validator for * [IronType](https://iltotore.github.io/iron/docs/index.html). @@ -13,3 +14,16 @@ trait IronTypeValidator[T, C] { */ def validate(a: String): Either[String, IronType[T, C]] } + +object IronTypeValidator { + + /** Validator for [Iron type Double + * positive](https://iltotore.github.io/iron/io/github/iltotore/iron/constraint/numeric$.html#Positive-0). + */ + inline given IronTypeValidator[Double, Positive] with + def validate(a: String): Either[String, IronType[Double, Positive]] = + a.toDoubleOption match + case None => Left("Not a number") + case Some(double) => double.refineEither[Positive] + +} diff --git a/modules/core/src/main/scala/dev/cheleb/scalamigen/package.scala b/modules/core/src/main/scala/dev/cheleb/scalamigen/package.scala index 049b621..4bcef64 100644 --- a/modules/core/src/main/scala/dev/cheleb/scalamigen/package.scala +++ b/modules/core/src/main/scala/dev/cheleb/scalamigen/package.scala @@ -1,35 +1,9 @@ package dev.cheleb.scalamigen -import io.github.iltotore.iron.* -import io.github.iltotore.iron.constraint.all.* - -import com.raquo.laminar.api.L.* - -import scala.util.Try import com.raquo.airstream.state.Var -import java.time.LocalDate - -/** Default value for Int is 0. - */ -given Defaultable[Int] with - def default = 0 - -/** Default value for String is "". - */ -given Defaultable[String] with - def default = "" - -/** Default value for [Iron type Double - * positive](https://iltotore.github.io/iron/io/github/iltotore/iron/constraint/numeric$.html#Positive-0) - * is 0.0. - */ -given Defaultable[IronType[Double, Positive]] with - def default = 1.0.refineUnsafe[Positive] +import com.raquo.laminar.api.L.* -/** Use this form to render a string that can be converted to A, can be used for - * Opaque types. - */ def stringForm[A](to: String => A) = new Form[A]: override def render( variable: Var[A], @@ -65,262 +39,6 @@ def numericForm[A](f: String => Option[A], zero: A): Form[A] = new Form[A] { ) } -/** Validator for [Iron type Double - * positive](https://iltotore.github.io/iron/io/github/iltotore/iron/constraint/numeric$.html#Positive-0). - */ -given IronTypeValidator[Double, Positive] with - def validate(a: String): Either[String, IronType[Double, Positive]] = - a.toDoubleOption match - case None => Left("Not a number") - case Some(double) => double.refineEither[Positive] - -/** Form for an Iron type. This is a form for a type that can be validated with - * an Iron type. - */ -given [T, C](using fv: IronTypeValidator[T, C]): Form[IronType[T, C]] = - new Form[IronType[T, C]] { - - override def render( - variable: Var[IronType[T, C]], - syncParent: () => Unit - )(using factory: WidgetFactory): HtmlElement = - - val errorVar = Var("") - div( - div(child <-- errorVar.signal.map { item => - div( - s"$item" - ) - }), - input( - // _.showClearIcon := true, - backgroundColor <-- errorVar.signal.map { - case "" => "white" - case _ => "red" - }, - value <-- variable.signal.map(toString(_)), - onInput.mapToValue --> { str => - fromString(str, variable, errorVar) - - } - ) - ) - - override def fromString( - str: String, - variable: Var[IronType[T, C]], - errorVar: Var[String] - ): Unit = - fv.validate(str) match - case Left(error) => - errorVar.set(error) - case Right(value) => - errorVar.set("") - variable.set(value) - } - -/** Form for to a string, aka without validation. - */ -given Form[String] with - override def render( - variable: Var[String], - syncParent: () => Unit - )(using factory: WidgetFactory): HtmlElement = - factory.renderText - .amend( - value <-- variable.signal, - onInput.mapToValue --> { v => - variable.set(v) - syncParent() - } - ) - -given Form[Nothing] = new Form[Nothing] { - override def render( - variable: Var[Nothing], - syncParent: () => Unit - )(using factory: WidgetFactory): HtmlElement = - div() -} - -given Form[Boolean] = new Form[Boolean] { - override def render( - variable: Var[Boolean], - syncParent: () => Unit - )(using factory: WidgetFactory): HtmlElement = - div( - factory.renderCheckbox - .amend( - checked <-- variable.signal, - onChange.mapToChecked --> { v => - variable.set(v) - syncParent() - } - ) - ) -} -given Form[Double] = numericForm(_.toDoubleOption, 0) -given Form[Int] = numericForm(_.toIntOption, 0) -given Form[Float] = numericForm(_.toFloatOption, 0) -given Form[BigInt] = - numericForm(str => Try(BigInt(str)).toOption, BigInt(0)) -given Form[BigDecimal] = - numericForm(str => Try(BigDecimal(str)).toOption, BigDecimal(0)) - -//given - -given eitherOf[L, R](using - lf: Form[L], - rf: Form[R], - ld: Defaultable[L], - rd: Defaultable[R] -): Form[Either[L, R]] = - new Form[Either[L, R]] { - override def render( - variable: Var[Either[L, R]], - syncParent: () => Unit - )(using factory: WidgetFactory): HtmlElement = - - val (vl, vr) = variable.now() match - case Left(l) => - (Var(l), Var(rd.default)) - case Right(r) => - (Var(ld.default), Var(r)) - - div( - span( - factory - .renderLink( - ld.label, - onClick.mapTo(Left(vl.now())) --> variable.writer - ), - "----", - factory.renderLink( - rd.label, - onClick.mapTo( - Right(vr.now()) - ) --> variable.writer - ) - ), - div( - display <-- variable.signal.map { - case Left(_) => "block" - case _ => "none" - }, - lf.render(vl, () => variable.set(Left(vl.now()))) - ), - div( - display <-- variable.signal.map { - case Left(_) => "none" - case _ => "block" - }, - rf.render(vr, () => variable.set(Right(vr.now()))) - ) - ) - - } - -given optionOfA[A](using - d: Defaultable[A], - fa: Form[A] -): Form[Option[A]] = - new Form[Option[A]] { - override def render( - variable: Var[Option[A]], - syncParent: () => Unit - )(using factory: WidgetFactory): HtmlElement = - val a = variable.zoom { - case Some(a) => - a - case None => d.default - } { case (_, a) => - Some(a) - } - a.now() match - case null => - factory.renderButton.amend( - "Set", - onClick.mapTo(Some(d.default)) --> variable.writer - ) - case _ => - div( - div( - display <-- variable.signal.map { - case Some(_) => "block" - case None => "none" - }, - fa.render(a, syncParent) - ), - div( - factory.renderButton.amend( - display <-- variable.signal.map { - case Some(_) => "none" - case None => "block" - }, - "Set", - onClick.mapTo(Some(d.default)) --> variable.writer - ), - factory.renderButton.amend( - display <-- variable.signal.map { - case Some(_) => "block" - case None => "none" - }, - "Clear", - onClick.mapTo(None) --> variable.writer - ) - ) - ) - } - -given listOfA[A, K](using fa: Form[A], idOf: A => K): Form[List[A]] = - new Form[List[A]] { - - override def render( - variable: Var[List[A]], - syncParent: () => Unit - )(using factory: WidgetFactory): HtmlElement = - div( - children <-- variable.split(idOf)((id, initial, aVar) => { - div( - idAttr := s"list-item-$id", - div( - fa.render(aVar, syncParent) - ) - ) - }) - ) - } - -given Form[LocalDate] = new Form[LocalDate] { - override def render( - variable: Var[LocalDate], - syncParent: () => Unit - )(using factory: WidgetFactory): HtmlElement = - div( - factory.renderDatePicker - .amend( - value <-- variable.signal.map(_.toString), - onChange.mapToValue --> { v => - variable.set(LocalDate.parse(v)) - syncParent() - } - ) - ) -} - -def secretForm[A <: String](to: String => A) = new Form[A]: - override def render( - variable: Var[A], - syncParent: () => Unit - )(using factory: WidgetFactory): HtmlElement = - factory.renderSecret.amend( - value <-- variable.signal, - onInput.mapToValue.map(to) --> { v => - variable.set(v) - syncParent() - } - ) - def enumForm[A](values: Array[A], f: Int => A) = new Form[A] { override def render(