Skip to content

Commit

Permalink
Refactor Form.scala and Validation.scala
Browse files Browse the repository at this point in the history
  • Loading branch information
cheleb committed Sep 25, 2024
1 parent 655cdc4 commit aded87b
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 84 deletions.
1 change: 0 additions & 1 deletion examples/client/src/main/scala/samples/Tree.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ val tree = {
given treeInstance[A](using
default: Defaultable[A]
)(using Form[A]): Form[Tree[A]] = new Form[Tree[A]] { self =>
override def isAnyRef = true
override def render(
variable: Var[Tree[A]],
syncParent: () => Unit
Expand Down
2 changes: 1 addition & 1 deletion examples/client/src/main/scala/samples/Validation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ val validation = {

given IronTypeValidator[Double, GreaterEqual[8.0]] =
_.toDoubleOption match
case None => Left("Nots a number")
case None => Left("Not a number")
case Some(double) => double.refineEither[GreaterEqual[8.0]]

val ironSampleVar = Var(
Expand Down
111 changes: 29 additions & 82 deletions modules/core/src/main/scala/dev/cheleb/scalamigen/Form.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,44 @@ import org.scalajs.dom.HTMLElement
import com.raquo.laminar.nodes.ReactiveHtmlElement
import magnolia1.SealedTrait.Subtype

extension [A](v: Var[A])
def asForm(using WidgetFactory, Form[A]) = Form.renderVar(v)

/** A form for a type A.
*/
trait Form[A] { self =>

def isAnyRef = false
// def isAnyRef = false

/** Parse a string and return an Option[A].
*
* @param s
* @return
*/
def fromString(s: String): Option[A] = None

/** Parse a string and set t he variable to the parsed value or set the
* errorVar to an error message.
*
* @param s
* @param variable
* @param errorVar
*/
def fromString(s: String, variable: Var[A], errorVar: Var[String]): Unit = ()

def toString(a: A) = a.toString

/** Render a form for a variable.
*
* Sometimes the form is a part of a larger form and the parent form needs to
* be updated when the variable changes. This is the purpose of the
* syncParent function.
*
* @param variable
* the variable to render
* @param syncParent
* a function to sync the parent state
* @param factory
* the widget factory
* @return
*/
def render(
variable: Var[A],
syncParent: () => Unit
Expand Down Expand Up @@ -68,7 +93,6 @@ object Form extends AutoDerivation[Form] {
caseClass: CaseClass[Typeclass, A]
): Form[A] = new Form[A] {

override def isAnyRef: Boolean = true
override def render(
variable: Var[A],
syncParent: () => Unit
Expand Down Expand Up @@ -128,7 +152,6 @@ object Form extends AutoDerivation[Form] {
*/
def split[A](sealedTrait: SealedTrait[Form, A]): Form[A] = new Form[A] {

override def isAnyRef: Boolean = true
override def render(
variable: Var[A],
syncParent: () => Unit
Expand Down Expand Up @@ -180,79 +203,3 @@ object Form extends AutoDerivation[Form] {
string.split("(?=[A-Z])").map(_.capitalize).mkString(" ")

}

/** 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],
syncParent: () => Unit
)(using factory: WidgetFactory): HtmlElement =
factory.renderText.amend(
value <-- variable.signal.map(_.toString),
onInput.mapToValue.map(to) --> { v =>
variable.set(v)
syncParent()
}
)

/** Form for a numeric type.
*/
def numericForm[A](f: String => Option[A], zero: A): Form[A] = new Form[A] {
self =>
override def fromString(s: String): Option[A] =
f(s).orElse(Some(zero))
override def render(
variable: Var[A],
syncParent: () => Unit
)(using factory: WidgetFactory): HtmlElement =
factory.renderNumeric
.amend(
value <-- variable.signal.map { str =>
str.toString()
},
onInput.mapToValue --> { v =>
fromString(v).foreach(variable.set)
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(
variable: Var[A],
syncParent: () => Unit
)(using factory: WidgetFactory): HtmlElement =
val valuesLabels = values.map(_.toString)
div(
factory
.renderSelect(idx => variable.set(f(idx)))
.amend(
valuesLabels.map { label =>
factory.renderOption(
label,
values
.map(_.toString)
.indexOf(label),
label == variable.now().toString
)
}.toSeq
)
)

}
79 changes: 79 additions & 0 deletions modules/core/src/main/scala/dev/cheleb/scalamigen/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,44 @@ given Defaultable[String] with
given Defaultable[IronType[Double, Positive]] with
def default = 1.0.refineUnsafe[Positive]

/** 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],
syncParent: () => Unit
)(using factory: WidgetFactory): HtmlElement =
factory.renderText.amend(
value <-- variable.signal.map(_.toString),
onInput.mapToValue.map(to) --> { v =>
variable.set(v)
syncParent()
}
)

/** Form for a numeric type.
*/
def numericForm[A](f: String => Option[A], zero: A): Form[A] = new Form[A] {
self =>
override def fromString(s: String): Option[A] =
f(s).orElse(Some(zero))
override def render(
variable: Var[A],
syncParent: () => Unit
)(using factory: WidgetFactory): HtmlElement =
factory.renderNumeric
.amend(
value <-- variable.signal.map { str =>
str.toString()
},
onInput.mapToValue --> { v =>
fromString(v).foreach(variable.set)
syncParent()
}
)
}

/** Validator for [Iron type Double
* positive](https://iltotore.github.io/iron/io/github/iltotore/iron/constraint/numeric$.html#Positive-0).
*/
Expand Down Expand Up @@ -269,3 +307,44 @@ given Form[LocalDate] = new Form[LocalDate] {
)
)
}

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(
variable: Var[A],
syncParent: () => Unit
)(using factory: WidgetFactory): HtmlElement =
val valuesLabels = values.map(_.toString)
div(
factory
.renderSelect(idx => variable.set(f(idx)))
.amend(
valuesLabels.map { label =>
factory.renderOption(
label,
values
.map(_.toString)
.indexOf(label),
label == variable.now().toString
)
}.toSeq
)
)

}

extension [A](v: Var[A])
def asForm(using WidgetFactory, Form[A]) = Form.renderVar(v)

0 comments on commit aded87b

Please sign in to comment.