Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(forms): enhance form handling with selectForm and selectMappedForm #250

Merged
merged 1 commit into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 53 additions & 19 deletions examples/client/src/main/scala/samples/EnumSample.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,49 @@ import com.raquo.laminar.api.L.*
import dev.cheleb.scalamigen.*
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.all.*
import java.util.UUID

val enums = {
enum Color(val code: String):
case Black extends Color("000")
case White extends Color("FFF")
case Isabelle extends Color("???")

given colorForm: Form[Color] = enumForm(Color.values, Color.fromOrdinal)
given colorForm: Form[Color] =
selectForm(Color.values, labelMapper = c => s"$c ${c.code}")

case class Meal(id: UUID, name: String)

val allMeals = List(
Meal(UUID.fromString("00000000-0000-0000-0000-000000000001"), "Pizza"),
Meal(UUID.fromString("00000000-0000-0000-0000-000000000002"), "Pasta")
)

given mealForm: Form[UUID] =
selectMappedForm(allMeals, mapper = m => m.id, labelMapper = _.name)

case class Basket(color: Color, cat: Cat)

case class Cat(
name: String,
weight: Int :| Positive,
kind: Boolean = true,
colol: Color
color: Color,
mealId: UUID
)
// case class Dog(name: String, weight: Int)

val enumVar = Var(
Basket(Color.Black, Cat("Scala", 10, true, Color.White))
Basket(
Color.Black,
Cat(
"Scala",
10,
true,
Color.White,
UUID.fromString("00000000-0000-0000-0000-000000000000")
)
)
)

Sample(
Expand All @@ -38,21 +60,33 @@ val enums = {
}
),
"""|
| enum Color(val code: String):
| case Black extends Color("000")
| case White extends Color("FFF")
| case Isabelle extends Color("???")
|
| given colorForm: Form[Color] = enumForm(Color.values, Color.fromOrdinal)
|
| case class Basket(color: Color, cat: Cat)
|
| case class Cat(
| name: String,
| age: Int,
| color: Color
| )
|
|""".stripMargin
|enum Color(val code: String):
| case Black extends Color("000")
| case White extends Color("FFF")
| case Isabelle extends Color("???")
|
|given colorForm: Form[Color] =
| selectForm(Color.values, labelMapper = c => s"$c ${c.code}")
|
|case class Meal(id: UUID, name: String)
|
|val allMeals = List(
| Meal(UUID.fromString("00000000-0000-0000-0000-000000000001"), "Pizza"),
| Meal(UUID.fromString("00000000-0000-0000-0000-000000000002"), "Pasta")
|)
|
|given mealForm: Form[UUID] =
| selectMappedForm(allMeals, mapper = m => m.id, labelMapper = _.name)
|
|case class Basket(color: Color, cat: Cat)
|
|case class Cat(
| name: String,
| weight: Int :| Positive,
| kind: Boolean = true,
| color: Color,
| mealId: UUID
|)
|""".stripMargin
)
}
4 changes: 2 additions & 2 deletions examples/client/src/main/scala/samples/Sealed.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ val sealedClasses = {
case White extends Color("FFF")
case Isabelle extends Color("???")

given colorForm: Form[Color] = enumForm(Color.values, Color.fromOrdinal)
given colorForm: Form[Color] = selectForm(Color.values)

sealed trait Animal

Expand Down Expand Up @@ -86,7 +86,7 @@ val sealedClasses = {
| case White extends Color("FFF")
| case Isabelle extends Color("???")
|
| given colorForm: Form[Color] = enumForm(Color.values, Color.fromOrdinal)
| given colorForm: Form[Color] = selectForm(Color.values)
|
| sealed trait Animal
|
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,42 @@
package dev.cheleb.scalamigen

/** A sealed trait representing the possible events that can be emitted by a
* validation.
*
* Events are emitted by the validation of a field. They are used to update the
* state.
*/
trait ValidationEvent

/** The event emitted when the validation is successful.
*
* It will clear any error message for a field.
*/
case object ValidEvent extends ValidationEvent

/** The event emitted when the validation is unsuccessful.
*
* @param errorMessage
* The error message.
*/
final case class InvalideEvent(errorMessage: String) extends ValidationEvent

/** The event emitted when the field is hidden (when Option is set to None).
*
* It will then ignore any validation status.
*/
case object HiddenEvent extends ValidationEvent

/** The event emitted when the field is shown (when Option is set to Some).
*
* It will then redem any validation status.
*/
case object ShownEvent extends ValidationEvent

/** Validation status.
*
* It is used to determine the status of a field.
*/
enum ValidationStatus:
case Unknown
case Valid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ package dev.cheleb.scalamigen

import scala.util.Try

/** A trait representing a validator for a type A.
*
* A validator is a function that takes a string and returns either an error
* message or a value of type A.
*/
trait Validator[A] {
def validate(str: String): Either[String, A]
}
Expand Down
136 changes: 105 additions & 31 deletions modules/core/src/main/scala/dev/cheleb/scalamigen/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ def stringForm[A](to: String => A) = new Form[A]:
syncParent()
}
)

/** A form for a secret type.
*
* The secret type is a string that should not be displayed in clear text.
*
* In general it is used for passwords, api keys, etc...
*
* Hence this sensitive data should be declared as an opaque type.
*
* @param to
* The function to convert the string to the secret type.
* @return
*/
def secretForm[A <: String](to: String => A) = new Form[A]:
override def render(
path: List[Symbol],
Expand Down Expand Up @@ -68,40 +81,101 @@ def numericForm[A](f: String => Option[A], zero: A): Form[A] = new Form[A] {
)
}

def enumForm[A](values: Array[A], f: Int => A) = new Form[A] {
/** Render form as html select.
*
* @param elements
* The elements to render.
* @param labelMapper
* The function to map the element to a label. Default is toString.
* @return
*/
def selectForm[A](
elements: Array[A],
labelMapper: A => String = (a: A) => a.toString
) =
new Form[A] {

override def render(
path: List[Symbol],
variable: Var[A],
syncParent: () => Unit
)(using
factory: WidgetFactory,
errorBus: EventBus[(String, ValidationEvent)]
): HtmlElement =
val valuesLabels = values.map(_.toString)
div(
factory
.renderSelect { idx =>
variable.set(f(idx))
syncParent()
}
.amend(
valuesLabels.map { label =>
factory.renderOption(
label,
values
.map(_.toString)
.indexOf(label),
label == variable.now().toString
)
}.toSeq
)
)
override def render(
path: List[Symbol],
variable: Var[A],
syncParent: () => Unit
)(using
factory: WidgetFactory,
errorBus: EventBus[(String, ValidationEvent)]
): HtmlElement =
val labels = elements.map(labelMapper)
div(
factory
.renderSelect { idx =>
variable.set(elements(idx))
syncParent()
}
.amend(
labels.map { label =>
factory.renderOption(
label,
elements
.map(labelMapper)
.indexOf(label),
label == labelMapper(variable.now())
)
}.toSeq
)
)

}
}

/** A form for a type A, no validation. Convenient to use for Opaque types. If
* you need validation, use a Form with a ValidationEvent.
/** Render form as html select.
*
* @param elements
* The elements to render.
* @param mapper
* The function to map the element to a value.
* @param labelMapper
* The function to map the element to a label. Default is toString.
* @return
*/
def selectMappedForm[A, B](
elements: Seq[A],
mapper: A => B,
labelMapper: A => String = (a: A) => a.toString
) =
new Form[B] {

override def render(
path: List[Symbol],
variable: Var[B],
syncParent: () => Unit
)(using
factory: WidgetFactory,
errorBus: EventBus[(String, ValidationEvent)]
): HtmlElement =
val labels = elements.map(labelMapper).zip(elements)
div(
factory
.renderSelect { idx =>
variable.set(mapper(elements(idx)))
syncParent()
}
.amend(
labels.map { (label, a) =>
factory.renderOption(
label,
elements
.map(labelMapper)
.indexOf(label),
a == variable.now()
)
}.toSeq
)
)

}

/** A form for a type A, with validation.
*
* @param validator
* The validator for the type A.
*/
def stringFormWithValidation[A](using
validator: Validator[A]
Expand Down