rnentjes authored on 24 Jan
.github/ workflows Fix build 2 years ago
gradle/ wrapper Kotlin version 1.7.20, kotlin-komponent version 1.0.7, gradle version 7.5.1 1 year ago
src/ jsMain Kotlin version 1.9.22. Kotlin-komponent version 1.2.1. 3 months ago
.gitignore Kotlin version 1.9.22. Kotlin-komponent version 1.2.1. 3 months ago
build.gradle.kts Kotlin version 1.9.22. Kotlin-komponent version 1.2.1. 3 months ago
gradlew Initial commit 7 years ago
gradlew.bat Initial commit 7 years ago
komp-todo.iml Update Komp version 4 years ago
komp-todo.ipr Testing dom diffing 6 years ago
readme.md Update readme 3 months ago
settings.gradle.kts Kotlin version 1.7.20, kotlin-komponent version 1.0.7, gradle version 7.5.1 1 year ago
readme.md

An example on how to build a single page app with Kotlin (javascript) using static html builders.

Runing example: komp-todo

Compare it with other js frameworks: https://github.com/tastejs/todomvc/

You can also find an example of a complete application here: simple-password-manager

Dependencies:

This is the complete source:

package nl.astraeus.komp.todo

import kotlinx.browser.document
import kotlinx.html.*
import kotlinx.html.js.onClickFunction
import kotlinx.html.js.onDoubleClickFunction
import kotlinx.html.js.onKeyPressFunction
import nl.astraeus.komp.HtmlBuilder
import nl.astraeus.komp.Komponent
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.events.Event
import org.w3c.dom.events.KeyboardEvent
import kotlin.js.Date

/**
 * see: https://github.com/tastejs/todomvc/
 */

data class Todo(
  val dataId: String,
  var title: String,
  var completed: Boolean = false,
  var editing: Boolean = false
)

enum class Selection(val title: String) {
  ALL("All"),
  ACTIVE("Active"),
  COMPLETED("Completed")
}

class TodoKomponent(
  val app: TodoApp,
  val todo: Todo
) : Komponent() {

  override fun HtmlBuilder.render() {
    li {
      if (todo.editing) {
        classes = classes + "editing"
        input(classes = "edit") {
          value = todo.title
          onKeyPressFunction = { e ->
            val target = e.target
            if (target is HTMLInputElement && e is KeyboardEvent && e.keyCode == 13 && target.value.isNotBlank()) {
              app.editTodo(e, todo)
            }
          }
        }
      } else {
        if (todo.completed) {
          classes = classes + "completed"
        }
        div(classes = "view") {
          input(classes = "toggle") {
            type = InputType.checkBox
            checked = todo.completed
            onClickFunction = {
              app.todoClicked(todo)
              it.preventDefault()
            }
          }
          label(classes = "todo-content") {
            +todo.title

            onDoubleClickFunction = {
              app.setEditing(todo)
            }
          }
          button(classes = "destroy") {
            onClickFunction = {
              app.destroyTodo(todo)
            }
          }
        }
      }
    }
  }

}

class TodoApp : Komponent() {
  val todoList: MutableList<Todo> = ArrayList()
  var selected: Selection = Selection.ALL

  fun addTodo(e: Event) {
    val target = e.target

    if (target is HTMLInputElement) {
      todoList.add(Todo("${Date().getTime()}", target.value))

      this@TodoApp.requestUpdate()
    }
  }

  fun editTodo(e: Event, todo: Todo) {
    val target = e.target

    if (target is HTMLInputElement) {
      todo.title = target.value
      todo.editing = false

      requestUpdate()
    }

  }

  fun destroyTodo(todo: Todo) {
    todoList.remove(todo)

    requestUpdate()
  }

  fun selectSelection(selection: Selection) {
    selected = selection

    requestUpdate()
  }

  fun clearCompleted(e: Event) {
    for (todo in ArrayList(todoList)) {
      if (todo.completed) {
        todoList.remove(todo)
      }
    }

    requestUpdate()
  }

  fun todoClicked(todo: Todo) {
    todo.completed = !todo.completed

    requestUpdate()
  }

  private fun getItemsLeft(): Int = todoList.count { todo -> !todo.completed }

  fun setEditing(editTodo: Todo) {
    for (todo in todoList) {
      todo.editing = todo == editTodo
    }

    requestUpdate()
  }

  override fun HtmlBuilder.render() {
    section(classes = "todoapp") {
      header(classes = "header") {
        h1 { +"todos" }
        input(classes = "new-todo") {
          id = "todo_input"
          placeholder = "What needs to be done?"
          autoFocus = true
          onKeyPressFunction = { e ->
            val target = e.target
            if (target is HTMLInputElement &&
              e is KeyboardEvent &&
              e.keyCode == 13 &&
              target.value.isNotBlank()
            ) {
              e.preventDefault()
              e.stopPropagation()

              addTodo(e)

              target.value = ""
              target.defaultValue = ""
            }
          }
        }
      }

      section(classes = "main") {
        input(classes = "toggle-all") {
          type = InputType.checkBox
        }
        label {
          htmlFor = "toggle-all"
          + "Mark all as complete"
        }
        ul(classes = "todo-list") {
          for (todo in todoList) {
            if (selected == Selection.ALL ||
              (todo.completed && selected == Selection.COMPLETED) ||
              (!todo.completed && selected == Selection.ACTIVE)) {
              include(TodoKomponent(this@TodoApp, todo))
            }
          }
        }
      }

      footer(classes = "footer") {
        span(classes = "todo-count") {
          when(getItemsLeft()) {
            0 -> {
              + "No items"
            }
            1 -> {
              + "1 item left"
            }
            else ->
              + "${getItemsLeft()} items left"
          }
        }
        ul(classes = "filters") {
          for (selection in Selection.values()) {
            li {
              a {
                if (selection == selected) {
                  classes = classes + "selected"
                }
                href = "#"
                +selection.title
                onClickFunction = {
                  selectSelection(selection)
                }
              }
            }
          }
        }
        button(classes = "clear-completed") {
          +"Clear completed"
          onClickFunction = ::clearCompleted
        }
      }
    }
  }

}

fun main() {
  Komponent.create(document.body!!, TodoApp())
}