Skip to content

Commit

Permalink
Add Kotlin/JS webapp example (#3725)
Browse files Browse the repository at this point in the history
Point 4 in #3611: add a sample of Kotlin/JS frontend+ Ktor backend.

---------

Co-authored-by: 0xnm <[email protected]>
Co-authored-by: Li Haoyi <[email protected]>
  • Loading branch information
3 people authored Oct 14, 2024
1 parent 91ff5f8 commit 3542fdd
Show file tree
Hide file tree
Showing 6 changed files with 787 additions and 1 deletion.
75 changes: 75 additions & 0 deletions example/kotlinlib/web/4-webapp-kotlinjs/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package build

import mill._, kotlinlib._, kotlinlib.js._

object `package` extends RootModule with KotlinModule {

def kotlinVersion = "1.9.24"
def ktorVersion = "2.3.12"
def kotlinHtmlVersion = "0.11.0"

def mainClass = Some("webapp.WebApp")

def ivyDeps = Agg(
ivy"io.ktor:ktor-server-core-jvm:$ktorVersion",
ivy"io.ktor:ktor-server-netty-jvm:$ktorVersion",
ivy"io.ktor:ktor-server-html-builder-jvm:$ktorVersion",
)

def resources = Task {
os.makeDir(Task.dest / "webapp")
val jsPath = client.linkBinary().classes.path
// Move root.js[.map]into the proper filesystem position
// in the resource folder for the web server code to pick up
os.copy(jsPath / "client.js", Task.dest / "webapp/client.js")
os.copy(jsPath / "client.js.map", Task.dest / "webapp/client.js.map")
super.resources() ++ Seq(PathRef(Task.dest))
}

object test extends KotlinTests with TestModule.Junit5 {
def ivyDeps = super.ivyDeps() ++ Agg(
ivy"io.kotest:kotest-runner-junit5-jvm:5.9.1",
ivy"io.ktor:ktor-server-test-host-jvm:$ktorVersion"
)
}

object client extends KotlinJSModule {
def kotlinVersion = "1.9.24"

override def splitPerModule = false

def ivyDeps = Agg(
ivy"org.jetbrains.kotlinx:kotlinx-html-js:$kotlinHtmlVersion",
)
}
}

// A minimal example of a Kotlin backend server wired up with a Kotlin/JS
// front-end. The backend code is identical to the <<_todomvc_web_app>> example, but
// we replace the `main.js` client side code with the Javascript output of
// `ClientApp.kt`.
//
// Note that the client-side Kotlin code is the simplest 1-to-1 translation of
// the original Javascript, using `kotlinx.browser`, as this example is intended to
// demonstrate the `build.mill` config in Mill. A real codebase is likely to use
// Javascript or Kotlin/JS UI frameworks to manage the UI, but those are beyond the
// scope of this example.

/** Usage

> ./mill test
...webapp.WebAppTestssimpleRequest ...

> ./mill runBackground

> curl http://localhost:8092
...What needs to be done...
...

> curl http://localhost:8092/static/client.js
...bindEvent(this, 'todo-all', '/list/all', 'all')...
...

> ./mill clean runBackground

*/
80 changes: 80 additions & 0 deletions example/kotlinlib/web/4-webapp-kotlinjs/client/src/ClientApp.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package client

import kotlinx.browser.document
import kotlinx.browser.window
import org.w3c.dom.Element
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.asList
import org.w3c.dom.events.KeyboardEvent
import org.w3c.dom.get
import org.w3c.fetch.RequestInit

object ClientApp {

private var state = "all"

private val todoApp: Element
get() = checkNotNull(document.getElementsByClassName("todoapp")[0])

private fun postFetchUpdate(url: String) {
window
.fetch(url, RequestInit(method = "POST"))
.then { it.text() }
.then { text ->
todoApp.innerHTML = text
initListeners()
}
}

private fun bindEvent(cls: String, url: String, endState: String? = null) {
document.getElementsByClassName(cls)[0]
?.addEventListener("click", {
postFetchUpdate(url)
if (endState != null) state = endState
}
)
}

private fun bindIndexedEvent(cls: String, block: (String) -> String) {
for (elem in document.getElementsByClassName(cls).asList()) {
elem.addEventListener(
"click",
{ postFetchUpdate(block(elem.getAttribute("data-todo-index")!!)) }
)
}
}

fun initListeners() {
bindIndexedEvent("destroy") {
"/delete/$state/$it"
}
bindIndexedEvent("toggle") {
"/toggle/$state/$it"
}
bindEvent("toggle-all", "/toggle-all/$state")
bindEvent("todo-all", "/list/all", "all")
bindEvent("todo-active", "/list/active", "active")
bindEvent("todo-completed", "/list/completed", "completed")
bindEvent("clear-completed", "/clear-completed/$state")

val newTodoInput = document.getElementsByClassName("new-todo")[0] as HTMLInputElement
newTodoInput.addEventListener(
"keydown",
{
check(it is KeyboardEvent)
if (it.keyCode == 13) {
window
.fetch("/add/$state", RequestInit(method = "POST", body = newTodoInput.value))
.then { it.text() }
.then { text ->
newTodoInput.value = ""
todoApp.innerHTML = text
initListeners()
}
}
}
)
}
}

fun main(args: Array<String>) = ClientApp.initListeners()
Loading

0 comments on commit 3542fdd

Please sign in to comment.