3

I want to tinker with Kotlin Compose for Web a bit. In some of my past web projects, I made use of some web components of the Clarity Design System (CDS).

In a JavaScript or TypeScript project, you first need to install both npm packages@cds/core and @cds/city. Secondly, you have to include some global stylesheets, e.g. via HTML or sass-import. For each component you want to use, you need to import the corresponding register.js. Lastly, you can include the component in your HTML like any other tag:

<cds-button>Click me!</cds-button>

I tried to replicate the steps with Kotlin Compose for Web, but wasn't able to get it to work. Any help appreciated!

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
Endzeit
  • 4,810
  • 5
  • 29
  • 52

1 Answers1

3

Okay, I've got it to work now, which included several steps.

  1. Install npm dependencies
kotlin {
    ...
    
    sourceSets {
        val jsMain by getting {
            dependencies {
                // dependencies for Compose for Web
                implementation(compose.web.core)
                implementation(compose.runtime)

                // dependencies for Clarity Design System
                implementation(npm("@cds/core", "5.6.0"))
                implementation(npm("@cds/city", "1.1.0"))

                // dependency for webpack - see step 3
                implementation(npm("file-loader", "6.2.0"))
            }
        }
        ...
    }
}

  1. Enable css support
    This seems to be required, in order to include the global stylesheets.
kotlin {
    js(IR) {
        browser {
            ... 
            commonWebpackConfig {
                cssSupport.enabled = true
            }
        }
        ...
    }
    ...
}

  1. Add support for .woff2 files included in stylesheet of Clarity
    The stylesheet of CDS include font files of type .woff2, whose support in webpack must be configured. This can be achieved by creating the file webpack.config.d/support-fonts.js at the project root with the following content:
config.module.rules.push({
    test: /\.(woff(2)?|ttf|eot|svg|gif|png|jpe?g)(\?v=\d+\.\d+\.\d+)?$/,
    use: [{
        loader: 'file-loader',
        options: {
            name: '[name].[ext]',
            outputPath: 'fonts/'
        }
    }]
});
  1. Include global stylesheets
external fun require(module: String): dynamic

fun main() {
    require("modern-normalize/modern-normalize.css")
    require("@cds/core/global.min.css")
    require("@cds/core/styles/module.shims.min.css")
    require("@cds/city/css/bundles/default.min.css")
    
    ...
}
  1. Import register.js for desired web component
external fun require(module: String): dynamic

fun main() {
    ...
    
    require("@cds/core/button/register.js")

    ...
}
  1. Create @Composable for the web component
    Sadly this solution makes use of APIs marked as @OptIn(ComposeWebInternalApi::class), which stands for "This API is internal and is likely to change in the future". Any hints on how this may be implemented without relying on internal APIs are appreciated.
@Composable
fun CdsButton(
    status: CdsButtonStatus = CdsButtonStatus.Primary,
    attrs: AttrBuilderContext<HTMLElement>? = null,
    content: ContentBuilder<HTMLElement>? = null
) = TagElement(
    elementBuilder = CdsElementBuilder("cds-button"),
    applyAttrs = {
        if (attrs != null) apply(attrs)

        attr("status", status.attributeValue)
    },
    content = content
)

/**
 * This is a copy of the private class org.jetbrains.compose.web.dom.ElementBuilderImplementation
 */
internal class CdsElementBuilder<TElement : Element>(private val tagName: String) : ElementBuilder<TElement> {
    private val element: Element by lazy {
        document.createElement(tagName)
    }

    override fun create(): TElement = element.cloneNode() as TElement
}

sealed interface CdsButtonStatus {
    object Primary : CdsButtonStatus
    ...
}

internal val CdsButtonStatus.attributeValue
    get() = when (this) {
        CdsButtonStatus.Primary -> "primary"
        ...
    }
  1. Make us of your @Composable!
fun main() {
    ...
    
    renderComposable(rootElementId = "root") {
        CdsButton(
            status = CdsButtonStatus.Success
        ) {
            Text("It works! :-)")
        }
    }
}
Endzeit
  • 4,810
  • 5
  • 29
  • 52