Skip to content

Commit

Permalink
Annotations (#201)
Browse files Browse the repository at this point in the history
* Add customisation annotations

* Update sample component in HelloWorld.scala and Person model

* feat: Update password field rendering in UI5WidgetFactory

* refactor: Simplify getSubtypeLabel method in Form.scala
  • Loading branch information
cheleb authored Aug 27, 2024
1 parent c246f35 commit f1a8195
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 33 deletions.
2 changes: 1 addition & 1 deletion examples/client/src/main/scala/HelloWorld.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ case class Sample(name: String, component: HtmlElement)

object App extends App {

val sample = Var(samples.list.component)
val sample = Var(samples.person.component)

private def item(name: String) = SideNavigation.item(
_.text := name,
Expand Down
4 changes: 3 additions & 1 deletion examples/client/src/main/scala/samples/Persons.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import samples.model.Password
import java.time.LocalDate

// Define some models
@NoPanel
case class Person(
@FieldName("First Name")
name: String,
password: Password,
birthDate: LocalDate,
Expand All @@ -37,7 +39,7 @@ given Defaultable[Pet] with
// Instance your model
val vlad =
Person(
"Vlad",
"",
Password("not a password"),
LocalDate.of(1431, 11, 8),
Pet("Batman", 666, House(2), 169),
Expand Down
19 changes: 3 additions & 16 deletions examples/client/src/main/scala/samples/model/LoginPassword.scala
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
package samples.model

import dev.cheleb.scalamigen.Form
import com.raquo.laminar.api.L.*
import dev.cheleb.scalamigen.WidgetFactory

import dev.cheleb.scalamigen.secretForm

opaque type Password = String

object Password:
def apply(password: String): Password = password
given Form[Password] with
override def render(
variable: Var[Password],
syncParent: () => Unit,
values: List[Password] = List.empty
)(using factory: WidgetFactory): HtmlElement =
factory.renderSecret
.amend(
value <-- variable.signal,
onInput.mapToValue --> { v =>
variable.set(v)
syncParent()
}
)
given Form[Password] = secretForm(apply)
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,12 @@ import scala.annotation.StaticAnnotation
* Use this annotation to specify enum values for sealed traits.
*/
class EnumValues[A](val values: Array[A]) extends StaticAnnotation

/** Annotation for field names
*
* Use this annotation to specify field names for case class fields.
*/
class FieldName(val value: String) extends StaticAnnotation

class PanelName(val value: String) extends StaticAnnotation
class NoPanel extends StaticAnnotation
59 changes: 52 additions & 7 deletions modules/core/src/main/scala/dev/cheleb/scalamigen/Form.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.raquo.airstream.state.Var
import org.scalajs.dom.HTMLDivElement
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)
Expand Down Expand Up @@ -79,22 +80,41 @@ object Form extends AutoDerivation[Form] {
variable: Var[A],
syncParent: () => Unit = () => (),
values: List[A] = List.empty
)(using factory: WidgetFactory): HtmlElement =
)(using factory: WidgetFactory): HtmlElement = {

val panel =
caseClass.annotations.find(_.isInstanceOf[PanelName]) match
case None =>
caseClass.annotations.find(_.isInstanceOf[NoPanel]) match
case None =>
Some(caseClass.typeInfo.short)
case Some(_) =>
None
case Some(value) =>
Option(value.asInstanceOf[PanelName].value)

factory
.renderPanel(caseClass.typeInfo.short)
.renderPanel(panel)
.amend(
className := "panel panel-default",
caseClass.params.map { param =>
val isOption = param.deref(variable.now()).isInstanceOf[Option[?]]

val enumValues =
if param.annotations.isEmpty then List.empty[A]
else if param.annotations(0).isInstanceOf[EnumValues[?]] then
param.annotations(0).asInstanceOf[EnumValues[A]].values.toList
else List.empty[A]
param.annotations
.find(_.isInstanceOf[EnumValues[?]]) match
case None => List.empty
case Some(value) =>
value.asInstanceOf[EnumValues[A]].values.toList

val fieldName = param.annotations
.find(_.isInstanceOf[FieldName]) match
case None => param.label
case Some(value) =>
value.asInstanceOf[FieldName].value

param.typeclass
.labelled(param.label, !isOption)
.labelled(fieldName, !isOption)
.render(
variable.zoom { a =>
Try(param.deref(a))
Expand All @@ -114,6 +134,7 @@ object Form extends AutoDerivation[Form] {
)
}.toSeq
)
}
}

/** Split a sealed trait into a form
Expand Down Expand Up @@ -154,6 +175,16 @@ object Form extends AutoDerivation[Form] {

}

def getSubtypeLabel[T](sub: Subtype[Typeclass, T, ?]): String =
sub.annotations
.collectFirst { case label: FieldName => label.value }
.getOrElse(titleCase(sub.typeInfo.short))

/** someParameterName -> Some Parameter Name camelCase -> Title Case
*/
private def titleCase(string: String): String =
string.split("(?=[A-Z])").map(_.capitalize).mkString(" ")

}

/** Use this form to render a string that can be converted to A, can be used for
Expand Down Expand Up @@ -195,3 +226,17 @@ def numericForm[A](f: String => Option[A], zero: A): Form[A] = new Form[A] {
}
)
}

def secretForm[A <: String](to: String => A) = new Form[A]:
override def render(
variable: Var[A],
syncParent: () => Unit,
values: List[A] = List.empty
)(using factory: WidgetFactory): HtmlElement =
factory.renderSecret.amend(
value <-- variable.signal,
onInput.mapToValue.map(to) --> { v =>
variable.set(v)
syncParent()
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ object LaminarWidgetFactory extends WidgetFactory:
el
)
override def renderUL(id: String): HtmlElement = ul(idAttr := id)
override def renderPanel(headerText: String): HtmlElement = div(headerText)
override def renderPanel(headerText: Option[String]): HtmlElement =
headerText match
case None => div()
case Some(headerText) =>
div(
headerText
)

override def renderSelect(f: Int => Unit): HtmlElement = select(
onChange.map(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ trait WidgetFactory:
/** Render a panel. This is a container for other widgets derived from a case
* class.
*/
def renderPanel(headerText: String): HtmlElement
def renderPanel(headerText: Option[String]): HtmlElement

/** Render an unordered list. This is a container for other widgets derived
* from a case class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ object UI5WidgetFactory extends WidgetFactory:
)

override def renderText: HtmlElement = Input(
_.showClearIcon := true
_.showClearIcon := true,
_.placeholder := "Enter text"
)
override def renderLabel(required: Boolean, name: String): HtmlElement =
Label(
Expand All @@ -38,7 +39,8 @@ object UI5WidgetFactory extends WidgetFactory:
).amend(name)

override def renderNumeric: HtmlElement = Input(
tpe := "number"
tpe := "number",
_.placeholder := "Enter number"
)
override def renderButton: HtmlElement = Button()
override def renderLink(text: String, el: EventListener[?, ?]): HtmlElement =
Expand All @@ -49,10 +51,14 @@ object UI5WidgetFactory extends WidgetFactory:
_.noDataText := "No data",
_.separators := ListSeparator.None
)
override def renderPanel(headerText: String): HtmlElement = Panel(
_.headerText := headerText,
_.headerLevel := TitleLevel.H3
)
override def renderPanel(headerText: Option[String]): HtmlElement =
headerText match
case Some(headerText) =>
Panel(
_.headerText := headerText,
_.headerLevel := TitleLevel.H3
)
case None => Panel()

override def renderSelect(f: Int => Unit): HtmlElement = Select(
_.events.onChange
Expand Down

0 comments on commit f1a8195

Please sign in to comment.