Skip to content

Micro templating and DSL customizing

CJ Cutrone edited this page Dec 16, 2017 · 5 revisions

Customizing DSL

It is very likely that your page may have repeated parts, for example, "components" or containers that are typical for your application and that you don't want to "copy-paste". The first solution is to extract them to functions.

For example, consider this code with Twitter Bootstrap with dropdowns:

li {
    classes = setOf("dropdown")

    a("#", null) {
        classes = setOf("dropdown-toggle")
        attributes["data-toggle"] = "dropdown"
        role = "button"
        attributes["aria-expanded"] = "false"

        ul {
            classes = setOf("dropdown-menu")
            role = "menu"

            li { a("#") { +"Action" } }
            li { a("#") { +"Another action" } }
            li { a("#") { +"Something else here" } }
            li { classes = setOf("divider")}
            li { classes = setOf("dropdown-header"); +"Nav header" }
            li { a("#") { +"Separated link" } }
            li { a("#") { +"One more separated link" } }
        }

        span {
            classes = setOf("caret")
        }
    }
}

Doesn't look as simple as it should be, does it? To get it look better we can introduce some extension functions to extend DSL

Let's introduce a top level function dropdown like this:

fun UL.dropdown(block : LI.() -> Unit) {
    li("dropdown") {
        block()
    }
}

As you can see, the function dropdown can be called on <UL> and consumes a lambda with this of type LI. So the content inside this lambda will be executed inside <LI>. Inside we construct li with class dropdown and call the lambda inside it.

We can introduce more extension functions to be able to shortcut even more code

fun LI.dropdownToggle(block : A.() -> Unit) {
    a("#", null, "dropdown-toggle") {
        attributes["data-toggle"] = "dropdown"
        role = "button"
        attributes["aria-expanded"] = "false"

        block()

        span {
            classes = setOf("caret")
        }
    }
}

fun LI.dropdownMenu(block : UL.() -> Unit) : Unit = ul("dropdown-menu") {
    role = "menu"

    block()
}

fun UL.dropdownHeader(text : String) : Unit = li { classes = setOf("dropdown-header"); +text }
fun UL.divider() : Unit = li { classes = setOf("divider")}

After that, we can create dropdowns easier

createHTML().ul {
    dropdown {
        dropdownToggle { +"Dropdown" }
        dropdownMenu {
            li { a("#") { +"Action" } }
            li { a("#") { +"Another action" } }
            li { a("#") { +"Something else here" } }
            divider()
            dropdownHeader("Nav header")
            li { a("#") { +"Separated link" } }
            li { a("#") { +"One more separated link" } }
        }
    }

It looks much clearer and it is easier to understand and modify.

Introduce custom tag

There are cases when you need something missing in which case, you have two options: file a bug report and wait or write the code yourself.

First of all, we have to define a tag type

class CUSTOM(consumer: TagConsumer<*>) : 
        HTMLTag("custom", consumer, emptyMap(), 
                inlineTag = true, 
                emptyTag = false), HtmlInlineTag {
}

We know that it is applicable to <div> only so let's declare it

fun DIV.custom(block: CUSTOM.() -> Unit = {}) {
    CUSTOM(consumer).visit(block)
}

If you want it to be available on the root (like this: appendHTML().custom { }) you have to declare it on TagConsumer:

fun <T> TagConsumer<T>.custom(block: CUSTOM.() -> Unit = {}): T {
    return CUSTOM(this).visitAndFinalize(this, block)
}

So now let's see how it works

// inside div
buildString {
    appendHTML(false).div {
        custom {
            span { +"content" }
        }
    }
}

// on the root
buildString {
    appendHTML(false).custom {
        span {
            +"content"
        }
    }
}