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 new file mode 100644 index 0000000..855d8b3 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala @@ -0,0 +1,36 @@ +package works.iterative.ui.components.laminar.forms + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom.html +import com.raquo.laminar.nodes.ReactiveHtmlElement + +trait InputField[A]: + def render: ReactiveHtmlElement[html.Input] + +sealed trait Form[A] + +object Form: + case class Input(name: String) extends Form[String] + +trait FormBuilderModule: + def formMessagesResolver: FormMessagesResolver + def formUIFactory: FormUIFactory + def buildForm[A](form: Form[A], submit: Observer[Unit]): HtmlFormBuilder[A] = + HtmlFormBuilder[A](form, submit) + + case class HtmlFormBuilder[A](form: Form[A], submit: Observer[Unit]): + def renderForm[A](form: Form[A]): HtmlElement = form match + case Form.Input(name) => + formUIFactory.field( + formUIFactory.label(formMessagesResolver.label(name))() + )( + formUIFactory.input( + name, + placeholder = formMessagesResolver.placeholder(name) + )() + ) + + def build: HtmlElement = + formUIFactory.form( + onSubmit.preventDefault.mapTo(()) --> submit + )(renderForm(form))() 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 new file mode 100644 index 0000000..855d8b3 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala @@ -0,0 +1,36 @@ +package works.iterative.ui.components.laminar.forms + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom.html +import com.raquo.laminar.nodes.ReactiveHtmlElement + +trait InputField[A]: + def render: ReactiveHtmlElement[html.Input] + +sealed trait Form[A] + +object Form: + case class Input(name: String) extends Form[String] + +trait FormBuilderModule: + def formMessagesResolver: FormMessagesResolver + def formUIFactory: FormUIFactory + def buildForm[A](form: Form[A], submit: Observer[Unit]): HtmlFormBuilder[A] = + HtmlFormBuilder[A](form, submit) + + case class HtmlFormBuilder[A](form: Form[A], submit: Observer[Unit]): + def renderForm[A](form: Form[A]): HtmlElement = form match + case Form.Input(name) => + formUIFactory.field( + formUIFactory.label(formMessagesResolver.label(name))() + )( + formUIFactory.input( + name, + placeholder = formMessagesResolver.placeholder(name) + )() + ) + + def build: HtmlElement = + formUIFactory.form( + onSubmit.preventDefault.mapTo(()) --> submit + )(renderForm(form))() 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 new file mode 100644 index 0000000..58d0068 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormMessagesResolver.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar.forms + +import works.iterative.core.MessageCatalogue +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormMessagesResolver: + def label(name: String): String + def help(name: String): Option[String] + def placeholder(name: String): Option[String] + +object FormMessagesResolver: + given (using ctx: ComponentContext[_]): FormMessagesResolver = + MessageCatalogueFormMessagesResolver(using ctx.messages) + + given (using cat: MessageCatalogue): FormMessagesResolver = + MessageCatalogueFormMessagesResolver() + +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") 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 new file mode 100644 index 0000000..855d8b3 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala @@ -0,0 +1,36 @@ +package works.iterative.ui.components.laminar.forms + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom.html +import com.raquo.laminar.nodes.ReactiveHtmlElement + +trait InputField[A]: + def render: ReactiveHtmlElement[html.Input] + +sealed trait Form[A] + +object Form: + case class Input(name: String) extends Form[String] + +trait FormBuilderModule: + def formMessagesResolver: FormMessagesResolver + def formUIFactory: FormUIFactory + def buildForm[A](form: Form[A], submit: Observer[Unit]): HtmlFormBuilder[A] = + HtmlFormBuilder[A](form, submit) + + case class HtmlFormBuilder[A](form: Form[A], submit: Observer[Unit]): + def renderForm[A](form: Form[A]): HtmlElement = form match + case Form.Input(name) => + formUIFactory.field( + formUIFactory.label(formMessagesResolver.label(name))() + )( + formUIFactory.input( + name, + placeholder = formMessagesResolver.placeholder(name) + )() + ) + + def build: HtmlElement = + formUIFactory.form( + onSubmit.preventDefault.mapTo(()) --> submit + )(renderForm(form))() 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 new file mode 100644 index 0000000..58d0068 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormMessagesResolver.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar.forms + +import works.iterative.core.MessageCatalogue +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormMessagesResolver: + def label(name: String): String + def help(name: String): Option[String] + def placeholder(name: String): Option[String] + +object FormMessagesResolver: + given (using ctx: ComponentContext[_]): FormMessagesResolver = + MessageCatalogueFormMessagesResolver(using ctx.messages) + + given (using cat: MessageCatalogue): FormMessagesResolver = + MessageCatalogueFormMessagesResolver() + +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") 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 new file mode 100644 index 0000000..17475e4 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala @@ -0,0 +1,28 @@ +package works.iterative.ui.components.laminar.forms + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html + +trait FormUIFactory: + def form(mods: HtmlMod*)(sections: HtmlMod*)( + actions: HtmlMod* + ): ReactiveHtmlElement[html.Form] + + def section(title: HtmlMod, subtitle: Option[HtmlMod])( + content: HtmlMod* + ): HtmlElement + + def label(labelText: String, forId: Option[String] = None)( + mods: HtmlMod* + ): ReactiveHtmlElement[html.Label] + + def field(label: HtmlMod)(content: HtmlMod*): HtmlElement + + def input( + name: String, + id: Option[String] = None, + placeholder: Option[String] = None + )( + mods: HtmlMod* + ): ReactiveHtmlElement[html.Input] 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 new file mode 100644 index 0000000..855d8b3 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormBuilderModule.scala @@ -0,0 +1,36 @@ +package works.iterative.ui.components.laminar.forms + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom.html +import com.raquo.laminar.nodes.ReactiveHtmlElement + +trait InputField[A]: + def render: ReactiveHtmlElement[html.Input] + +sealed trait Form[A] + +object Form: + case class Input(name: String) extends Form[String] + +trait FormBuilderModule: + def formMessagesResolver: FormMessagesResolver + def formUIFactory: FormUIFactory + def buildForm[A](form: Form[A], submit: Observer[Unit]): HtmlFormBuilder[A] = + HtmlFormBuilder[A](form, submit) + + case class HtmlFormBuilder[A](form: Form[A], submit: Observer[Unit]): + def renderForm[A](form: Form[A]): HtmlElement = form match + case Form.Input(name) => + formUIFactory.field( + formUIFactory.label(formMessagesResolver.label(name))() + )( + formUIFactory.input( + name, + placeholder = formMessagesResolver.placeholder(name) + )() + ) + + def build: HtmlElement = + formUIFactory.form( + onSubmit.preventDefault.mapTo(()) --> submit + )(renderForm(form))() 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 new file mode 100644 index 0000000..58d0068 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormMessagesResolver.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar.forms + +import works.iterative.core.MessageCatalogue +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormMessagesResolver: + def label(name: String): String + def help(name: String): Option[String] + def placeholder(name: String): Option[String] + +object FormMessagesResolver: + given (using ctx: ComponentContext[_]): FormMessagesResolver = + MessageCatalogueFormMessagesResolver(using ctx.messages) + + given (using cat: MessageCatalogue): FormMessagesResolver = + MessageCatalogueFormMessagesResolver() + +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") 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 new file mode 100644 index 0000000..17475e4 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/forms/FormUIFactory.scala @@ -0,0 +1,28 @@ +package works.iterative.ui.components.laminar.forms + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html + +trait FormUIFactory: + def form(mods: HtmlMod*)(sections: HtmlMod*)( + actions: HtmlMod* + ): ReactiveHtmlElement[html.Form] + + def section(title: HtmlMod, subtitle: Option[HtmlMod])( + content: HtmlMod* + ): HtmlElement + + def label(labelText: String, forId: Option[String] = None)( + mods: HtmlMod* + ): ReactiveHtmlElement[html.Label] + + def field(label: HtmlMod)(content: HtmlMod*): HtmlElement + + def input( + name: String, + id: Option[String] = None, + placeholder: Option[String] = None + )( + mods: HtmlMod* + ): ReactiveHtmlElement[html.Input] diff --git a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala index e89a55b..2e3b7b8 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala @@ -4,17 +4,26 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.core.UserMessage import io.laminext.syntax.core.* +import works.iterative.core.MessageId object LaminarExtensions: extension (msg: UserMessage) inline def asElement(using ctx: ComponentContext[_]): HtmlElement = span(msg.asMod) + inline def asOptionalElement(using + ctx: ComponentContext[_] + ): Option[HtmlElement] = + ctx.messages.get(msg).map(t => span(msgAttrs(msg.id, t))) + inline def asString(using ctx: ComponentContext[_]): String = ctx.messages(msg) inline def asMod(using ctx: ComponentContext[_]): Mod[HtmlElement] = - nodeSeq(dataAttr("msgid")(msg.id.toString()), ctx.messages(msg)) + msgAttrs(msg.id, ctx.messages(msg)) + + private inline def msgAttrs(id: MessageId, text: String): HtmlMod = + nodeSeq(dataAttr("msgid")(id.toString()), text) given (using ComponentContext[_]): HtmlRenderable[UserMessage] with def toHtml(msg: UserMessage): Modifier[HtmlElement] =