Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Kotlin/JS webapp example #3725

Merged
merged 4 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading