diff --git a/build.sbt b/build.sbt index 58c734f9..07610ee3 100644 --- a/build.sbt +++ b/build.sbt @@ -25,9 +25,9 @@ lazy val commonSettings: Seq[sbt.Def.Setting[_]] = Seq( version := tyrianVersion, organization := "io.indigoengine", libraryDependencies ++= Seq( - "org.typelevel" %%% "munit-cats-effect" % Dependancies.munitCatsEffect3 % Test, - "org.typelevel" %%% "discipline-munit" % Dependancies.disciplineMUnit % Test, - "org.typelevel" %%% "cats-laws" % Dependancies.catsLaws % Test + "org.typelevel" %%% "munit-cats-effect" % Dependencies.munitCatsEffect3 % Test, + "org.typelevel" %%% "discipline-munit" % Dependencies.disciplineMUnit % Test, + "org.typelevel" %%% "cats-laws" % Dependencies.catsLaws % Test ), testFrameworks += new TestFramework("munit.Framework"), scalacOptions ++= Seq("-language:strictEquality"), @@ -57,10 +57,10 @@ lazy val commonJsSettings: Seq[sbt.Def.Setting[_]] = Seq( lazy val commonBrowserTestJsSettings: Seq[sbt.Def.Setting[_]] = Seq( scalacOptions ++= commonScalacOptions.value, libraryDependencies ++= Seq( - "org.scala-js" %%% "scalajs-dom" % Dependancies.scalajsDomVersion, - "org.typelevel" %%% "cats-effect" % Dependancies.catsEffect, - "io.circe" %%% "circe-core" % Dependancies.circe, - "io.circe" %%% "circe-parser" % Dependancies.circe + "org.scala-js" %%% "scalajs-dom" % Dependencies.scalajsDomVersion, + "org.typelevel" %%% "cats-effect" % Dependencies.catsEffect, + "io.circe" %%% "circe-core" % Dependencies.circe, + "io.circe" %%% "circe-parser" % Dependencies.circe ) ) @@ -129,7 +129,10 @@ lazy val tyrianProject = sandboxZIO.js, firefoxTests.js, chromeTests.js, - docs + docs, + tyrianHtmx.js, + tyrianHtmx.jvm, + sandboxSSR.jvm ) lazy val tyrian = @@ -145,9 +148,9 @@ lazy val tyrian = .jsSettings( commonJsSettings, libraryDependencies ++= Seq( - "org.typelevel" %%% "cats-effect-kernel" % Dependancies.catsEffect, - "co.fs2" %%% "fs2-core" % Dependancies.fs2, - "io.github.buntec" %%% "scala-js-snabbdom" % Dependancies.scalajsSnabbdom + "org.typelevel" %%% "cats-effect-kernel" % Dependencies.catsEffect, + "co.fs2" %%% "fs2-core" % Dependencies.fs2, + "io.github.buntec" %%% "scala-js-snabbdom" % Dependencies.scalajsSnabbdom ) ) @@ -163,8 +166,8 @@ lazy val tyrianIO = .jsSettings( commonJsSettings, libraryDependencies ++= Seq( - "org.scala-js" %%% "scalajs-dom" % Dependancies.scalajsDomVersion, - "org.typelevel" %%% "cats-effect" % Dependancies.catsEffect + "org.scala-js" %%% "scalajs-dom" % Dependencies.scalajsDomVersion, + "org.typelevel" %%% "cats-effect" % Dependencies.catsEffect ) ) .dependsOn(tyrian) @@ -181,9 +184,9 @@ lazy val tyrianZIO = .jsSettings( commonJsSettings, libraryDependencies ++= Seq( - "org.scala-js" %%% "scalajs-dom" % Dependancies.scalajsDomVersion, - "io.github.cquiroz" %%% "scala-java-time" % Dependancies.scalaJavaTime, - "dev.zio" %%% "zio" % Dependancies.zio + "org.scala-js" %%% "scalajs-dom" % Dependencies.scalajsDomVersion, + "io.github.cquiroz" %%% "scala-java-time" % Dependencies.scalaJavaTime, + "dev.zio" %%% "zio" % Dependencies.zio ) ) .dependsOn(tyrian) @@ -213,11 +216,28 @@ lazy val sandboxZIO = name := "Sandbox ZIO", scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) }, libraryDependencies ++= Seq( - "dev.zio" %%% "zio-interop-cats" % Dependancies.zioInteropCats + "dev.zio" %%% "zio-interop-cats" % Dependencies.zioInteropCats ), scalacOptions -= "-language:strictEquality" ) +lazy val sandboxSSR = + crossProject(JVMPlatform) + .crossType(CrossType.Pure) + .dependsOn(tyrian, tyrianHtmx) + .in(file("sandbox-ssr")) + .settings( + neverPublish, + commonSettings, + name := "sandbox-ssr", + scalacOptions -= "-language:strictEquality", + libraryDependencies ++= Seq( + "org.http4s" %% "http4s-ember-server" % Dependencies.http4sServer, + "org.http4s" %% "http4s-dsl" % Dependencies.http4sServer + ), + run / fork := true + ) + lazy val unidocs = project .enablePlugins(ScalaJSPlugin, ScalaUnidocPlugin) @@ -240,14 +260,14 @@ lazy val jsdocs = neverPublish, organization := "io.indigoengine", libraryDependencies ++= Seq( - "org.scala-js" %%% "scalajs-dom" % Dependancies.scalajsDomVersion, - "io.circe" %%% "circe-core" % Dependancies.circe, - "io.circe" %%% "circe-parser" % Dependancies.circe, - "io.indigoengine" %%% "indigo" % indigoDocsVersion, - "io.indigoengine" %%% "tyrian-io" % tyrianDocsVersion, - "org.http4s" %%% "http4s-dom" % Dependancies.http4sDom, - "org.http4s" %%% "http4s-circe" % Dependancies.http4sCirce, - "org.typelevel" %%% "cats-effect" % Dependancies.catsEffect + "org.scala-js" %%% "scalajs-dom" % Dependencies.scalajsDomVersion, + "io.circe" %%% "circe-core" % Dependencies.circe, + "io.circe" %%% "circe-parser" % Dependencies.circe, + "io.indigoengine" %%% "indigo" % indigoDocsVersion, + "io.indigoengine" %%% "tyrian-io" % tyrianDocsVersion, + "org.http4s" %%% "http4s-dom" % Dependencies.http4sDom, + "org.http4s" %%% "http4s-circe" % Dependencies.http4sCirce, + "org.typelevel" %%% "cats-effect" % Dependencies.catsEffect ), Compile / tpolecatExcludeOptions ++= Set( ScalacOptions.warnValueDiscard, @@ -311,6 +331,24 @@ lazy val chromeTests = .jsSettings(commonBrowserTestJsSettings) .dependsOn(tyrian) +lazy val tyrianHtmx = + crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("tyrian-htmx")) + .settings( + name := "tyrian-htmx", + commonSettings ++ publishSettings, + Compile / sourceGenerators += codeGen( + "tyrian.htmx", + (fullyQualifiedPath, sourceManagedDir) => + List(HtmxAttributes.gen(fullyQualifiedPath, sourceManagedDir)(HtmxAttributes.htmxAttrsList)) + ).taskValue + ) + .jsSettings( + commonJsSettings + ) + .dependsOn(tyrian) + addCommandAlias( "sandboxBuild", List( @@ -323,6 +361,18 @@ addCommandAlias( "sandboxZIO/fastLinkJS" ).mkString("", ";", ";") ) +addCommandAlias( + "sandboxSSRBuild", + List( + "sandboxSSRJVM/compile" + ).mkString("", ";", ";") +) +addCommandAlias( + "sandboxSSRServer", + List( + "sandboxSSRJVM/run" + ).mkString("", ";", ";") +) addCommandAlias( "gendocs", @@ -394,7 +444,7 @@ addCommandAlias( "tyrianJS/test", "tyrianJVM/test", "tyrianIO/test", - "tyrianZIO/test", + "tyrianZIO/test" ).mkString(";", ";", "") ) diff --git a/examples/build.sbt b/examples/build.sbt index 7d50993d..5d7192ac 100644 --- a/examples/build.sbt +++ b/examples/build.sbt @@ -115,9 +115,9 @@ lazy val indigo = name := "indigo-bridge", libraryDependencies ++= Seq( "io.indigoengine" %%% "tyrian-indigo-bridge" % tyrianVersion, - "io.indigoengine" %%% "indigo" % Dependancies.indigoVersion, - "io.indigoengine" %%% "indigo-extras" % Dependancies.indigoVersion, - "io.indigoengine" %%% "indigo-json-circe" % Dependancies.indigoVersion + "io.indigoengine" %%% "indigo" % Dependencies.indigoVersion, + "io.indigoengine" %%% "indigo-extras" % Dependencies.indigoVersion, + "io.indigoengine" %%% "indigo-json-circe" % Dependencies.indigoVersion ) ) diff --git a/examples/project/Dependancies.scala b/examples/project/Dependancies.scala index 092901d3..c7986c1a 100644 --- a/examples/project/Dependancies.scala +++ b/examples/project/Dependancies.scala @@ -1,4 +1,4 @@ -object Dependancies { +object Dependencies { val indigoVersion = "0.15.2" diff --git a/project/Dependancies.scala b/project/Dependancies.scala index d8629c29..cc001d64 100644 --- a/project/Dependancies.scala +++ b/project/Dependancies.scala @@ -1,4 +1,4 @@ -object Dependancies { +object Dependencies { val catsEffect = "3.5.2" val circe = "0.14.6" @@ -13,5 +13,5 @@ object Dependancies { val catsLaws = "2.10.0" val http4sCirce = "0.23.24" val http4sDom = "0.2.11" - + val http4sServer = "0.23.24" } diff --git a/project/HtmxAttributes.scala b/project/HtmxAttributes.scala new file mode 100644 index 00000000..140da5cf --- /dev/null +++ b/project/HtmxAttributes.scala @@ -0,0 +1,114 @@ +import sbt._ +import scala.sys.process._ + +object HtmxAttributes { + lazy val htmxAttrs = List( + Normal("hxOn", "hx-on"), // TODO: this is the deprecated version, should be hx-on:event + + Normal("hxBoost", "hx-boost"), + Normal("hxGet", "hx-get"), + Normal("hxPost", "hx-post"), + Normal("hxPushUrl", "hx-push-url"), + Normal("hxSelect", "hx-select"), + Normal("hxSelectOob", "hx-select-oob"), + Normal("hxSwap", "hx-swap"), + Normal("hxSwapOob", "hx-swap-oob"), + Normal("hxTarget", "hx-target"), + Normal("hxTrigger", "hx-trigger"), + Normal("hxVals", "hx-vals"), + Normal("hxConfirm", "hx-confirm"), + Normal("hxDelete", "hx-delete"), + NoValue("hxDisable", "hx-disable"), + Normal("hxDisabledElt", "hx-disabled-elt"), + Normal("hxDisinherit", "hx-disinherit"), + Normal("hxEncoding", "hx-encoding"), + Normal("hxExt", "hx-ext"), + Normal("hxHeaders", "hx-headers"), + Normal("hxHistory", "hx-history").withTypes("String", "Boolean"), + NoValue("hxHistoryElt", "hx-history-elt"), + Normal("hxInclude", "hx-include"), + Normal("hxIndicator", "hx-indicator"), + Normal("hxParams", "hx-params"), + Normal("hxPatch", "hx-patch"), + NoValue("hxPreserve", "hx-preserve"), + Normal("hxPrompt", "hx-prompt"), + Normal("hxPut", "hx-put"), + Normal("hxReplaceUrl", "hx-replace-url").withTypes("String", "Boolean"), + Normal("hxRequest", "hx-request"), + Normal("hxSync", "hx-sync"), + Normal("hxValidate", "hx-validate"), + Normal("hxVars", "hx-vars") + ) + + def htmxAttrsList: AttributesList = AttributesList(htmxAttrs, List(), "HtmxAttributes") + + def genAttr(tag: AttributeType, isAttribute: Boolean): String = + tag match { + case Normal("hxTrigger", Some("hx-trigger"), _) => genHtmxTrigger + case Normal(name, attrName, types) => AttributeGen.genNormal(name, attrName, types) + case NoValue(name, attrName) => AttributeGen.genNoValue(name, attrName) + } + + def triggerAttributeName = + s""" final class AttributeNameTrigger(name: String): + | def :=(value: Trigger): Attribute = Attribute(name.toString, value.render) + | final class AttributeNameTriggers(name: String): + | def :=(values: List[Trigger]): Attribute = Attribute(name.toString, values.map(_.render).mkString(",")) + |""".stripMargin + + def genHtmxTrigger: String = { + val res = s""" @targetName("hxTrigger-Trigger") + | val hxTrigger: AttributeNameTrigger = AttributeNameTrigger("hx-trigger") + | @targetName("hxTrigger-Triggers") + | val hxTrigger: AttributeNameTriggers = AttributeNameTriggers("hx-trigger") + | @targetName("hxTrigger-String") + | val hxTrigger: AttributeNameString = AttributeNameString("hx-trigger") + |""".stripMargin + + "\n" + res + "\n" + } + + def template(moduleName: String, fullyQualifiedPath: String, contents: String): String = + s"""package $fullyQualifiedPath + | + |import tyrian.* + |import tyrian.Html.* + |import scala.annotation.targetName + | + |// GENERATED by AttributeGen.scala - DO NOT EDIT + |trait $moduleName { + | + |$contents + | + |} + """.stripMargin + + def gen(fullyQualifiedPath: String, sourceManagedDir: File)(attributesList: AttributesList): File = + attributesList match { + case AttributesList(attrs, props, name) => + val file: File = + sourceManagedDir / s"$name.scala" + + if (!file.exists()) { + println("Generating Html Attributes") + + val contents: String = + AttributeGen.generateAttributeNameTypes + + AttributeGen.genAttributesAndProperties + + triggerAttributeName + + "\n\n // Attributes\n\n" + + attrs.map(a => genAttr(a, true)).mkString + + "\n\n // Properties\n\n" + + props.map(p => genAttr(p, false)).mkString + + val newContents: String = + template(name, fullyQualifiedPath, contents) + + IO.write(file, newContents) + + println("Written: " + file.getCanonicalPath) + } + + file + } +} diff --git a/sandbox-ssr/.jvm/src/main/scala/example/CorvidDatabase.scala b/sandbox-ssr/.jvm/src/main/scala/example/CorvidDatabase.scala new file mode 100644 index 00000000..dec1584f --- /dev/null +++ b/sandbox-ssr/.jvm/src/main/scala/example/CorvidDatabase.scala @@ -0,0 +1,43 @@ +package example + +import cats.Applicative +import cats.implicits.* + +trait CorvidDatabase[F[_]]: + def search(query: String): F[List[(String, String)]] + def listAll: F[List[(String, String)]] + +object CorvidDatabase: + private val data: List[(String, String)] = List( + "Canada Jay" -> "Perisoreus canadensis", + "Green Jay" -> "Cyanocorax yncas", + "Pinyon Jay" -> "Gymnorhinus cyanocephalus", + "Steller's Jay" -> "Cyanocitta stelleri", + "Blue Jay" -> "Cyanocitta cristata", + "Florida Scrub-Jay" -> "Aphelocoma coerulescens", + "California Scrub-Jay" -> "Aphelocoma californica", + "Woodhouse's Scrub-Jay" -> "Aphelocoma woodhouseii", + "Mexican Jay" -> "Aphelocoma wollweberi", + "Black-billed Magpie" -> "Pica hudsonia", + "Yellow-billed Magpie" -> "Pica nuttalli", + "Clark's Nutcracker" -> "Nucifraga columbiana", + "American Crow" -> "Corvus brachyrhynchos", + "Fish Crow" -> "Corvus ossifragus", + "Chihuahuan Raven" -> "Corvus cryptoleucus", + "Eurasian Magpie" -> "Pica pica", + "Eurasian Jackdaw" -> "Corvus monedula", + "Common Raven" -> "Corvus corax" + ) + + def fakeImpl[F[_]: Applicative]: CorvidDatabase[F] = + new CorvidDatabase[F]: + override def search(query: String): F[List[(String, String)]] = + data + .filter(row => + row._1.toLowerCase.contains(query.toLowerCase) || + row._2.toLowerCase.contains(query.toLowerCase) + ) + .pure[F] + + override def listAll: F[List[(String, String)]] = + data.pure[F] diff --git a/sandbox-ssr/.jvm/src/main/scala/example/HomePage.scala b/sandbox-ssr/.jvm/src/main/scala/example/HomePage.scala new file mode 100644 index 00000000..8b38a70a --- /dev/null +++ b/sandbox-ssr/.jvm/src/main/scala/example/HomePage.scala @@ -0,0 +1,52 @@ +package example + +import tyrian.Attribute +import tyrian.Html +import tyrian.Html.* +import tyrian.htmx.Html.* +import tyrian.htmx.Modifier +import tyrian.htmx.Trigger + +object HomePage: + def page(initialList: List[(String, String)]): Html[Nothing] = + html( + head( + script( + src := "https://unpkg.com/htmx.org@1.9.10", + Attribute( + "integrity", + "sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" + ), + Attribute("crossorigin", "anonymous") + )() + ), + body( + input( + `type` := "text", + name := "q", + hxGet := "/search", + hxTrigger := List( + Trigger(tyrian.htmx.Event.Input) + .withModifiers(Modifier.Changed, Modifier.Delay("300ms")), + Trigger(tyrian.htmx.Event.KeyDown) + .withFilter("key=='Enter'") + .withModifiers(Modifier.From("body")) + ), + hxTarget := "#search-results", + placeholder := "Search..." + ), + table(`class` := "table")( + thead( + tr(th("Name"), th("Binomial Name")) + ), + tbody(id := "search-results")( + listToHtmlTableRows(initialList) + ) + ) + ) + ) + + def listToHtmlTableRows(list: List[(String, String)]): List[Html[Nothing]] = + list.map { row => + tr(td(row._1), td(row._2)) + } diff --git a/sandbox-ssr/.jvm/src/main/scala/example/Routes.scala b/sandbox-ssr/.jvm/src/main/scala/example/Routes.scala new file mode 100644 index 00000000..f4b98693 --- /dev/null +++ b/sandbox-ssr/.jvm/src/main/scala/example/Routes.scala @@ -0,0 +1,42 @@ +package example + +import cats.effect.Sync +import cats.implicits.* +import fs2.io.file.Files +import org.http4s.Header +import org.http4s.HttpRoutes +import org.http4s.MediaType +import org.http4s.dsl.Http4sDsl +import org.http4s.dsl.impl.QueryParamDecoderMatcher +import org.http4s.headers.`Content-Type` + +object SearchQueryParamMatcher extends QueryParamDecoderMatcher[String]("q") + +object Routes: + + def routes[F[_]: Sync: Files](db: CorvidDatabase[F]): HttpRoutes[F] = + val dsl = new Http4sDsl[F] {} + import dsl.* + + HttpRoutes.of[F] { + case GET -> Root => + for { + corvids <- db.listAll + resp <- Ok( + HomePage.page(corvids).toString, + `Content-Type`(MediaType.text.html) + ) + } yield resp + + case GET -> Root / "search" :? SearchQueryParamMatcher(query) => + for { + corvids <- db.search(query) + htmlString = HomePage + .listToHtmlTableRows(corvids) + .mkString("\n") + resp <- Ok( + htmlString, + `Content-Type`(MediaType.text.html) + ) + } yield resp + } diff --git a/sandbox-ssr/.jvm/src/main/scala/example/SandboxSSR.scala b/sandbox-ssr/.jvm/src/main/scala/example/SandboxSSR.scala new file mode 100644 index 00000000..a5d550a4 --- /dev/null +++ b/sandbox-ssr/.jvm/src/main/scala/example/SandboxSSR.scala @@ -0,0 +1,23 @@ +package example + +import cats.effect.ExitCode +import cats.effect.IO +import cats.effect.IOApp +import com.comcast.ip4s.* +import org.http4s.ember.server.EmberServerBuilder +import org.http4s.implicits.* +import org.http4s.server.middleware.Logger + +object SandboxSSR extends IOApp: + + def run(args: List[String]): IO[ExitCode] = + val httpApp = Routes.routes[IO](CorvidDatabase.fakeImpl[IO]).orNotFound + val finalHttpApp = Logger.httpApp(true, false)(httpApp) + EmberServerBuilder + .default[IO] + .withHost(ipv4"0.0.0.0") + .withPort(port"8080") + .withHttpApp(finalHttpApp) + .build + .use(_ => IO.never) + .as(ExitCode.Success) diff --git a/tyrian-htmx/shared/src/main/scala/tyrian/htmx/Event.scala b/tyrian-htmx/shared/src/main/scala/tyrian/htmx/Event.scala new file mode 100644 index 00000000..5395c6a4 --- /dev/null +++ b/tyrian-htmx/shared/src/main/scala/tyrian/htmx/Event.scala @@ -0,0 +1,53 @@ +package tyrian.htmx + +enum Event(val name: String): + case DOMContent extends Event("DOMContentLoaded") + case Afterprint extends Event("afterprint") + case Beforeprint extends Event("beforeprint") + case Beforematch extends Event("beforematch") + case Beforetoggle extends Event("beforetoggle") + case Beforeunload extends Event("beforeunload") + case Blur extends Event("blur") + case Cancel extends Event("cancel") + case Change extends Event("change") + case Click extends Event("click") + case Close extends Event("close") + case Connect extends Event("connect") + case Contextlost extends Event("contextlost") + case Contextrestored extends Event("contextrestored") + case Currententrychange extends Event("currententrychange") + case Dispose extends Event("dispose") + case Error extends Event("error") + case Focus extends Event("focus") + case Formdata extends Event("formdata") + case Hashchange extends Event("hashchange") + case Input extends Event("input") + case Invalid extends Event("invalid") + case KeyDown extends Event("keydown") + case KeyPress extends Event("keypress") + case KeyUp extends Event("keyup") + case Languagechange extends Event("languagechange") + case Load extends Event("load") + case Message extends Event("message") + case Messageerror extends Event("messageerror") + case Navigate extends Event("navigate") + case Navigateerror extends Event("navigateerror") + case Navigatesuccess extends Event("navigatesuccess") + case Offline extends Event("offline") + case Online extends Event("online") + case Open extends Event("open") + case Pagehide extends Event("pagehide") + case Pagereveal extends Event("pagereveal") + case Pageshow extends Event("pageshow") + case Pointercancel extends Event("pointercancel") + case Popstate extends Event("popstate") + case Readystatechange extends Event("readystatechange") + case Rejectionhandled extends Event("rejectionhandled") + case Reset extends Event("reset") + case Select extends Event("select") + case Storage extends Event("storage") + case Submit extends Event("submit") + case Toggle extends Event("toggle") + case Unhandledrejection extends Event("unhandledrejection") + case Unload extends Event("unload") + case Visibilitychange extends Event("visibilitychange") diff --git a/tyrian-htmx/shared/src/main/scala/tyrian/htmx/Html.scala b/tyrian-htmx/shared/src/main/scala/tyrian/htmx/Html.scala new file mode 100644 index 00000000..a1860dee --- /dev/null +++ b/tyrian-htmx/shared/src/main/scala/tyrian/htmx/Html.scala @@ -0,0 +1,3 @@ +package tyrian.htmx + +object Html extends HtmxAttributes \ No newline at end of file diff --git a/tyrian-htmx/shared/src/main/scala/tyrian/htmx/Modifier.scala b/tyrian-htmx/shared/src/main/scala/tyrian/htmx/Modifier.scala new file mode 100644 index 00000000..4ce74849 --- /dev/null +++ b/tyrian-htmx/shared/src/main/scala/tyrian/htmx/Modifier.scala @@ -0,0 +1,18 @@ +package tyrian.htmx + +enum QueueType(val name: String): + case First extends QueueType("first") + case Last extends QueueType("last") + case All extends QueueType("all") + +enum Modifier(val name: String): + case Changed extends Modifier("changed") + case Once extends Modifier("once") + case Consume extends Modifier("consume") + case Delay(interval: String) extends Modifier("delay:" + interval) + case From(cssSelector: String) extends Modifier("from" + cssSelector) + case Target(cssSelector: String) extends Modifier("target:" + cssSelector) + case Throttle(interval: String) extends Modifier("throttle:" + interval) + case Queue(tpe: QueueType) extends Modifier("queue:" + tpe.name) + case Root(cssSelector: String) extends Modifier("root:" + cssSelector) + case Threshold(value: Float) extends Modifier("threshold:" + value.toString) diff --git a/tyrian-htmx/shared/src/main/scala/tyrian/htmx/Trigger.scala b/tyrian-htmx/shared/src/main/scala/tyrian/htmx/Trigger.scala new file mode 100644 index 00000000..9de0db85 --- /dev/null +++ b/tyrian-htmx/shared/src/main/scala/tyrian/htmx/Trigger.scala @@ -0,0 +1,23 @@ +package tyrian.htmx + +case class Trigger( + event: Event, + filter: Option[String], + modifiers: List[Modifier] +) { + def render: String = + val eventString = event.name + val filterString = filter.map(f => s"[$f]").getOrElse("") + val modifiersString = modifiers + .map(_.name) + .mkString(" ", " ", "") + s"$eventString$filterString$modifiersString" + + def withFilter(filter: String): Trigger = this.copy(filter = Some(filter)) + + def withModifiers(newModifiers: Modifier*): Trigger = + this.copy(modifiers = modifiers ++ newModifiers) +} + +object Trigger: + def apply(event: Event): Trigger = Trigger(event, None, List())