{"id":15506278,"url":"https://github.com/adrw/hotwire-kt","last_synced_at":"2025-10-18T04:56:31.707Z","repository":{"id":44515300,"uuid":"409301573","full_name":"adrw/hotwire-kt","owner":"adrw","description":"Full stack examples of how to use Hotwire JS in Kotlin services","archived":false,"fork":false,"pushed_at":"2024-01-13T16:50:29.000Z","size":1324,"stargazers_count":9,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-23T01:57:27.271Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/adrw.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-09-22T17:46:27.000Z","updated_at":"2025-02-23T11:50:38.000Z","dependencies_parsed_at":"2024-01-13T17:34:11.401Z","dependency_job_id":"0f536237-e843-4b01-a7a3-2279dff616de","html_url":"https://github.com/adrw/hotwire-kt","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/adrw/hotwire-kt","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrw%2Fhotwire-kt","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrw%2Fhotwire-kt/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrw%2Fhotwire-kt/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrw%2Fhotwire-kt/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adrw","download_url":"https://codeload.github.com/adrw/hotwire-kt/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrw%2Fhotwire-kt/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279467386,"owners_count":26175247,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-18T02:00:06.492Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-02T09:26:35.653Z","updated_at":"2025-10-18T04:56:31.686Z","avatar_url":"https://github.com/adrw.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# hotwire-kt \n\nA collection of Kotlin examples using the Hotwire JS framework to build interactive web apps with a Kotlin [Misk](https://github.com/cashapp/misk) or [Armeria](https://armeria.dev) server backend.\n\nUsing Hotwire and `kotlinx.html` together has made building web apps fun again since I can build everything I want in sweet, sweet Kotlin.\n\n## Getting Started\n\nSee the links below to each example README.md for instructions on how to run each project locally and try it in your browser.\n\n## Examples\n\n- [flagpole](./misk-db/misk-db-feature-sample/README.md): Misk admin dashboard tab built with Hotwire, feature flags backed by a SqlDelight database\n- [dashboard-search-table](./armeria/dashboard-search-table/README.md): Armeria powered dashboard that searches a large JSON file\n- [full-spec](./armeria/full-spec/README.md): Armeria implementation of a Hotwire example elsewhere built in Spring that shows use of different Hotwire patterns (TurboFrames...etc)\n\n\n## Activate Hermit\n\nBefore building any example project, you need to activate the [Hermit](https://go.sqprod.co/hermit/)\nenvironment, unless you are using\nthe [Hermit Shell Hooks](https://cashapp.github.io/hermit/docs/usage/shell/) or Hermit IntelliJ Plugin.\n\n```shell\n. ./bin/activate-hermit\n```\n\n## Misk vs Armeria Limitations\n\nMisk supports WebSockets and in theory should be able to support all Hotwire patterns. The existing examples in the repo do not yet showcase TurboStreams but there is no reason why it should not work with Misk.\n\nNotably for Armeria, the lack of WebSocket support in Armeria limits it being a complete backend for Hotwire JS. For example, Turbo Streams which require WebSockets do not currently work. Turbo Links and Turbo Frames work well. WebSocket support is being tracked [here](https://github.com/line/armeria/issues/1076) and hopefully will be added soon.\n\n## Workflow\n\nWith the [HTML to kotlinx.html IntelliJ Plugin](https://plugins.jetbrains.com/plugin/12205-html-to-kotlinx-html), I could copy pasta any HTML UI code I found into a Kotlin file, and the corresponding `kotlinx.html` DSL would be generated and *just work*. \n\nThe one caveat is that for certain custom tags or attributes or somecases like ButtonType where `kotlinx.html` uses an enum you'll need to manually fix the DSL. Any required fixes were always straight forward in my testing.\n\nThe overall workflow of copying HTML from UI frameworks like Tailwind CSS, refactoring into Turbo Frame components, and adding props data classes for component inputs, proved to have the best of the React workflows I was used to without all the bad complex abstractions of React, Redux, Webpack, CSS-in-JS, and other novelties of the modern JS front end stack.  \n\n```kotlin\nimport xyz.adrw.hotwire.templates.turbo_frame\nimport xyz.adrw.hotwire.templates.template\nimport kotlinx.html.*\n\ndata class TableProps(\n  val data: List\u003cList\u003cString\u003e\u003e,\n  val limit: Int? = null,\n  val query: String? = null,\n)\n\nval TableId = \"table_frame\"\n\nval Table = template\u003cTableProps\u003e { props -\u003e\n  val header = props.data.first()\n  val dataRows = props.query?.let { query -\u003e\n    props.data.drop(1).filter { row -\u003e\n      row.any { it.contains(query) }\n    }\n  } ?: props.data.drop(1)\n  val truncated = dataRows.take(props.limit ?: 5)\n\n  turbo_frame(TableId) {\n    div(\"flex flex-col\") {\n      div(\"-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8\") {\n        div(\"py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8\") {\n          div(\"shadow overflow-hidden border-b border-gray-200 sm:rounded-lg\") {\n            table(\"min-w-full divide-y divide-gray-200\") {\n              thead(\"bg-gray-50\") {\n                tr {\n                  header.map {\n                    th(classes = \"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\") {\n                      attributes[\"scope\"] = \"col\"\n                      +it\n                    }\n                  }\n                    ...\n\n```\n\nThe request lifecycle of encoding inputs within the request path and using a when statement to choose which component to return with new props was simple and straightforward, a joy to write and use.\n\n```kotlin\n// File: armeria/dashboard-search-table/src/main/kotlin/.../Pages.kt\n\n/**\n * Endpoint that handles interactive UI from Hotwire Turbo Frame related clicks\n * Configuration of which UI to return and input data (ie. from forms) is provided by query parameters\n */\nclass TurboServiceHtml {\n  private val logger = getLogger\u003cTurboServiceHtml\u003e()\n\n  @Get\n  @Produces(\"text/html\")\n  fun get(params: QueryParams): String {\n    val screen = params[ScreenParam]\n    val currentValue = params[BooleanParam].toBoolean()\n    val searchQuery = params[SearchParam]\n    val limit = params[LimitParam]?.toIntOrNull()\n\n    return buildHtml {\n      Wrapper(\"\") {\n        when (screen) {\n          NavbarMobileMenuId -\u003e NavbarMobileMenu(NavbarMobileMenuProps(visible = !currentValue))\n          NavbarAvatarMenuId -\u003e NavbarAvatarMenu(NavbarAvatarMenuProps(visible = !currentValue))\n          TableId -\u003e Table(TableProps(carsData, limit, searchQuery))\n          TableWithQueryId -\u003e TableWithQuery(TableWithQueryProps(carsData, limit, searchQuery))\n          else -\u003e logger.error(\"GET [screen=$screen] not found\")\n        }\n      }\n    }\n  }\n}\n```\n\n## Resources\n\n* [Misk](https://github.com/cashapp/misk)\n* [Armeria](https://armeria.dev)\n* [Hotwire](https://hotwired.dev)\n* [kotlinx.html](https://kotlinlang.org/docs/typesafe-html-dsl.html)\n* [HTML to kotlinx.html IntelliJ Plugin](https://plugins.jetbrains.com/plugin/12205-html-to-kotlinx-html)\n* [delitescere/hotwire-samples](https://github.com/delitescere/hotwire-samples)\n* [Mustache.java](https://github.com/spullara/mustache.java)\n* [Tailwind CSS](https://tailwindcss.com/)\n* [kotlinx.html Template Types](https://the-cogitator.com/posts/blog/2020/07/21/functional-templating-with-kotlin.html)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadrw%2Fhotwire-kt","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadrw%2Fhotwire-kt","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadrw%2Fhotwire-kt/lists"}