kotlinweb-componentkotlin-jsjetbrains-composecompose-for-web

Make use of web component library in Kotlin Compose for Web


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!


Solution

  • 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! :-)")
            }
        }
    }