diff --git a/ui/js/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala b/ui/js/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala index 79aef16..dc898f3 100644 --- a/ui/js/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala +++ b/ui/js/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala @@ -5,6 +5,7 @@ import scala.scalajs.js import works.iterative.core.UserMessage import java.text.MessageFormat +import scala.util.Try // TODO: support hierarchical json structure trait JsonMessageCatalogue extends MessageCatalogue: @@ -16,4 +17,9 @@ override def get(msg: UserMessage): Option[String] = assume(messages != null, "Message catalogue must not be null") - get(msg.id).map(_.format(msg.args: _*)) + get(msg.id).map(m => + Try(m.format(msg.args*)).fold( + t => s"error formatting [${msg.id.toString()}]: '$m': ${t.getMessage}", + identity + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala b/ui/js/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala index 79aef16..dc898f3 100644 --- a/ui/js/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala +++ b/ui/js/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala @@ -5,6 +5,7 @@ import scala.scalajs.js import works.iterative.core.UserMessage import java.text.MessageFormat +import scala.util.Try // TODO: support hierarchical json structure trait JsonMessageCatalogue extends MessageCatalogue: @@ -16,4 +17,9 @@ override def get(msg: UserMessage): Option[String] = assume(messages != null, "Message catalogue must not be null") - get(msg.id).map(_.format(msg.args: _*)) + get(msg.id).map(m => + Try(m.format(msg.args*)).fold( + t => s"error formatting [${msg.id.toString()}]: '$m': ${t.getMessage}", + identity + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala index a7b1166..37d80f8 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala @@ -9,7 +9,9 @@ trait InputField[A]: def render: ReactiveHtmlElement[html.Input] -type Validated[A] = Validation[UserMessage, A] +case class InvalidValue(name: String, message: String => UserMessage) + +type Validated[A] = Validation[InvalidValue, A] sealed trait Form[A]: def value: Validated[A] @@ -17,7 +19,9 @@ object Form: case class Input(name: String) extends Form[String]: override def value: Validated[String] = - Validation.fail(UserMessage("error.invalid.value")) + Validation.fail( + InvalidValue(name, UserMessage("error.value.required", _)) + ) trait FormBuilderModule: def formMessagesResolver: FormMessagesResolver @@ -27,14 +31,26 @@ case class HtmlFormBuilder[A](form: Form[A], submit: Observer[A]): def renderForm[A](form: Form[A]): HtmlElement = form match - case Form.Input(name) => + case i @ Form.Input(name) => + val userLabel = formMessagesResolver.label(name) formUIFactory.field( - formUIFactory.label(formMessagesResolver.label(name))() + formUIFactory.label(userLabel)() )( formUIFactory.input( name, placeholder = formMessagesResolver.placeholder(name) - )() + )(), + i.value.fold( + msgs => + msgs + .map(msg => + formUIFactory.validationError( + formMessagesResolver.message(msg.message(userLabel)) + ) + ) + .toList, + _ => List.empty[HtmlMod] + ) ) def build: HtmlElement = diff --git a/ui/js/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala b/ui/js/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala index 79aef16..dc898f3 100644 --- a/ui/js/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala +++ b/ui/js/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala @@ -5,6 +5,7 @@ import scala.scalajs.js import works.iterative.core.UserMessage import java.text.MessageFormat +import scala.util.Try // TODO: support hierarchical json structure trait JsonMessageCatalogue extends MessageCatalogue: @@ -16,4 +17,9 @@ override def get(msg: UserMessage): Option[String] = assume(messages != null, "Message catalogue must not be null") - get(msg.id).map(_.format(msg.args: _*)) + get(msg.id).map(m => + Try(m.format(msg.args*)).fold( + t => s"error formatting [${msg.id.toString()}]: '$m': ${t.getMessage}", + identity + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala index a7b1166..37d80f8 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala @@ -9,7 +9,9 @@ trait InputField[A]: def render: ReactiveHtmlElement[html.Input] -type Validated[A] = Validation[UserMessage, A] +case class InvalidValue(name: String, message: String => UserMessage) + +type Validated[A] = Validation[InvalidValue, A] sealed trait Form[A]: def value: Validated[A] @@ -17,7 +19,9 @@ object Form: case class Input(name: String) extends Form[String]: override def value: Validated[String] = - Validation.fail(UserMessage("error.invalid.value")) + Validation.fail( + InvalidValue(name, UserMessage("error.value.required", _)) + ) trait FormBuilderModule: def formMessagesResolver: FormMessagesResolver @@ -27,14 +31,26 @@ case class HtmlFormBuilder[A](form: Form[A], submit: Observer[A]): def renderForm[A](form: Form[A]): HtmlElement = form match - case Form.Input(name) => + case i @ Form.Input(name) => + val userLabel = formMessagesResolver.label(name) formUIFactory.field( - formUIFactory.label(formMessagesResolver.label(name))() + formUIFactory.label(userLabel)() )( formUIFactory.input( name, placeholder = formMessagesResolver.placeholder(name) - )() + )(), + i.value.fold( + msgs => + msgs + .map(msg => + formUIFactory.validationError( + formMessagesResolver.message(msg.message(userLabel)) + ) + ) + .toList, + _ => List.empty[HtmlMod] + ) ) def build: HtmlElement = diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormMessagesResolver.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormMessagesResolver.scala index 58d0068..bd1ab7b 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormMessagesResolver.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormMessagesResolver.scala @@ -2,11 +2,13 @@ import works.iterative.core.MessageCatalogue import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.core.UserMessage trait FormMessagesResolver: def label(name: String): String def help(name: String): Option[String] def placeholder(name: String): Option[String] + def message(msg: UserMessage): String object FormMessagesResolver: given (using ctx: ComponentContext[_]): FormMessagesResolver = @@ -17,6 +19,9 @@ class MessageCatalogueFormMessagesResolver(using cat: MessageCatalogue) extends FormMessagesResolver: - def label(name: String): String = cat(name) - def help(name: String): Option[String] = cat.get(s"$name.help") - def placeholder(name: String): Option[String] = cat.get(s"$name.placeholder") + override def label(name: String): String = cat(name) + override def help(name: String): Option[String] = cat.get(s"$name.help") + override def placeholder(name: String): Option[String] = + cat.get(s"$name.placeholder") + + override def message(msg: UserMessage): String = cat(msg) diff --git a/ui/js/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala b/ui/js/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala index 79aef16..dc898f3 100644 --- a/ui/js/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala +++ b/ui/js/src/main/scala/works/iterative/ui/JsonMessageCatalogue.scala @@ -5,6 +5,7 @@ import scala.scalajs.js import works.iterative.core.UserMessage import java.text.MessageFormat +import scala.util.Try // TODO: support hierarchical json structure trait JsonMessageCatalogue extends MessageCatalogue: @@ -16,4 +17,9 @@ override def get(msg: UserMessage): Option[String] = assume(messages != null, "Message catalogue must not be null") - get(msg.id).map(_.format(msg.args: _*)) + get(msg.id).map(m => + Try(m.format(msg.args*)).fold( + t => s"error formatting [${msg.id.toString()}]: '$m': ${t.getMessage}", + identity + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala index a7b1166..37d80f8 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala @@ -9,7 +9,9 @@ trait InputField[A]: def render: ReactiveHtmlElement[html.Input] -type Validated[A] = Validation[UserMessage, A] +case class InvalidValue(name: String, message: String => UserMessage) + +type Validated[A] = Validation[InvalidValue, A] sealed trait Form[A]: def value: Validated[A] @@ -17,7 +19,9 @@ object Form: case class Input(name: String) extends Form[String]: override def value: Validated[String] = - Validation.fail(UserMessage("error.invalid.value")) + Validation.fail( + InvalidValue(name, UserMessage("error.value.required", _)) + ) trait FormBuilderModule: def formMessagesResolver: FormMessagesResolver @@ -27,14 +31,26 @@ case class HtmlFormBuilder[A](form: Form[A], submit: Observer[A]): def renderForm[A](form: Form[A]): HtmlElement = form match - case Form.Input(name) => + case i @ Form.Input(name) => + val userLabel = formMessagesResolver.label(name) formUIFactory.field( - formUIFactory.label(formMessagesResolver.label(name))() + formUIFactory.label(userLabel)() )( formUIFactory.input( name, placeholder = formMessagesResolver.placeholder(name) - )() + )(), + i.value.fold( + msgs => + msgs + .map(msg => + formUIFactory.validationError( + formMessagesResolver.message(msg.message(userLabel)) + ) + ) + .toList, + _ => List.empty[HtmlMod] + ) ) def build: HtmlElement = diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormMessagesResolver.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormMessagesResolver.scala index 58d0068..bd1ab7b 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormMessagesResolver.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormMessagesResolver.scala @@ -2,11 +2,13 @@ import works.iterative.core.MessageCatalogue import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.core.UserMessage trait FormMessagesResolver: def label(name: String): String def help(name: String): Option[String] def placeholder(name: String): Option[String] + def message(msg: UserMessage): String object FormMessagesResolver: given (using ctx: ComponentContext[_]): FormMessagesResolver = @@ -17,6 +19,9 @@ class MessageCatalogueFormMessagesResolver(using cat: MessageCatalogue) extends FormMessagesResolver: - def label(name: String): String = cat(name) - def help(name: String): Option[String] = cat.get(s"$name.help") - def placeholder(name: String): Option[String] = cat.get(s"$name.placeholder") + override def label(name: String): String = cat(name) + override def help(name: String): Option[String] = cat.get(s"$name.help") + override def placeholder(name: String): Option[String] = + cat.get(s"$name.placeholder") + + override def message(msg: UserMessage): String = cat(msg) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala index 0fa941e..fd3d3e3 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala @@ -21,6 +21,8 @@ def submit(label: HtmlMod): HtmlElement + def validationError(text: HtmlMod): HtmlElement + def input( name: String, id: Option[String] = None,