diff --git a/core/src/MessageCatalogue.scala b/core/src/MessageCatalogue.scala index b5daf9f..6319344 100644 --- a/core/src/MessageCatalogue.scala +++ b/core/src/MessageCatalogue.scala @@ -3,3 +3,4 @@ trait MessageCatalogue: def apply(id: MessageId): Option[String] + def apply(msg: UserMessage): Option[String] diff --git a/core/src/MessageCatalogue.scala b/core/src/MessageCatalogue.scala index b5daf9f..6319344 100644 --- a/core/src/MessageCatalogue.scala +++ b/core/src/MessageCatalogue.scala @@ -3,3 +3,4 @@ trait MessageCatalogue: def apply(id: MessageId): Option[String] + def apply(msg: UserMessage): Option[String] diff --git a/core/src/UserMessage.scala b/core/src/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/src/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/core/src/MessageCatalogue.scala b/core/src/MessageCatalogue.scala index b5daf9f..6319344 100644 --- a/core/src/MessageCatalogue.scala +++ b/core/src/MessageCatalogue.scala @@ -3,3 +3,4 @@ trait MessageCatalogue: def apply(id: MessageId): Option[String] + def apply(msg: UserMessage): Option[String] diff --git a/core/src/UserMessage.scala b/core/src/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/src/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/ui/components/src/ui/JsonMessageCatalogue.scala b/ui/components/src/ui/JsonMessageCatalogue.scala index a502713..aea3c6f 100644 --- a/ui/components/src/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/ui/JsonMessageCatalogue.scala @@ -3,6 +3,8 @@ import core.{MessageCatalogue, MessageId} import scala.scalajs.js +import works.iterative.core.UserMessage +import java.text.MessageFormat // TODO: support hierarchical json structure trait JsonMessageCatalogue extends MessageCatalogue: @@ -10,3 +12,6 @@ override def apply(id: MessageId): Option[String] = messages.get(id.toString) + + override def apply(msg: UserMessage): Option[String] = + apply(msg.id).map(_.format(msg.args: _*)) diff --git a/core/src/MessageCatalogue.scala b/core/src/MessageCatalogue.scala index b5daf9f..6319344 100644 --- a/core/src/MessageCatalogue.scala +++ b/core/src/MessageCatalogue.scala @@ -3,3 +3,4 @@ trait MessageCatalogue: def apply(id: MessageId): Option[String] + def apply(msg: UserMessage): Option[String] diff --git a/core/src/UserMessage.scala b/core/src/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/src/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/ui/components/src/ui/JsonMessageCatalogue.scala b/ui/components/src/ui/JsonMessageCatalogue.scala index a502713..aea3c6f 100644 --- a/ui/components/src/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/ui/JsonMessageCatalogue.scala @@ -3,6 +3,8 @@ import core.{MessageCatalogue, MessageId} import scala.scalajs.js +import works.iterative.core.UserMessage +import java.text.MessageFormat // TODO: support hierarchical json structure trait JsonMessageCatalogue extends MessageCatalogue: @@ -10,3 +12,6 @@ override def apply(id: MessageId): Option[String] = messages.get(id.toString) + + override def apply(msg: UserMessage): Option[String] = + apply(msg.id).map(_.format(msg.args: _*)) diff --git a/ui/components/src/ui/components/tailwind/Switch.scala b/ui/components/src/ui/components/tailwind/Switch.scala deleted file mode 100644 index dd24ed0..0000000 --- a/ui/components/src/ui/components/tailwind/Switch.scala +++ /dev/null @@ -1,38 +0,0 @@ -package works.iterative.ui.components.tailwind - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.UIString - -object Switch { - def apply(toggle: Var[Boolean], name: Option[UIString] = None): HtmlElement = - div( - cls := "flex items-center", - button( - tpe := "button", - cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - cls <-- toggle.signal.map(a => - if a then "bg-indigo-600" else "bg-gray-200" - ), - role := "switch", - dataAttr("aria-checked") := "false", - dataAttr("aria-labelledby") := "active-only-label", - span( - dataAttr("aria-hidden") := "true", - cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", - cls <-- toggle.signal.map(a => - if a then "translate-x-5" else "translate-x-0" - ) - ), - composeEvents(onClick)( - _.sample(toggle.signal).map(a => !a) - ) --> toggle.writer - ), - name.map(n => - span( - cls := "ml-3", - idAttr := "active-only-label", - span(cls := "text-sm font-medium text-gray-900", n) - ) - ) - ) -} diff --git a/core/src/MessageCatalogue.scala b/core/src/MessageCatalogue.scala index b5daf9f..6319344 100644 --- a/core/src/MessageCatalogue.scala +++ b/core/src/MessageCatalogue.scala @@ -3,3 +3,4 @@ trait MessageCatalogue: def apply(id: MessageId): Option[String] + def apply(msg: UserMessage): Option[String] diff --git a/core/src/UserMessage.scala b/core/src/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/src/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/ui/components/src/ui/JsonMessageCatalogue.scala b/ui/components/src/ui/JsonMessageCatalogue.scala index a502713..aea3c6f 100644 --- a/ui/components/src/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/ui/JsonMessageCatalogue.scala @@ -3,6 +3,8 @@ import core.{MessageCatalogue, MessageId} import scala.scalajs.js +import works.iterative.core.UserMessage +import java.text.MessageFormat // TODO: support hierarchical json structure trait JsonMessageCatalogue extends MessageCatalogue: @@ -10,3 +12,6 @@ override def apply(id: MessageId): Option[String] = messages.get(id.toString) + + override def apply(msg: UserMessage): Option[String] = + apply(msg.id).map(_.format(msg.args: _*)) diff --git a/ui/components/src/ui/components/tailwind/Switch.scala b/ui/components/src/ui/components/tailwind/Switch.scala deleted file mode 100644 index dd24ed0..0000000 --- a/ui/components/src/ui/components/tailwind/Switch.scala +++ /dev/null @@ -1,38 +0,0 @@ -package works.iterative.ui.components.tailwind - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.UIString - -object Switch { - def apply(toggle: Var[Boolean], name: Option[UIString] = None): HtmlElement = - div( - cls := "flex items-center", - button( - tpe := "button", - cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - cls <-- toggle.signal.map(a => - if a then "bg-indigo-600" else "bg-gray-200" - ), - role := "switch", - dataAttr("aria-checked") := "false", - dataAttr("aria-labelledby") := "active-only-label", - span( - dataAttr("aria-hidden") := "true", - cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", - cls <-- toggle.signal.map(a => - if a then "translate-x-5" else "translate-x-0" - ) - ), - composeEvents(onClick)( - _.sample(toggle.signal).map(a => !a) - ) --> toggle.writer - ), - name.map(n => - span( - cls := "ml-3", - idAttr := "active-only-label", - span(cls := "text-sm font-medium text-gray-900", n) - ) - ) - ) -} diff --git a/ui/components/src/ui/components/tailwind/form/Form.scala b/ui/components/src/ui/components/tailwind/form/Form.scala index 1b41dee..e232ae1 100644 --- a/ui/components/src/ui/components/tailwind/form/Form.scala +++ b/ui/components/src/ui/components/tailwind/form/Form.scala @@ -9,6 +9,9 @@ val Section = FormSection val Row = FormRow + @deprecated( + "use specific form instance's form method (see form.LabelsOnLeft)" + ) def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = form( cls := "space-y-8 divide-y divide-gray-200", diff --git a/core/src/MessageCatalogue.scala b/core/src/MessageCatalogue.scala index b5daf9f..6319344 100644 --- a/core/src/MessageCatalogue.scala +++ b/core/src/MessageCatalogue.scala @@ -3,3 +3,4 @@ trait MessageCatalogue: def apply(id: MessageId): Option[String] + def apply(msg: UserMessage): Option[String] diff --git a/core/src/UserMessage.scala b/core/src/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/src/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/ui/components/src/ui/JsonMessageCatalogue.scala b/ui/components/src/ui/JsonMessageCatalogue.scala index a502713..aea3c6f 100644 --- a/ui/components/src/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/ui/JsonMessageCatalogue.scala @@ -3,6 +3,8 @@ import core.{MessageCatalogue, MessageId} import scala.scalajs.js +import works.iterative.core.UserMessage +import java.text.MessageFormat // TODO: support hierarchical json structure trait JsonMessageCatalogue extends MessageCatalogue: @@ -10,3 +12,6 @@ override def apply(id: MessageId): Option[String] = messages.get(id.toString) + + override def apply(msg: UserMessage): Option[String] = + apply(msg.id).map(_.format(msg.args: _*)) diff --git a/ui/components/src/ui/components/tailwind/Switch.scala b/ui/components/src/ui/components/tailwind/Switch.scala deleted file mode 100644 index dd24ed0..0000000 --- a/ui/components/src/ui/components/tailwind/Switch.scala +++ /dev/null @@ -1,38 +0,0 @@ -package works.iterative.ui.components.tailwind - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.UIString - -object Switch { - def apply(toggle: Var[Boolean], name: Option[UIString] = None): HtmlElement = - div( - cls := "flex items-center", - button( - tpe := "button", - cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - cls <-- toggle.signal.map(a => - if a then "bg-indigo-600" else "bg-gray-200" - ), - role := "switch", - dataAttr("aria-checked") := "false", - dataAttr("aria-labelledby") := "active-only-label", - span( - dataAttr("aria-hidden") := "true", - cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", - cls <-- toggle.signal.map(a => - if a then "translate-x-5" else "translate-x-0" - ) - ), - composeEvents(onClick)( - _.sample(toggle.signal).map(a => !a) - ) --> toggle.writer - ), - name.map(n => - span( - cls := "ml-3", - idAttr := "active-only-label", - span(cls := "text-sm font-medium text-gray-900", n) - ) - ) - ) -} diff --git a/ui/components/src/ui/components/tailwind/form/Form.scala b/ui/components/src/ui/components/tailwind/form/Form.scala index 1b41dee..e232ae1 100644 --- a/ui/components/src/ui/components/tailwind/form/Form.scala +++ b/ui/components/src/ui/components/tailwind/form/Form.scala @@ -9,6 +9,9 @@ val Section = FormSection val Row = FormRow + @deprecated( + "use specific form instance's form method (see form.LabelsOnLeft)" + ) def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = form( cls := "space-y-8 divide-y divide-gray-200", diff --git a/ui/components/src/ui/components/tailwind/form/FormBody.scala b/ui/components/src/ui/components/tailwind/form/FormBody.scala index 89abc9c..f6afcd0 100644 --- a/ui/components/src/ui/components/tailwind/form/FormBody.scala +++ b/ui/components/src/ui/components/tailwind/form/FormBody.scala @@ -3,6 +3,7 @@ import com.raquo.laminar.api.L.{*, given} object FormBody: + @deprecated("use specific form's 'body' method (see LabelsOnLeft)") def apply(sections: HtmlElement*): HtmlElement = div( cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", diff --git a/core/src/MessageCatalogue.scala b/core/src/MessageCatalogue.scala index b5daf9f..6319344 100644 --- a/core/src/MessageCatalogue.scala +++ b/core/src/MessageCatalogue.scala @@ -3,3 +3,4 @@ trait MessageCatalogue: def apply(id: MessageId): Option[String] + def apply(msg: UserMessage): Option[String] diff --git a/core/src/UserMessage.scala b/core/src/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/src/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/ui/components/src/ui/JsonMessageCatalogue.scala b/ui/components/src/ui/JsonMessageCatalogue.scala index a502713..aea3c6f 100644 --- a/ui/components/src/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/ui/JsonMessageCatalogue.scala @@ -3,6 +3,8 @@ import core.{MessageCatalogue, MessageId} import scala.scalajs.js +import works.iterative.core.UserMessage +import java.text.MessageFormat // TODO: support hierarchical json structure trait JsonMessageCatalogue extends MessageCatalogue: @@ -10,3 +12,6 @@ override def apply(id: MessageId): Option[String] = messages.get(id.toString) + + override def apply(msg: UserMessage): Option[String] = + apply(msg.id).map(_.format(msg.args: _*)) diff --git a/ui/components/src/ui/components/tailwind/Switch.scala b/ui/components/src/ui/components/tailwind/Switch.scala deleted file mode 100644 index dd24ed0..0000000 --- a/ui/components/src/ui/components/tailwind/Switch.scala +++ /dev/null @@ -1,38 +0,0 @@ -package works.iterative.ui.components.tailwind - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.UIString - -object Switch { - def apply(toggle: Var[Boolean], name: Option[UIString] = None): HtmlElement = - div( - cls := "flex items-center", - button( - tpe := "button", - cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - cls <-- toggle.signal.map(a => - if a then "bg-indigo-600" else "bg-gray-200" - ), - role := "switch", - dataAttr("aria-checked") := "false", - dataAttr("aria-labelledby") := "active-only-label", - span( - dataAttr("aria-hidden") := "true", - cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", - cls <-- toggle.signal.map(a => - if a then "translate-x-5" else "translate-x-0" - ) - ), - composeEvents(onClick)( - _.sample(toggle.signal).map(a => !a) - ) --> toggle.writer - ), - name.map(n => - span( - cls := "ml-3", - idAttr := "active-only-label", - span(cls := "text-sm font-medium text-gray-900", n) - ) - ) - ) -} diff --git a/ui/components/src/ui/components/tailwind/form/Form.scala b/ui/components/src/ui/components/tailwind/form/Form.scala index 1b41dee..e232ae1 100644 --- a/ui/components/src/ui/components/tailwind/form/Form.scala +++ b/ui/components/src/ui/components/tailwind/form/Form.scala @@ -9,6 +9,9 @@ val Section = FormSection val Row = FormRow + @deprecated( + "use specific form instance's form method (see form.LabelsOnLeft)" + ) def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = form( cls := "space-y-8 divide-y divide-gray-200", diff --git a/ui/components/src/ui/components/tailwind/form/FormBody.scala b/ui/components/src/ui/components/tailwind/form/FormBody.scala index 89abc9c..f6afcd0 100644 --- a/ui/components/src/ui/components/tailwind/form/FormBody.scala +++ b/ui/components/src/ui/components/tailwind/form/FormBody.scala @@ -3,6 +3,7 @@ import com.raquo.laminar.api.L.{*, given} object FormBody: + @deprecated("use specific form's 'body' method (see LabelsOnLeft)") def apply(sections: HtmlElement*): HtmlElement = div( cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", diff --git a/ui/components/src/ui/components/tailwind/form/FormCodec.scala b/ui/components/src/ui/components/tailwind/form/FormCodec.scala index 5c78b24..7b43f8d 100644 --- a/ui/components/src/ui/components/tailwind/form/FormCodec.scala +++ b/ui/components/src/ui/components/tailwind/form/FormCodec.scala @@ -10,26 +10,36 @@ import java.time.LocalDate import scala.util.Try -trait FormCodec[V]: - def toForm(v: V): String - def toValue(r: String): Validated[V] +trait FormCodec[V, A]: + def toForm(v: V): A + def toValue(r: A): Validated[V] object FormCodec: - given FormCodec[PlainMultiLine] with + given FormCodec[PlainMultiLine, String] with override def toForm(v: PlainMultiLine): String = v.toString override def toValue(r: String): Validated[PlainMultiLine] = PlainMultiLine(r).mapError(e => InvalidValue(e)) - given plainMultiLineCodec: FormCodec[Option[PlainMultiLine]] with + given plainMultiLineCodec: FormCodec[Option[PlainMultiLine], String] with override def toForm(v: Option[PlainMultiLine]): String = v match case Some(t) => t.toString case _ => "" override def toValue(r: String): Validated[Option[PlainMultiLine]] = PlainMultiLine.opt(r).mapError(e => InvalidValue(e)) - given optionLocalDateCodec: FormCodec[Option[LocalDate]] with + given optionLocalDateCodec: FormCodec[Option[LocalDate], String] with val df = DateTimeFormatter.ofPattern("yyyy-MM-dd") override def toForm(v: Option[LocalDate]): String = v.map(df.format(_)).getOrElse("") override def toValue(r: String): Validated[Option[LocalDate]] = Validation.succeed(Try(LocalDate.parse(r, df)).toOption) + + given optionBooleanCodec: FormCodec[Option[Boolean], Boolean] with + override def toForm(v: Option[Boolean]): Boolean = v.getOrElse(false) + override def toValue(r: Boolean): Validated[Option[Boolean]] = + Validation.succeed(Some(r)) + +trait LowPriorityFormCodecs: + given identityCodec[A]: FormCodec[A, A] with + override def toForm(v: A): A = v + override def toValue(r: A): Validated[A] = Validation.succeed(r) diff --git a/core/src/MessageCatalogue.scala b/core/src/MessageCatalogue.scala index b5daf9f..6319344 100644 --- a/core/src/MessageCatalogue.scala +++ b/core/src/MessageCatalogue.scala @@ -3,3 +3,4 @@ trait MessageCatalogue: def apply(id: MessageId): Option[String] + def apply(msg: UserMessage): Option[String] diff --git a/core/src/UserMessage.scala b/core/src/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/src/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/ui/components/src/ui/JsonMessageCatalogue.scala b/ui/components/src/ui/JsonMessageCatalogue.scala index a502713..aea3c6f 100644 --- a/ui/components/src/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/ui/JsonMessageCatalogue.scala @@ -3,6 +3,8 @@ import core.{MessageCatalogue, MessageId} import scala.scalajs.js +import works.iterative.core.UserMessage +import java.text.MessageFormat // TODO: support hierarchical json structure trait JsonMessageCatalogue extends MessageCatalogue: @@ -10,3 +12,6 @@ override def apply(id: MessageId): Option[String] = messages.get(id.toString) + + override def apply(msg: UserMessage): Option[String] = + apply(msg.id).map(_.format(msg.args: _*)) diff --git a/ui/components/src/ui/components/tailwind/Switch.scala b/ui/components/src/ui/components/tailwind/Switch.scala deleted file mode 100644 index dd24ed0..0000000 --- a/ui/components/src/ui/components/tailwind/Switch.scala +++ /dev/null @@ -1,38 +0,0 @@ -package works.iterative.ui.components.tailwind - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.UIString - -object Switch { - def apply(toggle: Var[Boolean], name: Option[UIString] = None): HtmlElement = - div( - cls := "flex items-center", - button( - tpe := "button", - cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - cls <-- toggle.signal.map(a => - if a then "bg-indigo-600" else "bg-gray-200" - ), - role := "switch", - dataAttr("aria-checked") := "false", - dataAttr("aria-labelledby") := "active-only-label", - span( - dataAttr("aria-hidden") := "true", - cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", - cls <-- toggle.signal.map(a => - if a then "translate-x-5" else "translate-x-0" - ) - ), - composeEvents(onClick)( - _.sample(toggle.signal).map(a => !a) - ) --> toggle.writer - ), - name.map(n => - span( - cls := "ml-3", - idAttr := "active-only-label", - span(cls := "text-sm font-medium text-gray-900", n) - ) - ) - ) -} diff --git a/ui/components/src/ui/components/tailwind/form/Form.scala b/ui/components/src/ui/components/tailwind/form/Form.scala index 1b41dee..e232ae1 100644 --- a/ui/components/src/ui/components/tailwind/form/Form.scala +++ b/ui/components/src/ui/components/tailwind/form/Form.scala @@ -9,6 +9,9 @@ val Section = FormSection val Row = FormRow + @deprecated( + "use specific form instance's form method (see form.LabelsOnLeft)" + ) def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = form( cls := "space-y-8 divide-y divide-gray-200", diff --git a/ui/components/src/ui/components/tailwind/form/FormBody.scala b/ui/components/src/ui/components/tailwind/form/FormBody.scala index 89abc9c..f6afcd0 100644 --- a/ui/components/src/ui/components/tailwind/form/FormBody.scala +++ b/ui/components/src/ui/components/tailwind/form/FormBody.scala @@ -3,6 +3,7 @@ import com.raquo.laminar.api.L.{*, given} object FormBody: + @deprecated("use specific form's 'body' method (see LabelsOnLeft)") def apply(sections: HtmlElement*): HtmlElement = div( cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", diff --git a/ui/components/src/ui/components/tailwind/form/FormCodec.scala b/ui/components/src/ui/components/tailwind/form/FormCodec.scala index 5c78b24..7b43f8d 100644 --- a/ui/components/src/ui/components/tailwind/form/FormCodec.scala +++ b/ui/components/src/ui/components/tailwind/form/FormCodec.scala @@ -10,26 +10,36 @@ import java.time.LocalDate import scala.util.Try -trait FormCodec[V]: - def toForm(v: V): String - def toValue(r: String): Validated[V] +trait FormCodec[V, A]: + def toForm(v: V): A + def toValue(r: A): Validated[V] object FormCodec: - given FormCodec[PlainMultiLine] with + given FormCodec[PlainMultiLine, String] with override def toForm(v: PlainMultiLine): String = v.toString override def toValue(r: String): Validated[PlainMultiLine] = PlainMultiLine(r).mapError(e => InvalidValue(e)) - given plainMultiLineCodec: FormCodec[Option[PlainMultiLine]] with + given plainMultiLineCodec: FormCodec[Option[PlainMultiLine], String] with override def toForm(v: Option[PlainMultiLine]): String = v match case Some(t) => t.toString case _ => "" override def toValue(r: String): Validated[Option[PlainMultiLine]] = PlainMultiLine.opt(r).mapError(e => InvalidValue(e)) - given optionLocalDateCodec: FormCodec[Option[LocalDate]] with + given optionLocalDateCodec: FormCodec[Option[LocalDate], String] with val df = DateTimeFormatter.ofPattern("yyyy-MM-dd") override def toForm(v: Option[LocalDate]): String = v.map(df.format(_)).getOrElse("") override def toValue(r: String): Validated[Option[LocalDate]] = Validation.succeed(Try(LocalDate.parse(r, df)).toOption) + + given optionBooleanCodec: FormCodec[Option[Boolean], Boolean] with + override def toForm(v: Option[Boolean]): Boolean = v.getOrElse(false) + override def toValue(r: Boolean): Validated[Option[Boolean]] = + Validation.succeed(Some(r)) + +trait LowPriorityFormCodecs: + given identityCodec[A]: FormCodec[A, A] with + override def toForm(v: A): A = v + override def toValue(r: A): Validated[A] = Validation.succeed(r) diff --git a/ui/components/src/ui/components/tailwind/form/FormFields.scala b/ui/components/src/ui/components/tailwind/form/FormFields.scala index a2e4cc6..322e0d8 100644 --- a/ui/components/src/ui/components/tailwind/form/FormFields.scala +++ b/ui/components/src/ui/components/tailwind/form/FormFields.scala @@ -5,6 +5,7 @@ import org.scalajs.dom object FormFields: + @deprecated("use LabelsOnLeft.fields") def apply( mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* ): HtmlElement = diff --git a/core/src/MessageCatalogue.scala b/core/src/MessageCatalogue.scala index b5daf9f..6319344 100644 --- a/core/src/MessageCatalogue.scala +++ b/core/src/MessageCatalogue.scala @@ -3,3 +3,4 @@ trait MessageCatalogue: def apply(id: MessageId): Option[String] + def apply(msg: UserMessage): Option[String] diff --git a/core/src/UserMessage.scala b/core/src/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/src/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/ui/components/src/ui/JsonMessageCatalogue.scala b/ui/components/src/ui/JsonMessageCatalogue.scala index a502713..aea3c6f 100644 --- a/ui/components/src/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/ui/JsonMessageCatalogue.scala @@ -3,6 +3,8 @@ import core.{MessageCatalogue, MessageId} import scala.scalajs.js +import works.iterative.core.UserMessage +import java.text.MessageFormat // TODO: support hierarchical json structure trait JsonMessageCatalogue extends MessageCatalogue: @@ -10,3 +12,6 @@ override def apply(id: MessageId): Option[String] = messages.get(id.toString) + + override def apply(msg: UserMessage): Option[String] = + apply(msg.id).map(_.format(msg.args: _*)) diff --git a/ui/components/src/ui/components/tailwind/Switch.scala b/ui/components/src/ui/components/tailwind/Switch.scala deleted file mode 100644 index dd24ed0..0000000 --- a/ui/components/src/ui/components/tailwind/Switch.scala +++ /dev/null @@ -1,38 +0,0 @@ -package works.iterative.ui.components.tailwind - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.UIString - -object Switch { - def apply(toggle: Var[Boolean], name: Option[UIString] = None): HtmlElement = - div( - cls := "flex items-center", - button( - tpe := "button", - cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - cls <-- toggle.signal.map(a => - if a then "bg-indigo-600" else "bg-gray-200" - ), - role := "switch", - dataAttr("aria-checked") := "false", - dataAttr("aria-labelledby") := "active-only-label", - span( - dataAttr("aria-hidden") := "true", - cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", - cls <-- toggle.signal.map(a => - if a then "translate-x-5" else "translate-x-0" - ) - ), - composeEvents(onClick)( - _.sample(toggle.signal).map(a => !a) - ) --> toggle.writer - ), - name.map(n => - span( - cls := "ml-3", - idAttr := "active-only-label", - span(cls := "text-sm font-medium text-gray-900", n) - ) - ) - ) -} diff --git a/ui/components/src/ui/components/tailwind/form/Form.scala b/ui/components/src/ui/components/tailwind/form/Form.scala index 1b41dee..e232ae1 100644 --- a/ui/components/src/ui/components/tailwind/form/Form.scala +++ b/ui/components/src/ui/components/tailwind/form/Form.scala @@ -9,6 +9,9 @@ val Section = FormSection val Row = FormRow + @deprecated( + "use specific form instance's form method (see form.LabelsOnLeft)" + ) def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = form( cls := "space-y-8 divide-y divide-gray-200", diff --git a/ui/components/src/ui/components/tailwind/form/FormBody.scala b/ui/components/src/ui/components/tailwind/form/FormBody.scala index 89abc9c..f6afcd0 100644 --- a/ui/components/src/ui/components/tailwind/form/FormBody.scala +++ b/ui/components/src/ui/components/tailwind/form/FormBody.scala @@ -3,6 +3,7 @@ import com.raquo.laminar.api.L.{*, given} object FormBody: + @deprecated("use specific form's 'body' method (see LabelsOnLeft)") def apply(sections: HtmlElement*): HtmlElement = div( cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", diff --git a/ui/components/src/ui/components/tailwind/form/FormCodec.scala b/ui/components/src/ui/components/tailwind/form/FormCodec.scala index 5c78b24..7b43f8d 100644 --- a/ui/components/src/ui/components/tailwind/form/FormCodec.scala +++ b/ui/components/src/ui/components/tailwind/form/FormCodec.scala @@ -10,26 +10,36 @@ import java.time.LocalDate import scala.util.Try -trait FormCodec[V]: - def toForm(v: V): String - def toValue(r: String): Validated[V] +trait FormCodec[V, A]: + def toForm(v: V): A + def toValue(r: A): Validated[V] object FormCodec: - given FormCodec[PlainMultiLine] with + given FormCodec[PlainMultiLine, String] with override def toForm(v: PlainMultiLine): String = v.toString override def toValue(r: String): Validated[PlainMultiLine] = PlainMultiLine(r).mapError(e => InvalidValue(e)) - given plainMultiLineCodec: FormCodec[Option[PlainMultiLine]] with + given plainMultiLineCodec: FormCodec[Option[PlainMultiLine], String] with override def toForm(v: Option[PlainMultiLine]): String = v match case Some(t) => t.toString case _ => "" override def toValue(r: String): Validated[Option[PlainMultiLine]] = PlainMultiLine.opt(r).mapError(e => InvalidValue(e)) - given optionLocalDateCodec: FormCodec[Option[LocalDate]] with + given optionLocalDateCodec: FormCodec[Option[LocalDate], String] with val df = DateTimeFormatter.ofPattern("yyyy-MM-dd") override def toForm(v: Option[LocalDate]): String = v.map(df.format(_)).getOrElse("") override def toValue(r: String): Validated[Option[LocalDate]] = Validation.succeed(Try(LocalDate.parse(r, df)).toOption) + + given optionBooleanCodec: FormCodec[Option[Boolean], Boolean] with + override def toForm(v: Option[Boolean]): Boolean = v.getOrElse(false) + override def toValue(r: Boolean): Validated[Option[Boolean]] = + Validation.succeed(Some(r)) + +trait LowPriorityFormCodecs: + given identityCodec[A]: FormCodec[A, A] with + override def toForm(v: A): A = v + override def toValue(r: A): Validated[A] = Validation.succeed(r) diff --git a/ui/components/src/ui/components/tailwind/form/FormFields.scala b/ui/components/src/ui/components/tailwind/form/FormFields.scala index a2e4cc6..322e0d8 100644 --- a/ui/components/src/ui/components/tailwind/form/FormFields.scala +++ b/ui/components/src/ui/components/tailwind/form/FormFields.scala @@ -5,6 +5,7 @@ import org.scalajs.dom object FormFields: + @deprecated("use LabelsOnLeft.fields") def apply( mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* ): HtmlElement = diff --git a/ui/components/src/ui/components/tailwind/form/FormHeader.scala b/ui/components/src/ui/components/tailwind/form/FormHeader.scala index 4083841..3680628 100644 --- a/ui/components/src/ui/components/tailwind/form/FormHeader.scala +++ b/ui/components/src/ui/components/tailwind/form/FormHeader.scala @@ -4,6 +4,7 @@ object FormHeader: case class ViewModel(header: String, description: String) + @deprecated("use LabelsOnLeft.header") def apply(m: ViewModel): HtmlElement = div( h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), diff --git a/core/src/MessageCatalogue.scala b/core/src/MessageCatalogue.scala index b5daf9f..6319344 100644 --- a/core/src/MessageCatalogue.scala +++ b/core/src/MessageCatalogue.scala @@ -3,3 +3,4 @@ trait MessageCatalogue: def apply(id: MessageId): Option[String] + def apply(msg: UserMessage): Option[String] diff --git a/core/src/UserMessage.scala b/core/src/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/src/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/ui/components/src/ui/JsonMessageCatalogue.scala b/ui/components/src/ui/JsonMessageCatalogue.scala index a502713..aea3c6f 100644 --- a/ui/components/src/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/ui/JsonMessageCatalogue.scala @@ -3,6 +3,8 @@ import core.{MessageCatalogue, MessageId} import scala.scalajs.js +import works.iterative.core.UserMessage +import java.text.MessageFormat // TODO: support hierarchical json structure trait JsonMessageCatalogue extends MessageCatalogue: @@ -10,3 +12,6 @@ override def apply(id: MessageId): Option[String] = messages.get(id.toString) + + override def apply(msg: UserMessage): Option[String] = + apply(msg.id).map(_.format(msg.args: _*)) diff --git a/ui/components/src/ui/components/tailwind/Switch.scala b/ui/components/src/ui/components/tailwind/Switch.scala deleted file mode 100644 index dd24ed0..0000000 --- a/ui/components/src/ui/components/tailwind/Switch.scala +++ /dev/null @@ -1,38 +0,0 @@ -package works.iterative.ui.components.tailwind - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.UIString - -object Switch { - def apply(toggle: Var[Boolean], name: Option[UIString] = None): HtmlElement = - div( - cls := "flex items-center", - button( - tpe := "button", - cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - cls <-- toggle.signal.map(a => - if a then "bg-indigo-600" else "bg-gray-200" - ), - role := "switch", - dataAttr("aria-checked") := "false", - dataAttr("aria-labelledby") := "active-only-label", - span( - dataAttr("aria-hidden") := "true", - cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", - cls <-- toggle.signal.map(a => - if a then "translate-x-5" else "translate-x-0" - ) - ), - composeEvents(onClick)( - _.sample(toggle.signal).map(a => !a) - ) --> toggle.writer - ), - name.map(n => - span( - cls := "ml-3", - idAttr := "active-only-label", - span(cls := "text-sm font-medium text-gray-900", n) - ) - ) - ) -} diff --git a/ui/components/src/ui/components/tailwind/form/Form.scala b/ui/components/src/ui/components/tailwind/form/Form.scala index 1b41dee..e232ae1 100644 --- a/ui/components/src/ui/components/tailwind/form/Form.scala +++ b/ui/components/src/ui/components/tailwind/form/Form.scala @@ -9,6 +9,9 @@ val Section = FormSection val Row = FormRow + @deprecated( + "use specific form instance's form method (see form.LabelsOnLeft)" + ) def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = form( cls := "space-y-8 divide-y divide-gray-200", diff --git a/ui/components/src/ui/components/tailwind/form/FormBody.scala b/ui/components/src/ui/components/tailwind/form/FormBody.scala index 89abc9c..f6afcd0 100644 --- a/ui/components/src/ui/components/tailwind/form/FormBody.scala +++ b/ui/components/src/ui/components/tailwind/form/FormBody.scala @@ -3,6 +3,7 @@ import com.raquo.laminar.api.L.{*, given} object FormBody: + @deprecated("use specific form's 'body' method (see LabelsOnLeft)") def apply(sections: HtmlElement*): HtmlElement = div( cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", diff --git a/ui/components/src/ui/components/tailwind/form/FormCodec.scala b/ui/components/src/ui/components/tailwind/form/FormCodec.scala index 5c78b24..7b43f8d 100644 --- a/ui/components/src/ui/components/tailwind/form/FormCodec.scala +++ b/ui/components/src/ui/components/tailwind/form/FormCodec.scala @@ -10,26 +10,36 @@ import java.time.LocalDate import scala.util.Try -trait FormCodec[V]: - def toForm(v: V): String - def toValue(r: String): Validated[V] +trait FormCodec[V, A]: + def toForm(v: V): A + def toValue(r: A): Validated[V] object FormCodec: - given FormCodec[PlainMultiLine] with + given FormCodec[PlainMultiLine, String] with override def toForm(v: PlainMultiLine): String = v.toString override def toValue(r: String): Validated[PlainMultiLine] = PlainMultiLine(r).mapError(e => InvalidValue(e)) - given plainMultiLineCodec: FormCodec[Option[PlainMultiLine]] with + given plainMultiLineCodec: FormCodec[Option[PlainMultiLine], String] with override def toForm(v: Option[PlainMultiLine]): String = v match case Some(t) => t.toString case _ => "" override def toValue(r: String): Validated[Option[PlainMultiLine]] = PlainMultiLine.opt(r).mapError(e => InvalidValue(e)) - given optionLocalDateCodec: FormCodec[Option[LocalDate]] with + given optionLocalDateCodec: FormCodec[Option[LocalDate], String] with val df = DateTimeFormatter.ofPattern("yyyy-MM-dd") override def toForm(v: Option[LocalDate]): String = v.map(df.format(_)).getOrElse("") override def toValue(r: String): Validated[Option[LocalDate]] = Validation.succeed(Try(LocalDate.parse(r, df)).toOption) + + given optionBooleanCodec: FormCodec[Option[Boolean], Boolean] with + override def toForm(v: Option[Boolean]): Boolean = v.getOrElse(false) + override def toValue(r: Boolean): Validated[Option[Boolean]] = + Validation.succeed(Some(r)) + +trait LowPriorityFormCodecs: + given identityCodec[A]: FormCodec[A, A] with + override def toForm(v: A): A = v + override def toValue(r: A): Validated[A] = Validation.succeed(r) diff --git a/ui/components/src/ui/components/tailwind/form/FormFields.scala b/ui/components/src/ui/components/tailwind/form/FormFields.scala index a2e4cc6..322e0d8 100644 --- a/ui/components/src/ui/components/tailwind/form/FormFields.scala +++ b/ui/components/src/ui/components/tailwind/form/FormFields.scala @@ -5,6 +5,7 @@ import org.scalajs.dom object FormFields: + @deprecated("use LabelsOnLeft.fields") def apply( mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* ): HtmlElement = diff --git a/ui/components/src/ui/components/tailwind/form/FormHeader.scala b/ui/components/src/ui/components/tailwind/form/FormHeader.scala index 4083841..3680628 100644 --- a/ui/components/src/ui/components/tailwind/form/FormHeader.scala +++ b/ui/components/src/ui/components/tailwind/form/FormHeader.scala @@ -4,6 +4,7 @@ object FormHeader: case class ViewModel(header: String, description: String) + @deprecated("use LabelsOnLeft.header") def apply(m: ViewModel): HtmlElement = div( h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), diff --git a/ui/components/src/ui/components/tailwind/form/FormInput.scala b/ui/components/src/ui/components/tailwind/form/FormInput.scala index 6422870..73ed43b 100644 --- a/ui/components/src/ui/components/tailwind/form/FormInput.scala +++ b/ui/components/src/ui/components/tailwind/form/FormInput.scala @@ -5,6 +5,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate +import works.iterative.ui.components.tailwind.ComponentContext trait FormInput[V]: def render( @@ -12,9 +13,15 @@ updates: Observer[Validated[V]] ): HtmlElement + def validate(v: V => Validated[V]): FormInput[V] = + (property: Property[V], updates: Observer[Validated[V]]) => + this.render(property, updates.contramap(_.flatMap(v))) + object FormInput: given plainMultiLineInput: FormInput[PlainMultiLine] = TextArea() given optionPlainMultiLineInput: FormInput[Option[PlainMultiLine]] = TextArea() given optionLocalDateInput: FormInput[Option[LocalDate]] = Inputs.OptionDateInput() + given optionBooleanInput(using ComponentContext): FormInput[Option[Boolean]] = + Switch() diff --git a/core/src/MessageCatalogue.scala b/core/src/MessageCatalogue.scala index b5daf9f..6319344 100644 --- a/core/src/MessageCatalogue.scala +++ b/core/src/MessageCatalogue.scala @@ -3,3 +3,4 @@ trait MessageCatalogue: def apply(id: MessageId): Option[String] + def apply(msg: UserMessage): Option[String] diff --git a/core/src/UserMessage.scala b/core/src/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/src/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/ui/components/src/ui/JsonMessageCatalogue.scala b/ui/components/src/ui/JsonMessageCatalogue.scala index a502713..aea3c6f 100644 --- a/ui/components/src/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/ui/JsonMessageCatalogue.scala @@ -3,6 +3,8 @@ import core.{MessageCatalogue, MessageId} import scala.scalajs.js +import works.iterative.core.UserMessage +import java.text.MessageFormat // TODO: support hierarchical json structure trait JsonMessageCatalogue extends MessageCatalogue: @@ -10,3 +12,6 @@ override def apply(id: MessageId): Option[String] = messages.get(id.toString) + + override def apply(msg: UserMessage): Option[String] = + apply(msg.id).map(_.format(msg.args: _*)) diff --git a/ui/components/src/ui/components/tailwind/Switch.scala b/ui/components/src/ui/components/tailwind/Switch.scala deleted file mode 100644 index dd24ed0..0000000 --- a/ui/components/src/ui/components/tailwind/Switch.scala +++ /dev/null @@ -1,38 +0,0 @@ -package works.iterative.ui.components.tailwind - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.UIString - -object Switch { - def apply(toggle: Var[Boolean], name: Option[UIString] = None): HtmlElement = - div( - cls := "flex items-center", - button( - tpe := "button", - cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - cls <-- toggle.signal.map(a => - if a then "bg-indigo-600" else "bg-gray-200" - ), - role := "switch", - dataAttr("aria-checked") := "false", - dataAttr("aria-labelledby") := "active-only-label", - span( - dataAttr("aria-hidden") := "true", - cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", - cls <-- toggle.signal.map(a => - if a then "translate-x-5" else "translate-x-0" - ) - ), - composeEvents(onClick)( - _.sample(toggle.signal).map(a => !a) - ) --> toggle.writer - ), - name.map(n => - span( - cls := "ml-3", - idAttr := "active-only-label", - span(cls := "text-sm font-medium text-gray-900", n) - ) - ) - ) -} diff --git a/ui/components/src/ui/components/tailwind/form/Form.scala b/ui/components/src/ui/components/tailwind/form/Form.scala index 1b41dee..e232ae1 100644 --- a/ui/components/src/ui/components/tailwind/form/Form.scala +++ b/ui/components/src/ui/components/tailwind/form/Form.scala @@ -9,6 +9,9 @@ val Section = FormSection val Row = FormRow + @deprecated( + "use specific form instance's form method (see form.LabelsOnLeft)" + ) def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = form( cls := "space-y-8 divide-y divide-gray-200", diff --git a/ui/components/src/ui/components/tailwind/form/FormBody.scala b/ui/components/src/ui/components/tailwind/form/FormBody.scala index 89abc9c..f6afcd0 100644 --- a/ui/components/src/ui/components/tailwind/form/FormBody.scala +++ b/ui/components/src/ui/components/tailwind/form/FormBody.scala @@ -3,6 +3,7 @@ import com.raquo.laminar.api.L.{*, given} object FormBody: + @deprecated("use specific form's 'body' method (see LabelsOnLeft)") def apply(sections: HtmlElement*): HtmlElement = div( cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", diff --git a/ui/components/src/ui/components/tailwind/form/FormCodec.scala b/ui/components/src/ui/components/tailwind/form/FormCodec.scala index 5c78b24..7b43f8d 100644 --- a/ui/components/src/ui/components/tailwind/form/FormCodec.scala +++ b/ui/components/src/ui/components/tailwind/form/FormCodec.scala @@ -10,26 +10,36 @@ import java.time.LocalDate import scala.util.Try -trait FormCodec[V]: - def toForm(v: V): String - def toValue(r: String): Validated[V] +trait FormCodec[V, A]: + def toForm(v: V): A + def toValue(r: A): Validated[V] object FormCodec: - given FormCodec[PlainMultiLine] with + given FormCodec[PlainMultiLine, String] with override def toForm(v: PlainMultiLine): String = v.toString override def toValue(r: String): Validated[PlainMultiLine] = PlainMultiLine(r).mapError(e => InvalidValue(e)) - given plainMultiLineCodec: FormCodec[Option[PlainMultiLine]] with + given plainMultiLineCodec: FormCodec[Option[PlainMultiLine], String] with override def toForm(v: Option[PlainMultiLine]): String = v match case Some(t) => t.toString case _ => "" override def toValue(r: String): Validated[Option[PlainMultiLine]] = PlainMultiLine.opt(r).mapError(e => InvalidValue(e)) - given optionLocalDateCodec: FormCodec[Option[LocalDate]] with + given optionLocalDateCodec: FormCodec[Option[LocalDate], String] with val df = DateTimeFormatter.ofPattern("yyyy-MM-dd") override def toForm(v: Option[LocalDate]): String = v.map(df.format(_)).getOrElse("") override def toValue(r: String): Validated[Option[LocalDate]] = Validation.succeed(Try(LocalDate.parse(r, df)).toOption) + + given optionBooleanCodec: FormCodec[Option[Boolean], Boolean] with + override def toForm(v: Option[Boolean]): Boolean = v.getOrElse(false) + override def toValue(r: Boolean): Validated[Option[Boolean]] = + Validation.succeed(Some(r)) + +trait LowPriorityFormCodecs: + given identityCodec[A]: FormCodec[A, A] with + override def toForm(v: A): A = v + override def toValue(r: A): Validated[A] = Validation.succeed(r) diff --git a/ui/components/src/ui/components/tailwind/form/FormFields.scala b/ui/components/src/ui/components/tailwind/form/FormFields.scala index a2e4cc6..322e0d8 100644 --- a/ui/components/src/ui/components/tailwind/form/FormFields.scala +++ b/ui/components/src/ui/components/tailwind/form/FormFields.scala @@ -5,6 +5,7 @@ import org.scalajs.dom object FormFields: + @deprecated("use LabelsOnLeft.fields") def apply( mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* ): HtmlElement = diff --git a/ui/components/src/ui/components/tailwind/form/FormHeader.scala b/ui/components/src/ui/components/tailwind/form/FormHeader.scala index 4083841..3680628 100644 --- a/ui/components/src/ui/components/tailwind/form/FormHeader.scala +++ b/ui/components/src/ui/components/tailwind/form/FormHeader.scala @@ -4,6 +4,7 @@ object FormHeader: case class ViewModel(header: String, description: String) + @deprecated("use LabelsOnLeft.header") def apply(m: ViewModel): HtmlElement = div( h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), diff --git a/ui/components/src/ui/components/tailwind/form/FormInput.scala b/ui/components/src/ui/components/tailwind/form/FormInput.scala index 6422870..73ed43b 100644 --- a/ui/components/src/ui/components/tailwind/form/FormInput.scala +++ b/ui/components/src/ui/components/tailwind/form/FormInput.scala @@ -5,6 +5,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate +import works.iterative.ui.components.tailwind.ComponentContext trait FormInput[V]: def render( @@ -12,9 +13,15 @@ updates: Observer[Validated[V]] ): HtmlElement + def validate(v: V => Validated[V]): FormInput[V] = + (property: Property[V], updates: Observer[Validated[V]]) => + this.render(property, updates.contramap(_.flatMap(v))) + object FormInput: given plainMultiLineInput: FormInput[PlainMultiLine] = TextArea() given optionPlainMultiLineInput: FormInput[Option[PlainMultiLine]] = TextArea() given optionLocalDateInput: FormInput[Option[LocalDate]] = Inputs.OptionDateInput() + given optionBooleanInput(using ComponentContext): FormInput[Option[Boolean]] = + Switch() diff --git a/ui/components/src/ui/components/tailwind/form/FormSection.scala b/ui/components/src/ui/components/tailwind/form/FormSection.scala index 50811ee..d41ec7e 100644 --- a/ui/components/src/ui/components/tailwind/form/FormSection.scala +++ b/ui/components/src/ui/components/tailwind/form/FormSection.scala @@ -4,6 +4,7 @@ import com.raquo.laminar.nodes.ReactiveHtmlElement object FormSection: + @deprecated("use specific form's section method (see LabelsOnLeft)") def apply( header: HtmlElement, rows: HtmlElement* diff --git a/core/src/MessageCatalogue.scala b/core/src/MessageCatalogue.scala index b5daf9f..6319344 100644 --- a/core/src/MessageCatalogue.scala +++ b/core/src/MessageCatalogue.scala @@ -3,3 +3,4 @@ trait MessageCatalogue: def apply(id: MessageId): Option[String] + def apply(msg: UserMessage): Option[String] diff --git a/core/src/UserMessage.scala b/core/src/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/src/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/ui/components/src/ui/JsonMessageCatalogue.scala b/ui/components/src/ui/JsonMessageCatalogue.scala index a502713..aea3c6f 100644 --- a/ui/components/src/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/ui/JsonMessageCatalogue.scala @@ -3,6 +3,8 @@ import core.{MessageCatalogue, MessageId} import scala.scalajs.js +import works.iterative.core.UserMessage +import java.text.MessageFormat // TODO: support hierarchical json structure trait JsonMessageCatalogue extends MessageCatalogue: @@ -10,3 +12,6 @@ override def apply(id: MessageId): Option[String] = messages.get(id.toString) + + override def apply(msg: UserMessage): Option[String] = + apply(msg.id).map(_.format(msg.args: _*)) diff --git a/ui/components/src/ui/components/tailwind/Switch.scala b/ui/components/src/ui/components/tailwind/Switch.scala deleted file mode 100644 index dd24ed0..0000000 --- a/ui/components/src/ui/components/tailwind/Switch.scala +++ /dev/null @@ -1,38 +0,0 @@ -package works.iterative.ui.components.tailwind - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.UIString - -object Switch { - def apply(toggle: Var[Boolean], name: Option[UIString] = None): HtmlElement = - div( - cls := "flex items-center", - button( - tpe := "button", - cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - cls <-- toggle.signal.map(a => - if a then "bg-indigo-600" else "bg-gray-200" - ), - role := "switch", - dataAttr("aria-checked") := "false", - dataAttr("aria-labelledby") := "active-only-label", - span( - dataAttr("aria-hidden") := "true", - cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", - cls <-- toggle.signal.map(a => - if a then "translate-x-5" else "translate-x-0" - ) - ), - composeEvents(onClick)( - _.sample(toggle.signal).map(a => !a) - ) --> toggle.writer - ), - name.map(n => - span( - cls := "ml-3", - idAttr := "active-only-label", - span(cls := "text-sm font-medium text-gray-900", n) - ) - ) - ) -} diff --git a/ui/components/src/ui/components/tailwind/form/Form.scala b/ui/components/src/ui/components/tailwind/form/Form.scala index 1b41dee..e232ae1 100644 --- a/ui/components/src/ui/components/tailwind/form/Form.scala +++ b/ui/components/src/ui/components/tailwind/form/Form.scala @@ -9,6 +9,9 @@ val Section = FormSection val Row = FormRow + @deprecated( + "use specific form instance's form method (see form.LabelsOnLeft)" + ) def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = form( cls := "space-y-8 divide-y divide-gray-200", diff --git a/ui/components/src/ui/components/tailwind/form/FormBody.scala b/ui/components/src/ui/components/tailwind/form/FormBody.scala index 89abc9c..f6afcd0 100644 --- a/ui/components/src/ui/components/tailwind/form/FormBody.scala +++ b/ui/components/src/ui/components/tailwind/form/FormBody.scala @@ -3,6 +3,7 @@ import com.raquo.laminar.api.L.{*, given} object FormBody: + @deprecated("use specific form's 'body' method (see LabelsOnLeft)") def apply(sections: HtmlElement*): HtmlElement = div( cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", diff --git a/ui/components/src/ui/components/tailwind/form/FormCodec.scala b/ui/components/src/ui/components/tailwind/form/FormCodec.scala index 5c78b24..7b43f8d 100644 --- a/ui/components/src/ui/components/tailwind/form/FormCodec.scala +++ b/ui/components/src/ui/components/tailwind/form/FormCodec.scala @@ -10,26 +10,36 @@ import java.time.LocalDate import scala.util.Try -trait FormCodec[V]: - def toForm(v: V): String - def toValue(r: String): Validated[V] +trait FormCodec[V, A]: + def toForm(v: V): A + def toValue(r: A): Validated[V] object FormCodec: - given FormCodec[PlainMultiLine] with + given FormCodec[PlainMultiLine, String] with override def toForm(v: PlainMultiLine): String = v.toString override def toValue(r: String): Validated[PlainMultiLine] = PlainMultiLine(r).mapError(e => InvalidValue(e)) - given plainMultiLineCodec: FormCodec[Option[PlainMultiLine]] with + given plainMultiLineCodec: FormCodec[Option[PlainMultiLine], String] with override def toForm(v: Option[PlainMultiLine]): String = v match case Some(t) => t.toString case _ => "" override def toValue(r: String): Validated[Option[PlainMultiLine]] = PlainMultiLine.opt(r).mapError(e => InvalidValue(e)) - given optionLocalDateCodec: FormCodec[Option[LocalDate]] with + given optionLocalDateCodec: FormCodec[Option[LocalDate], String] with val df = DateTimeFormatter.ofPattern("yyyy-MM-dd") override def toForm(v: Option[LocalDate]): String = v.map(df.format(_)).getOrElse("") override def toValue(r: String): Validated[Option[LocalDate]] = Validation.succeed(Try(LocalDate.parse(r, df)).toOption) + + given optionBooleanCodec: FormCodec[Option[Boolean], Boolean] with + override def toForm(v: Option[Boolean]): Boolean = v.getOrElse(false) + override def toValue(r: Boolean): Validated[Option[Boolean]] = + Validation.succeed(Some(r)) + +trait LowPriorityFormCodecs: + given identityCodec[A]: FormCodec[A, A] with + override def toForm(v: A): A = v + override def toValue(r: A): Validated[A] = Validation.succeed(r) diff --git a/ui/components/src/ui/components/tailwind/form/FormFields.scala b/ui/components/src/ui/components/tailwind/form/FormFields.scala index a2e4cc6..322e0d8 100644 --- a/ui/components/src/ui/components/tailwind/form/FormFields.scala +++ b/ui/components/src/ui/components/tailwind/form/FormFields.scala @@ -5,6 +5,7 @@ import org.scalajs.dom object FormFields: + @deprecated("use LabelsOnLeft.fields") def apply( mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* ): HtmlElement = diff --git a/ui/components/src/ui/components/tailwind/form/FormHeader.scala b/ui/components/src/ui/components/tailwind/form/FormHeader.scala index 4083841..3680628 100644 --- a/ui/components/src/ui/components/tailwind/form/FormHeader.scala +++ b/ui/components/src/ui/components/tailwind/form/FormHeader.scala @@ -4,6 +4,7 @@ object FormHeader: case class ViewModel(header: String, description: String) + @deprecated("use LabelsOnLeft.header") def apply(m: ViewModel): HtmlElement = div( h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), diff --git a/ui/components/src/ui/components/tailwind/form/FormInput.scala b/ui/components/src/ui/components/tailwind/form/FormInput.scala index 6422870..73ed43b 100644 --- a/ui/components/src/ui/components/tailwind/form/FormInput.scala +++ b/ui/components/src/ui/components/tailwind/form/FormInput.scala @@ -5,6 +5,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate +import works.iterative.ui.components.tailwind.ComponentContext trait FormInput[V]: def render( @@ -12,9 +13,15 @@ updates: Observer[Validated[V]] ): HtmlElement + def validate(v: V => Validated[V]): FormInput[V] = + (property: Property[V], updates: Observer[Validated[V]]) => + this.render(property, updates.contramap(_.flatMap(v))) + object FormInput: given plainMultiLineInput: FormInput[PlainMultiLine] = TextArea() given optionPlainMultiLineInput: FormInput[Option[PlainMultiLine]] = TextArea() given optionLocalDateInput: FormInput[Option[LocalDate]] = Inputs.OptionDateInput() + given optionBooleanInput(using ComponentContext): FormInput[Option[Boolean]] = + Switch() diff --git a/ui/components/src/ui/components/tailwind/form/FormSection.scala b/ui/components/src/ui/components/tailwind/form/FormSection.scala index 50811ee..d41ec7e 100644 --- a/ui/components/src/ui/components/tailwind/form/FormSection.scala +++ b/ui/components/src/ui/components/tailwind/form/FormSection.scala @@ -4,6 +4,7 @@ import com.raquo.laminar.nodes.ReactiveHtmlElement object FormSection: + @deprecated("use specific form's section method (see LabelsOnLeft)") def apply( header: HtmlElement, rows: HtmlElement* diff --git a/ui/components/src/ui/components/tailwind/form/Inputs.scala b/ui/components/src/ui/components/tailwind/form/Inputs.scala index 89bbb8d..e1016e2 100644 --- a/ui/components/src/ui/components/tailwind/form/Inputs.scala +++ b/ui/components/src/ui/components/tailwind/form/Inputs.scala @@ -13,7 +13,7 @@ updates: Observer[Validated[V]], inputType: String, mods: Option[Modifier[Input]] = None - )(using codec: FormCodec[V]): Input = + )(using codec: FormCodec[V, String]): Input = input( idAttr := prop.id, name := prop.name, @@ -23,7 +23,7 @@ onInput.mapToValue.setAsValue.map(v => codec.toValue(v)) --> updates ) - class PlainInput[V](using FormCodec[V]) extends FormInput[V]: + class PlainInput[V](using FormCodec[V, String]) extends FormInput[V]: override def render( prop: Property[V], updates: Observer[Validated[V]] diff --git a/core/src/MessageCatalogue.scala b/core/src/MessageCatalogue.scala index b5daf9f..6319344 100644 --- a/core/src/MessageCatalogue.scala +++ b/core/src/MessageCatalogue.scala @@ -3,3 +3,4 @@ trait MessageCatalogue: def apply(id: MessageId): Option[String] + def apply(msg: UserMessage): Option[String] diff --git a/core/src/UserMessage.scala b/core/src/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/src/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/ui/components/src/ui/JsonMessageCatalogue.scala b/ui/components/src/ui/JsonMessageCatalogue.scala index a502713..aea3c6f 100644 --- a/ui/components/src/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/ui/JsonMessageCatalogue.scala @@ -3,6 +3,8 @@ import core.{MessageCatalogue, MessageId} import scala.scalajs.js +import works.iterative.core.UserMessage +import java.text.MessageFormat // TODO: support hierarchical json structure trait JsonMessageCatalogue extends MessageCatalogue: @@ -10,3 +12,6 @@ override def apply(id: MessageId): Option[String] = messages.get(id.toString) + + override def apply(msg: UserMessage): Option[String] = + apply(msg.id).map(_.format(msg.args: _*)) diff --git a/ui/components/src/ui/components/tailwind/Switch.scala b/ui/components/src/ui/components/tailwind/Switch.scala deleted file mode 100644 index dd24ed0..0000000 --- a/ui/components/src/ui/components/tailwind/Switch.scala +++ /dev/null @@ -1,38 +0,0 @@ -package works.iterative.ui.components.tailwind - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.UIString - -object Switch { - def apply(toggle: Var[Boolean], name: Option[UIString] = None): HtmlElement = - div( - cls := "flex items-center", - button( - tpe := "button", - cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - cls <-- toggle.signal.map(a => - if a then "bg-indigo-600" else "bg-gray-200" - ), - role := "switch", - dataAttr("aria-checked") := "false", - dataAttr("aria-labelledby") := "active-only-label", - span( - dataAttr("aria-hidden") := "true", - cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", - cls <-- toggle.signal.map(a => - if a then "translate-x-5" else "translate-x-0" - ) - ), - composeEvents(onClick)( - _.sample(toggle.signal).map(a => !a) - ) --> toggle.writer - ), - name.map(n => - span( - cls := "ml-3", - idAttr := "active-only-label", - span(cls := "text-sm font-medium text-gray-900", n) - ) - ) - ) -} diff --git a/ui/components/src/ui/components/tailwind/form/Form.scala b/ui/components/src/ui/components/tailwind/form/Form.scala index 1b41dee..e232ae1 100644 --- a/ui/components/src/ui/components/tailwind/form/Form.scala +++ b/ui/components/src/ui/components/tailwind/form/Form.scala @@ -9,6 +9,9 @@ val Section = FormSection val Row = FormRow + @deprecated( + "use specific form instance's form method (see form.LabelsOnLeft)" + ) def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = form( cls := "space-y-8 divide-y divide-gray-200", diff --git a/ui/components/src/ui/components/tailwind/form/FormBody.scala b/ui/components/src/ui/components/tailwind/form/FormBody.scala index 89abc9c..f6afcd0 100644 --- a/ui/components/src/ui/components/tailwind/form/FormBody.scala +++ b/ui/components/src/ui/components/tailwind/form/FormBody.scala @@ -3,6 +3,7 @@ import com.raquo.laminar.api.L.{*, given} object FormBody: + @deprecated("use specific form's 'body' method (see LabelsOnLeft)") def apply(sections: HtmlElement*): HtmlElement = div( cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", diff --git a/ui/components/src/ui/components/tailwind/form/FormCodec.scala b/ui/components/src/ui/components/tailwind/form/FormCodec.scala index 5c78b24..7b43f8d 100644 --- a/ui/components/src/ui/components/tailwind/form/FormCodec.scala +++ b/ui/components/src/ui/components/tailwind/form/FormCodec.scala @@ -10,26 +10,36 @@ import java.time.LocalDate import scala.util.Try -trait FormCodec[V]: - def toForm(v: V): String - def toValue(r: String): Validated[V] +trait FormCodec[V, A]: + def toForm(v: V): A + def toValue(r: A): Validated[V] object FormCodec: - given FormCodec[PlainMultiLine] with + given FormCodec[PlainMultiLine, String] with override def toForm(v: PlainMultiLine): String = v.toString override def toValue(r: String): Validated[PlainMultiLine] = PlainMultiLine(r).mapError(e => InvalidValue(e)) - given plainMultiLineCodec: FormCodec[Option[PlainMultiLine]] with + given plainMultiLineCodec: FormCodec[Option[PlainMultiLine], String] with override def toForm(v: Option[PlainMultiLine]): String = v match case Some(t) => t.toString case _ => "" override def toValue(r: String): Validated[Option[PlainMultiLine]] = PlainMultiLine.opt(r).mapError(e => InvalidValue(e)) - given optionLocalDateCodec: FormCodec[Option[LocalDate]] with + given optionLocalDateCodec: FormCodec[Option[LocalDate], String] with val df = DateTimeFormatter.ofPattern("yyyy-MM-dd") override def toForm(v: Option[LocalDate]): String = v.map(df.format(_)).getOrElse("") override def toValue(r: String): Validated[Option[LocalDate]] = Validation.succeed(Try(LocalDate.parse(r, df)).toOption) + + given optionBooleanCodec: FormCodec[Option[Boolean], Boolean] with + override def toForm(v: Option[Boolean]): Boolean = v.getOrElse(false) + override def toValue(r: Boolean): Validated[Option[Boolean]] = + Validation.succeed(Some(r)) + +trait LowPriorityFormCodecs: + given identityCodec[A]: FormCodec[A, A] with + override def toForm(v: A): A = v + override def toValue(r: A): Validated[A] = Validation.succeed(r) diff --git a/ui/components/src/ui/components/tailwind/form/FormFields.scala b/ui/components/src/ui/components/tailwind/form/FormFields.scala index a2e4cc6..322e0d8 100644 --- a/ui/components/src/ui/components/tailwind/form/FormFields.scala +++ b/ui/components/src/ui/components/tailwind/form/FormFields.scala @@ -5,6 +5,7 @@ import org.scalajs.dom object FormFields: + @deprecated("use LabelsOnLeft.fields") def apply( mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* ): HtmlElement = diff --git a/ui/components/src/ui/components/tailwind/form/FormHeader.scala b/ui/components/src/ui/components/tailwind/form/FormHeader.scala index 4083841..3680628 100644 --- a/ui/components/src/ui/components/tailwind/form/FormHeader.scala +++ b/ui/components/src/ui/components/tailwind/form/FormHeader.scala @@ -4,6 +4,7 @@ object FormHeader: case class ViewModel(header: String, description: String) + @deprecated("use LabelsOnLeft.header") def apply(m: ViewModel): HtmlElement = div( h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), diff --git a/ui/components/src/ui/components/tailwind/form/FormInput.scala b/ui/components/src/ui/components/tailwind/form/FormInput.scala index 6422870..73ed43b 100644 --- a/ui/components/src/ui/components/tailwind/form/FormInput.scala +++ b/ui/components/src/ui/components/tailwind/form/FormInput.scala @@ -5,6 +5,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate +import works.iterative.ui.components.tailwind.ComponentContext trait FormInput[V]: def render( @@ -12,9 +13,15 @@ updates: Observer[Validated[V]] ): HtmlElement + def validate(v: V => Validated[V]): FormInput[V] = + (property: Property[V], updates: Observer[Validated[V]]) => + this.render(property, updates.contramap(_.flatMap(v))) + object FormInput: given plainMultiLineInput: FormInput[PlainMultiLine] = TextArea() given optionPlainMultiLineInput: FormInput[Option[PlainMultiLine]] = TextArea() given optionLocalDateInput: FormInput[Option[LocalDate]] = Inputs.OptionDateInput() + given optionBooleanInput(using ComponentContext): FormInput[Option[Boolean]] = + Switch() diff --git a/ui/components/src/ui/components/tailwind/form/FormSection.scala b/ui/components/src/ui/components/tailwind/form/FormSection.scala index 50811ee..d41ec7e 100644 --- a/ui/components/src/ui/components/tailwind/form/FormSection.scala +++ b/ui/components/src/ui/components/tailwind/form/FormSection.scala @@ -4,6 +4,7 @@ import com.raquo.laminar.nodes.ReactiveHtmlElement object FormSection: + @deprecated("use specific form's section method (see LabelsOnLeft)") def apply( header: HtmlElement, rows: HtmlElement* diff --git a/ui/components/src/ui/components/tailwind/form/Inputs.scala b/ui/components/src/ui/components/tailwind/form/Inputs.scala index 89bbb8d..e1016e2 100644 --- a/ui/components/src/ui/components/tailwind/form/Inputs.scala +++ b/ui/components/src/ui/components/tailwind/form/Inputs.scala @@ -13,7 +13,7 @@ updates: Observer[Validated[V]], inputType: String, mods: Option[Modifier[Input]] = None - )(using codec: FormCodec[V]): Input = + )(using codec: FormCodec[V, String]): Input = input( idAttr := prop.id, name := prop.name, @@ -23,7 +23,7 @@ onInput.mapToValue.setAsValue.map(v => codec.toValue(v)) --> updates ) - class PlainInput[V](using FormCodec[V]) extends FormInput[V]: + class PlainInput[V](using FormCodec[V, String]) extends FormInput[V]: override def render( prop: Property[V], updates: Observer[Validated[V]] diff --git a/ui/components/src/ui/components/tailwind/form/LabelsOnLeft.scala b/ui/components/src/ui/components/tailwind/form/LabelsOnLeft.scala new file mode 100644 index 0000000..2e87c32 --- /dev/null +++ b/ui/components/src/ui/components/tailwind/form/LabelsOnLeft.scala @@ -0,0 +1,41 @@ +package works.iterative +package ui.components.tailwind.form + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} + +object LabelsOnLeft: + + def fields( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", + mods + ) + + def header(header: String, description: String): HtmlElement = + div( + h3(cls := "text-lg leading-6 font-medium text-gray-900", header), + p(cls := "mt-1 max-w-2xl text-sm text-gray-500", description) + ) + + def section(header: HtmlElement, rows: HtmlElement*): HtmlElement = + div( + cls := "space-y-6 sm:space-y-5", + header, + rows + ) + + def body(sections: HtmlElement*): HtmlElement = + div( + cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", + sections + ) + + def form(body: HtmlElement, buttons: HtmlElement): HtmlElement = + L.form( + cls := "space-y-8 divide-y divide-gray-200", + body, + buttons + ) diff --git a/core/src/MessageCatalogue.scala b/core/src/MessageCatalogue.scala index b5daf9f..6319344 100644 --- a/core/src/MessageCatalogue.scala +++ b/core/src/MessageCatalogue.scala @@ -3,3 +3,4 @@ trait MessageCatalogue: def apply(id: MessageId): Option[String] + def apply(msg: UserMessage): Option[String] diff --git a/core/src/UserMessage.scala b/core/src/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/src/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/ui/components/src/ui/JsonMessageCatalogue.scala b/ui/components/src/ui/JsonMessageCatalogue.scala index a502713..aea3c6f 100644 --- a/ui/components/src/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/ui/JsonMessageCatalogue.scala @@ -3,6 +3,8 @@ import core.{MessageCatalogue, MessageId} import scala.scalajs.js +import works.iterative.core.UserMessage +import java.text.MessageFormat // TODO: support hierarchical json structure trait JsonMessageCatalogue extends MessageCatalogue: @@ -10,3 +12,6 @@ override def apply(id: MessageId): Option[String] = messages.get(id.toString) + + override def apply(msg: UserMessage): Option[String] = + apply(msg.id).map(_.format(msg.args: _*)) diff --git a/ui/components/src/ui/components/tailwind/Switch.scala b/ui/components/src/ui/components/tailwind/Switch.scala deleted file mode 100644 index dd24ed0..0000000 --- a/ui/components/src/ui/components/tailwind/Switch.scala +++ /dev/null @@ -1,38 +0,0 @@ -package works.iterative.ui.components.tailwind - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.UIString - -object Switch { - def apply(toggle: Var[Boolean], name: Option[UIString] = None): HtmlElement = - div( - cls := "flex items-center", - button( - tpe := "button", - cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - cls <-- toggle.signal.map(a => - if a then "bg-indigo-600" else "bg-gray-200" - ), - role := "switch", - dataAttr("aria-checked") := "false", - dataAttr("aria-labelledby") := "active-only-label", - span( - dataAttr("aria-hidden") := "true", - cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", - cls <-- toggle.signal.map(a => - if a then "translate-x-5" else "translate-x-0" - ) - ), - composeEvents(onClick)( - _.sample(toggle.signal).map(a => !a) - ) --> toggle.writer - ), - name.map(n => - span( - cls := "ml-3", - idAttr := "active-only-label", - span(cls := "text-sm font-medium text-gray-900", n) - ) - ) - ) -} diff --git a/ui/components/src/ui/components/tailwind/form/Form.scala b/ui/components/src/ui/components/tailwind/form/Form.scala index 1b41dee..e232ae1 100644 --- a/ui/components/src/ui/components/tailwind/form/Form.scala +++ b/ui/components/src/ui/components/tailwind/form/Form.scala @@ -9,6 +9,9 @@ val Section = FormSection val Row = FormRow + @deprecated( + "use specific form instance's form method (see form.LabelsOnLeft)" + ) def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = form( cls := "space-y-8 divide-y divide-gray-200", diff --git a/ui/components/src/ui/components/tailwind/form/FormBody.scala b/ui/components/src/ui/components/tailwind/form/FormBody.scala index 89abc9c..f6afcd0 100644 --- a/ui/components/src/ui/components/tailwind/form/FormBody.scala +++ b/ui/components/src/ui/components/tailwind/form/FormBody.scala @@ -3,6 +3,7 @@ import com.raquo.laminar.api.L.{*, given} object FormBody: + @deprecated("use specific form's 'body' method (see LabelsOnLeft)") def apply(sections: HtmlElement*): HtmlElement = div( cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", diff --git a/ui/components/src/ui/components/tailwind/form/FormCodec.scala b/ui/components/src/ui/components/tailwind/form/FormCodec.scala index 5c78b24..7b43f8d 100644 --- a/ui/components/src/ui/components/tailwind/form/FormCodec.scala +++ b/ui/components/src/ui/components/tailwind/form/FormCodec.scala @@ -10,26 +10,36 @@ import java.time.LocalDate import scala.util.Try -trait FormCodec[V]: - def toForm(v: V): String - def toValue(r: String): Validated[V] +trait FormCodec[V, A]: + def toForm(v: V): A + def toValue(r: A): Validated[V] object FormCodec: - given FormCodec[PlainMultiLine] with + given FormCodec[PlainMultiLine, String] with override def toForm(v: PlainMultiLine): String = v.toString override def toValue(r: String): Validated[PlainMultiLine] = PlainMultiLine(r).mapError(e => InvalidValue(e)) - given plainMultiLineCodec: FormCodec[Option[PlainMultiLine]] with + given plainMultiLineCodec: FormCodec[Option[PlainMultiLine], String] with override def toForm(v: Option[PlainMultiLine]): String = v match case Some(t) => t.toString case _ => "" override def toValue(r: String): Validated[Option[PlainMultiLine]] = PlainMultiLine.opt(r).mapError(e => InvalidValue(e)) - given optionLocalDateCodec: FormCodec[Option[LocalDate]] with + given optionLocalDateCodec: FormCodec[Option[LocalDate], String] with val df = DateTimeFormatter.ofPattern("yyyy-MM-dd") override def toForm(v: Option[LocalDate]): String = v.map(df.format(_)).getOrElse("") override def toValue(r: String): Validated[Option[LocalDate]] = Validation.succeed(Try(LocalDate.parse(r, df)).toOption) + + given optionBooleanCodec: FormCodec[Option[Boolean], Boolean] with + override def toForm(v: Option[Boolean]): Boolean = v.getOrElse(false) + override def toValue(r: Boolean): Validated[Option[Boolean]] = + Validation.succeed(Some(r)) + +trait LowPriorityFormCodecs: + given identityCodec[A]: FormCodec[A, A] with + override def toForm(v: A): A = v + override def toValue(r: A): Validated[A] = Validation.succeed(r) diff --git a/ui/components/src/ui/components/tailwind/form/FormFields.scala b/ui/components/src/ui/components/tailwind/form/FormFields.scala index a2e4cc6..322e0d8 100644 --- a/ui/components/src/ui/components/tailwind/form/FormFields.scala +++ b/ui/components/src/ui/components/tailwind/form/FormFields.scala @@ -5,6 +5,7 @@ import org.scalajs.dom object FormFields: + @deprecated("use LabelsOnLeft.fields") def apply( mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* ): HtmlElement = diff --git a/ui/components/src/ui/components/tailwind/form/FormHeader.scala b/ui/components/src/ui/components/tailwind/form/FormHeader.scala index 4083841..3680628 100644 --- a/ui/components/src/ui/components/tailwind/form/FormHeader.scala +++ b/ui/components/src/ui/components/tailwind/form/FormHeader.scala @@ -4,6 +4,7 @@ object FormHeader: case class ViewModel(header: String, description: String) + @deprecated("use LabelsOnLeft.header") def apply(m: ViewModel): HtmlElement = div( h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), diff --git a/ui/components/src/ui/components/tailwind/form/FormInput.scala b/ui/components/src/ui/components/tailwind/form/FormInput.scala index 6422870..73ed43b 100644 --- a/ui/components/src/ui/components/tailwind/form/FormInput.scala +++ b/ui/components/src/ui/components/tailwind/form/FormInput.scala @@ -5,6 +5,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate +import works.iterative.ui.components.tailwind.ComponentContext trait FormInput[V]: def render( @@ -12,9 +13,15 @@ updates: Observer[Validated[V]] ): HtmlElement + def validate(v: V => Validated[V]): FormInput[V] = + (property: Property[V], updates: Observer[Validated[V]]) => + this.render(property, updates.contramap(_.flatMap(v))) + object FormInput: given plainMultiLineInput: FormInput[PlainMultiLine] = TextArea() given optionPlainMultiLineInput: FormInput[Option[PlainMultiLine]] = TextArea() given optionLocalDateInput: FormInput[Option[LocalDate]] = Inputs.OptionDateInput() + given optionBooleanInput(using ComponentContext): FormInput[Option[Boolean]] = + Switch() diff --git a/ui/components/src/ui/components/tailwind/form/FormSection.scala b/ui/components/src/ui/components/tailwind/form/FormSection.scala index 50811ee..d41ec7e 100644 --- a/ui/components/src/ui/components/tailwind/form/FormSection.scala +++ b/ui/components/src/ui/components/tailwind/form/FormSection.scala @@ -4,6 +4,7 @@ import com.raquo.laminar.nodes.ReactiveHtmlElement object FormSection: + @deprecated("use specific form's section method (see LabelsOnLeft)") def apply( header: HtmlElement, rows: HtmlElement* diff --git a/ui/components/src/ui/components/tailwind/form/Inputs.scala b/ui/components/src/ui/components/tailwind/form/Inputs.scala index 89bbb8d..e1016e2 100644 --- a/ui/components/src/ui/components/tailwind/form/Inputs.scala +++ b/ui/components/src/ui/components/tailwind/form/Inputs.scala @@ -13,7 +13,7 @@ updates: Observer[Validated[V]], inputType: String, mods: Option[Modifier[Input]] = None - )(using codec: FormCodec[V]): Input = + )(using codec: FormCodec[V, String]): Input = input( idAttr := prop.id, name := prop.name, @@ -23,7 +23,7 @@ onInput.mapToValue.setAsValue.map(v => codec.toValue(v)) --> updates ) - class PlainInput[V](using FormCodec[V]) extends FormInput[V]: + class PlainInput[V](using FormCodec[V, String]) extends FormInput[V]: override def render( prop: Property[V], updates: Observer[Validated[V]] diff --git a/ui/components/src/ui/components/tailwind/form/LabelsOnLeft.scala b/ui/components/src/ui/components/tailwind/form/LabelsOnLeft.scala new file mode 100644 index 0000000..2e87c32 --- /dev/null +++ b/ui/components/src/ui/components/tailwind/form/LabelsOnLeft.scala @@ -0,0 +1,41 @@ +package works.iterative +package ui.components.tailwind.form + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} + +object LabelsOnLeft: + + def fields( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", + mods + ) + + def header(header: String, description: String): HtmlElement = + div( + h3(cls := "text-lg leading-6 font-medium text-gray-900", header), + p(cls := "mt-1 max-w-2xl text-sm text-gray-500", description) + ) + + def section(header: HtmlElement, rows: HtmlElement*): HtmlElement = + div( + cls := "space-y-6 sm:space-y-5", + header, + rows + ) + + def body(sections: HtmlElement*): HtmlElement = + div( + cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", + sections + ) + + def form(body: HtmlElement, buttons: HtmlElement): HtmlElement = + L.form( + cls := "space-y-8 divide-y divide-gray-200", + body, + buttons + ) diff --git a/ui/components/src/ui/components/tailwind/form/Switch.scala b/ui/components/src/ui/components/tailwind/form/Switch.scala new file mode 100644 index 0000000..f742d3a --- /dev/null +++ b/ui/components/src/ui/components/tailwind/form/Switch.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.tailwind.form + +import com.raquo.laminar.api.L.{*, given} + +import zio.prelude.Validation +import works.iterative.ui.components.tailwind.ComponentContext + +class Switch[V](using codec: FormCodec[V, Boolean], ctx: ComponentContext) + extends FormInput[V]: + def render( + property: Property[V], + updates: Observer[Validated[V]] + ): HtmlElement = + val initialValue = property.value.map(codec.toForm).getOrElse(false) + val currentValue = Var(initialValue) + div( + currentValue.signal.map(codec.toValue) --> updates, + cls := "flex items-center", + button( + tpe := "button", + cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + cls <-- currentValue.signal.map(v => + if v then "bg-indigo-600" else "bg-gray-200" + ), + role := "switch", + dataAttr("aria-checked") := "false", + dataAttr("aria-labelledby") := "active-only-label", + span( + dataAttr("aria-hidden") := "true", + cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", + cls <-- currentValue.signal.map(v => + if v then "translate-x-5" else "translate-x-0" + ) + ), + composeEvents(onClick)( + _.sample(currentValue.signal).map(v => !v) + ) --> currentValue + ), + span( + cls := "ml-3", + idAttr := "active-only-label", + span( + cls := "text-sm font-medium text-gray-900", + ctx.messages(property.name) + ) + ) + ) diff --git a/core/src/MessageCatalogue.scala b/core/src/MessageCatalogue.scala index b5daf9f..6319344 100644 --- a/core/src/MessageCatalogue.scala +++ b/core/src/MessageCatalogue.scala @@ -3,3 +3,4 @@ trait MessageCatalogue: def apply(id: MessageId): Option[String] + def apply(msg: UserMessage): Option[String] diff --git a/core/src/UserMessage.scala b/core/src/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/src/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/ui/components/src/ui/JsonMessageCatalogue.scala b/ui/components/src/ui/JsonMessageCatalogue.scala index a502713..aea3c6f 100644 --- a/ui/components/src/ui/JsonMessageCatalogue.scala +++ b/ui/components/src/ui/JsonMessageCatalogue.scala @@ -3,6 +3,8 @@ import core.{MessageCatalogue, MessageId} import scala.scalajs.js +import works.iterative.core.UserMessage +import java.text.MessageFormat // TODO: support hierarchical json structure trait JsonMessageCatalogue extends MessageCatalogue: @@ -10,3 +12,6 @@ override def apply(id: MessageId): Option[String] = messages.get(id.toString) + + override def apply(msg: UserMessage): Option[String] = + apply(msg.id).map(_.format(msg.args: _*)) diff --git a/ui/components/src/ui/components/tailwind/Switch.scala b/ui/components/src/ui/components/tailwind/Switch.scala deleted file mode 100644 index dd24ed0..0000000 --- a/ui/components/src/ui/components/tailwind/Switch.scala +++ /dev/null @@ -1,38 +0,0 @@ -package works.iterative.ui.components.tailwind - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.UIString - -object Switch { - def apply(toggle: Var[Boolean], name: Option[UIString] = None): HtmlElement = - div( - cls := "flex items-center", - button( - tpe := "button", - cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - cls <-- toggle.signal.map(a => - if a then "bg-indigo-600" else "bg-gray-200" - ), - role := "switch", - dataAttr("aria-checked") := "false", - dataAttr("aria-labelledby") := "active-only-label", - span( - dataAttr("aria-hidden") := "true", - cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", - cls <-- toggle.signal.map(a => - if a then "translate-x-5" else "translate-x-0" - ) - ), - composeEvents(onClick)( - _.sample(toggle.signal).map(a => !a) - ) --> toggle.writer - ), - name.map(n => - span( - cls := "ml-3", - idAttr := "active-only-label", - span(cls := "text-sm font-medium text-gray-900", n) - ) - ) - ) -} diff --git a/ui/components/src/ui/components/tailwind/form/Form.scala b/ui/components/src/ui/components/tailwind/form/Form.scala index 1b41dee..e232ae1 100644 --- a/ui/components/src/ui/components/tailwind/form/Form.scala +++ b/ui/components/src/ui/components/tailwind/form/Form.scala @@ -9,6 +9,9 @@ val Section = FormSection val Row = FormRow + @deprecated( + "use specific form instance's form method (see form.LabelsOnLeft)" + ) def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = form( cls := "space-y-8 divide-y divide-gray-200", diff --git a/ui/components/src/ui/components/tailwind/form/FormBody.scala b/ui/components/src/ui/components/tailwind/form/FormBody.scala index 89abc9c..f6afcd0 100644 --- a/ui/components/src/ui/components/tailwind/form/FormBody.scala +++ b/ui/components/src/ui/components/tailwind/form/FormBody.scala @@ -3,6 +3,7 @@ import com.raquo.laminar.api.L.{*, given} object FormBody: + @deprecated("use specific form's 'body' method (see LabelsOnLeft)") def apply(sections: HtmlElement*): HtmlElement = div( cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", diff --git a/ui/components/src/ui/components/tailwind/form/FormCodec.scala b/ui/components/src/ui/components/tailwind/form/FormCodec.scala index 5c78b24..7b43f8d 100644 --- a/ui/components/src/ui/components/tailwind/form/FormCodec.scala +++ b/ui/components/src/ui/components/tailwind/form/FormCodec.scala @@ -10,26 +10,36 @@ import java.time.LocalDate import scala.util.Try -trait FormCodec[V]: - def toForm(v: V): String - def toValue(r: String): Validated[V] +trait FormCodec[V, A]: + def toForm(v: V): A + def toValue(r: A): Validated[V] object FormCodec: - given FormCodec[PlainMultiLine] with + given FormCodec[PlainMultiLine, String] with override def toForm(v: PlainMultiLine): String = v.toString override def toValue(r: String): Validated[PlainMultiLine] = PlainMultiLine(r).mapError(e => InvalidValue(e)) - given plainMultiLineCodec: FormCodec[Option[PlainMultiLine]] with + given plainMultiLineCodec: FormCodec[Option[PlainMultiLine], String] with override def toForm(v: Option[PlainMultiLine]): String = v match case Some(t) => t.toString case _ => "" override def toValue(r: String): Validated[Option[PlainMultiLine]] = PlainMultiLine.opt(r).mapError(e => InvalidValue(e)) - given optionLocalDateCodec: FormCodec[Option[LocalDate]] with + given optionLocalDateCodec: FormCodec[Option[LocalDate], String] with val df = DateTimeFormatter.ofPattern("yyyy-MM-dd") override def toForm(v: Option[LocalDate]): String = v.map(df.format(_)).getOrElse("") override def toValue(r: String): Validated[Option[LocalDate]] = Validation.succeed(Try(LocalDate.parse(r, df)).toOption) + + given optionBooleanCodec: FormCodec[Option[Boolean], Boolean] with + override def toForm(v: Option[Boolean]): Boolean = v.getOrElse(false) + override def toValue(r: Boolean): Validated[Option[Boolean]] = + Validation.succeed(Some(r)) + +trait LowPriorityFormCodecs: + given identityCodec[A]: FormCodec[A, A] with + override def toForm(v: A): A = v + override def toValue(r: A): Validated[A] = Validation.succeed(r) diff --git a/ui/components/src/ui/components/tailwind/form/FormFields.scala b/ui/components/src/ui/components/tailwind/form/FormFields.scala index a2e4cc6..322e0d8 100644 --- a/ui/components/src/ui/components/tailwind/form/FormFields.scala +++ b/ui/components/src/ui/components/tailwind/form/FormFields.scala @@ -5,6 +5,7 @@ import org.scalajs.dom object FormFields: + @deprecated("use LabelsOnLeft.fields") def apply( mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* ): HtmlElement = diff --git a/ui/components/src/ui/components/tailwind/form/FormHeader.scala b/ui/components/src/ui/components/tailwind/form/FormHeader.scala index 4083841..3680628 100644 --- a/ui/components/src/ui/components/tailwind/form/FormHeader.scala +++ b/ui/components/src/ui/components/tailwind/form/FormHeader.scala @@ -4,6 +4,7 @@ object FormHeader: case class ViewModel(header: String, description: String) + @deprecated("use LabelsOnLeft.header") def apply(m: ViewModel): HtmlElement = div( h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), diff --git a/ui/components/src/ui/components/tailwind/form/FormInput.scala b/ui/components/src/ui/components/tailwind/form/FormInput.scala index 6422870..73ed43b 100644 --- a/ui/components/src/ui/components/tailwind/form/FormInput.scala +++ b/ui/components/src/ui/components/tailwind/form/FormInput.scala @@ -5,6 +5,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate +import works.iterative.ui.components.tailwind.ComponentContext trait FormInput[V]: def render( @@ -12,9 +13,15 @@ updates: Observer[Validated[V]] ): HtmlElement + def validate(v: V => Validated[V]): FormInput[V] = + (property: Property[V], updates: Observer[Validated[V]]) => + this.render(property, updates.contramap(_.flatMap(v))) + object FormInput: given plainMultiLineInput: FormInput[PlainMultiLine] = TextArea() given optionPlainMultiLineInput: FormInput[Option[PlainMultiLine]] = TextArea() given optionLocalDateInput: FormInput[Option[LocalDate]] = Inputs.OptionDateInput() + given optionBooleanInput(using ComponentContext): FormInput[Option[Boolean]] = + Switch() diff --git a/ui/components/src/ui/components/tailwind/form/FormSection.scala b/ui/components/src/ui/components/tailwind/form/FormSection.scala index 50811ee..d41ec7e 100644 --- a/ui/components/src/ui/components/tailwind/form/FormSection.scala +++ b/ui/components/src/ui/components/tailwind/form/FormSection.scala @@ -4,6 +4,7 @@ import com.raquo.laminar.nodes.ReactiveHtmlElement object FormSection: + @deprecated("use specific form's section method (see LabelsOnLeft)") def apply( header: HtmlElement, rows: HtmlElement* diff --git a/ui/components/src/ui/components/tailwind/form/Inputs.scala b/ui/components/src/ui/components/tailwind/form/Inputs.scala index 89bbb8d..e1016e2 100644 --- a/ui/components/src/ui/components/tailwind/form/Inputs.scala +++ b/ui/components/src/ui/components/tailwind/form/Inputs.scala @@ -13,7 +13,7 @@ updates: Observer[Validated[V]], inputType: String, mods: Option[Modifier[Input]] = None - )(using codec: FormCodec[V]): Input = + )(using codec: FormCodec[V, String]): Input = input( idAttr := prop.id, name := prop.name, @@ -23,7 +23,7 @@ onInput.mapToValue.setAsValue.map(v => codec.toValue(v)) --> updates ) - class PlainInput[V](using FormCodec[V]) extends FormInput[V]: + class PlainInput[V](using FormCodec[V, String]) extends FormInput[V]: override def render( prop: Property[V], updates: Observer[Validated[V]] diff --git a/ui/components/src/ui/components/tailwind/form/LabelsOnLeft.scala b/ui/components/src/ui/components/tailwind/form/LabelsOnLeft.scala new file mode 100644 index 0000000..2e87c32 --- /dev/null +++ b/ui/components/src/ui/components/tailwind/form/LabelsOnLeft.scala @@ -0,0 +1,41 @@ +package works.iterative +package ui.components.tailwind.form + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} + +object LabelsOnLeft: + + def fields( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", + mods + ) + + def header(header: String, description: String): HtmlElement = + div( + h3(cls := "text-lg leading-6 font-medium text-gray-900", header), + p(cls := "mt-1 max-w-2xl text-sm text-gray-500", description) + ) + + def section(header: HtmlElement, rows: HtmlElement*): HtmlElement = + div( + cls := "space-y-6 sm:space-y-5", + header, + rows + ) + + def body(sections: HtmlElement*): HtmlElement = + div( + cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", + sections + ) + + def form(body: HtmlElement, buttons: HtmlElement): HtmlElement = + L.form( + cls := "space-y-8 divide-y divide-gray-200", + body, + buttons + ) diff --git a/ui/components/src/ui/components/tailwind/form/Switch.scala b/ui/components/src/ui/components/tailwind/form/Switch.scala new file mode 100644 index 0000000..f742d3a --- /dev/null +++ b/ui/components/src/ui/components/tailwind/form/Switch.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.tailwind.form + +import com.raquo.laminar.api.L.{*, given} + +import zio.prelude.Validation +import works.iterative.ui.components.tailwind.ComponentContext + +class Switch[V](using codec: FormCodec[V, Boolean], ctx: ComponentContext) + extends FormInput[V]: + def render( + property: Property[V], + updates: Observer[Validated[V]] + ): HtmlElement = + val initialValue = property.value.map(codec.toForm).getOrElse(false) + val currentValue = Var(initialValue) + div( + currentValue.signal.map(codec.toValue) --> updates, + cls := "flex items-center", + button( + tpe := "button", + cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + cls <-- currentValue.signal.map(v => + if v then "bg-indigo-600" else "bg-gray-200" + ), + role := "switch", + dataAttr("aria-checked") := "false", + dataAttr("aria-labelledby") := "active-only-label", + span( + dataAttr("aria-hidden") := "true", + cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", + cls <-- currentValue.signal.map(v => + if v then "translate-x-5" else "translate-x-0" + ) + ), + composeEvents(onClick)( + _.sample(currentValue.signal).map(v => !v) + ) --> currentValue + ), + span( + cls := "ml-3", + idAttr := "active-only-label", + span( + cls := "text-sm font-medium text-gray-900", + ctx.messages(property.name) + ) + ) + ) diff --git a/ui/components/src/ui/components/tailwind/form/TextArea.scala b/ui/components/src/ui/components/tailwind/form/TextArea.scala index 8ed7541..17f90e3 100644 --- a/ui/components/src/ui/components/tailwind/form/TextArea.scala +++ b/ui/components/src/ui/components/tailwind/form/TextArea.scala @@ -7,7 +7,7 @@ class TextArea[V]( initialRows: Int = 5 -)(using codec: FormCodec[V]) +)(using codec: FormCodec[V, String]) extends FormInput[V]: override def render( prop: Property[V],