How it works

Layout construction

Kotlin DSL

div {
    colorized(color = "blue") {
        +"Blue text"
    }
}

This is a strongly typed code with Code Assist support and compile time check in IDE.

Kotlin provides a nice a way how to create a custom DSL – see Kotlin Type-Safe Builders

Within the DSL you can use the power of Kotlin language, see on the example below to render list of cities:

ul {
  cities.forEeach { city ->
    li {
      +"${city.name} has ${city.inhabitants}"
    }
  }    
}

You see, there is no additional templating language, it is all just single Kotlin language.

OBJECT TREE

val col = Colorized(color = "blue")
col.setContent("Blue text")

val div = Div()
div.appendChild(col)

We can write the same in a classical imperative way, but it is not so legible.
Eventually, whether you write your code in a DSL way or this classical imperative, the output will be a DOM tree:

Div
 |
 -> Colorized

Constructed HTML DOM

<div>
  <span style="color: blue;"> Blue text </span>
</div>

Every Yested component encapsulate one HTML Element. This is what the code on the left represents. However, there is no “rendering” phase, each component contains “live” DOM element. This allows a component to provide a additional functions like event handlers of encapsulated HTML element.


How is it implemented

Just a single interface

Yested application is composed from components, exactly the same as some Swing, JavaFX, WPF, Flex or GWT applications. There is just one interface (Interfaces are called Traits in Kotlin) that component must implement:

public trait Component {
    val element : HTMLElement
}

Each component must return a HTML Element that which is then included in DOM tree.
I.e. when you compose components, you actually compose a HTML Elements:

component1.appendChild(component2)
//it is actually implemented as:
component1.element.appendChild(component2.element)

Example of a Component

Image component encapsulate a img HTML tag.

public class Image : Component {

  //document.createElement is a Browser (Javascript) function
  override val element = document.createElement("img")

  //let`s expose some properties of underlying element
  public var src:String
    get() = element.src
    set(value) { element.src = value}

  //this can be simplified thanks to Kotlin Delegates to:
  public var alt:String by Attribute()

}


HTMLComponent

HTMLComponent class provides a handy base class for the rest of the framework. After all we are building an HTML application so let’s wrap basic HTML tag wrappers like div, span, ul or table.

What is a HTMLComponent

  • Expose content manipulation functions like:
    public fun appendChild(component: Component) {
      element.appendComponent(component)
    }
    
  • It expose attributes like id, style, class
  • It expose Javascript element handlers like onclick, onblur, …
  • And it has DSL functions for creating basic HTML tags
    public fun img(src:String, alt:String? = null) {
     +(Image() with {  //with a handy function which passes Image as this
                this.src = src
                this.alt = alt?:""
            })
    }
    

Extending HTMLComponent DSL

HTMLComponent itself allows us to write DSL like this:

div {
  ul {
    li {
      +"Some text" // Kotlin allows overloading of + operation, this will call: element.appendText
    }
    li {
      a(href="http://...") { +"Some link" }
    }
  }
}

But, how can we add our own components to DSL language? That’s pretty simple due to Kotlin support of extension methods:

//define some simple component with one property - an attribute name
class MyComponent(val attributeName:String) : Component { 
   override val element = createElement("span") //create HTML element span
   public fun attributeValue(value:Boolean) {
     element.appendText("Value ${attributeName} is ${value}")
   }
}

//Kotlin supports extension methods, so we can add another method to HTMLComponent
public fun HTMLComponent.myComponent(someValue:String, init:MyComponent.()->Unit) {
  val myComponent = MyComponent(someValue = someValue) //create instance of MyComponent
  myComponent.init() //init block is executed with a myComponent as a THIS (see Kotlin builders)
  this.appendChild(myComponent) //here THIS is a HTMLComponent (thanks to fun HTMLComponent.myCo..)
}

//now we can do:
div {
  myComponent("visibility") {
    attributeValue(false)
  }
}

More complex example

Example below shows a function which opens a dialog to edit a person.
Do not forget that all the code below is checked by the IDE (compiler), not in runtime!

//first define some model
data class Person(val name:String, val age:Int)

/**
 * This is a function that opens a dialog and allow user to edit a Person object.
 * personToEdit is a object which should be edited, if null is passed then empty dialog is created
 * handler parameter is a callback to save a Person
 */
fun openEditPersonDialog(personToEdit:Person? = null, handler:(Person)->Unit) {

    //define a Text Field UI component for string input
    val fieldName = StringInputField(placeholder = "Name")
    
    //define a Number Field UI component for integer input
    val fieldAge = IntInputField(placeholder = "Age")
    
    //define validator, last parameter is a validator function
    //type of 'it' is derived from a fieldName
    val validatorName = Validator(fieldName, "Name is mandatory", { it.length() > 0})
    val validatorAge = Validator(fieldAge, "Age is mandatory and greater then 0", { it != null && it > 0 })

    //create empty dialog component
    //reason it is created here is because we can call dialog.close() in functions below
    val dialog = Dialog();

    //we are still in a function! let's populate fields before dialog is displayed
    if (personToEdit != null) {
        fieldName.data = personToEdit.name
        fieldAge.data = personToEdit.age.toInt()
    }

    //this is a function which is called to save a person
    //in Kotlin, we can create a method in a method
    //this one is called from Submit button
    fun submit() {
        //first check if all validations pass
        //each validator has isValid():Boolean function
        if (allValidatorsAreValid(array(validatorName, validatorAge))) {
            //create new person object with data from field inputs
            val person = Person(
                    name = fieldName.data,
                    age = fieldAge.data!!)
            handler(person) //call callback
            dialog.close() //close dialog
        }
    }

    //"with" is a simple construct that set "this" in a following lambda block to a value before with 
    dialog with {
        header { +"Add Regular Change"} //call function "header" to set header of dialog 
        body { //set content of dialog
            btsForm(formStyle = FormStyle.HORIZONTAL) { //create a form
                item(label = { +"Name"}, validator = validatorName) { //plus is to add text
                    +fieldName //plus is to add component child
                }
                item(label = { +"Age"}, validator = validatorAge) {
                    +fieldAge
                }
            }
        }
        footer {
            btsButton(look = ButtonLook.PRIMARY, label = { +"Save" }, onclick = ::submit)
        }
    }
    
    //finally, open a dialog
    dialog.open()

}