11

I created an app using the create-react-kotlin-app command and it loads in Chrome fine. I added the React Material UI package via NPM and that was successful. Now how do I use the Material UI module in my component?

Normally with JavaScript, it's a simple import Button from '@material-ui/core/Button' at the top of the component's file, but Kotlin doesn't like that.

How do I translate that line to Kotlin? I am not using Gradle.

Harry Cutts
  • 1,352
  • 11
  • 25
Alex
  • 1,082
  • 17
  • 27

2 Answers2

21

I have been struggling with this problem for days now. I came up with the following solution. First we will see multiple ways to declare external modules, then I will show how to use them .

Consider the following javascript code

import Button from '@material-ui/core/Button' // this means button is exported as default

This will be imported in kotlin in the following ways

Button.kt

@file:JsModule("@material-ui/core/Button")
@file:JsNonModule

package com.mypckage.mykillerapp

import react.Component
import react.RProps
import react.RState
import react.ReactElement

@JsName("default") // because it was exported as default
external val Button : RClass<RProps>

// way 2
@JsName("default")
external class Button : Component<RProps,RState> {
    override fun render(): ReactElement?
}

But again, if the statement intend for kotlin has to match the javascript import statement bellow,

import { Button } from "material-ui" // not exported as default

We use the following approach: Button.kt

@file:JsModule("material-ui")
@file:JsNonModule

package com.mypckage.mykillerapp

import react.Component
import react.RProps
import react.RState
import react.ReactElement

// way 1
@JsName("Button") // because it was exported as default
external val Button : RClass<RProps>

// way 2
@JsName("Button")
external class Button : Component<RProps,RState> {
    override fun render(): ReactElement?
}

once you have declared on how to use your components, you can just use them as follows:

//way 1:

fun RBuilder.render() {
    div {
        Button {
            attrs.asDynamic().className="submit-button"
            +"Submit"
        }
    }
}

//way 2:
fun RBuilder.render() {
    div {
        child(Button::class) {
            attrs.asDynamic().className="submit-button"
            +"Submit"
        }
    }
}

great. you have imported your component. But until then your are not relying on kotlin type safety and even code completion, to achieve that, you have to go to extra length

as shown bellow

external interface ButtonProps: RProps {
    var className : String
    var onClick: (Event?)->Unit
    var color: String
    // . . .
    var href: String
}

then go ahead and declare your button as

@JsModule("@material-ui/core/Button")
@JsNonModule
@JsName("default") // because it was exported as default
external val Button : RClass<ButtonProps>

and you can now use it with type safety and code completion as shown bellow

fun RBuilder.render() {
    div {
        Button {
            attrs {
                className = "submit-button"
                onClick = {
                    window.alert("Vois La")   
                }
            }
            +"Submit"
        }
    }
}

Hope this helps. Happy coding

EDIT: There is a community wrapper for material-ui components here

HINT: Use way 1, as you can see, it is less verbose

andylamax
  • 1,858
  • 1
  • 14
  • 31
  • 5
    That's awesome you can get libraries like Material-UI working with kotlin-react! But that's also a LOT of work for every component you use relative to something like, say, Typescript where you get all the type safety and code completion for free. Hopefully the improve it in the near future. – CorayThan Dec 16 '18 at 21:05
  • 2
    @CorayThan there is a tool called ts2kt that the community is working on it helps on creating the available typescript wrappers into Kotlin wrappers – andylamax Dec 18 '18 at 15:18
  • hey @andylamax, what about `import * as Sentry from '@sentry/browser';` ? – Gurupad Mamadapur Jan 14 '20 at 13:03
  • 1
    @GurupadMamadapur `import *` is a TypeScript syntax. KotlinJS is not interopable with TypeScript but Javascript. How ever you can define individual functions i.e. `init`,`configureScope`,`captureEvent`, e.t.c in a single file with anotatioin `@JSModule("@sentry/browser")` and use the files as top level functions in the module – andylamax Jan 18 '20 at 11:02
  • Hi @andylamax, do you know how to add external CSS? e.g., I need to `import "material-components-web/dist/material-components-web.min.css";` as shown here https://codesandbox.io/s/kl0w4xp95 – Franco Mar 18 '20 at 23:28
  • use `require("material-components-web/dist/material-components-web.min.css")` inside your main block. Just don't forget to add `webpack rules` for loading css files – andylamax Mar 26 '20 at 00:53
  • Hi, The react-infinite-grid React module I use your all way cannot work. Can you try react-infinite-grid? This is My Code: https://stackoverflow.com/questions/64004064/how-to-wrapper-react-infinite-grid-react-module-in-kotlin – iHad 169 Sep 28 '20 at 11:19
2

The Kotlin way for importing dependencies is close to standard JS importing:

import React from 'react';

export function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

Based on Creating a simple React component with Kotlin.

package hello

import react.*
import react.dom.*

fun RBuilder.hello(name: String) {
    h1 {
        +"Hello, $name"
    }
}

Usually (as Kotlin is Java-based) it uses Gradle tool to handle dependencies:

// part of build.gradle
kotlinFrontend {
    // ...

    npm {
        // ...

        dependency("react")
        dependency("react-dom")
        dependency("react-router")
        dependency("react-markdown")

        devDependency("css-loader")
        devDependency("babel-core")
        // ...
    }

And are referenced like above:

HomeView.kt:

// https://github.com/Kotlin/kotlin-fullstack-sample/blob/master/frontend/src/org/jetbrains/demo/thinkter/HomeView.kt

import kotlinx.html.*
import org.jetbrains.demo.thinkter.model.*
import react.*
import react.dom.*
import kotlinx.coroutines.experimental.launch

ReactMarkdown.kt:

// https://github.com/Kotlin/kotlin-fullstack-sample/blob/master/frontend/src/org/jetbrains/demo/thinkter/ReactMarkdown.kt

package org.jetbrains.demo.thinkter

import react.*

private val ReactMarkdown: dynamic = runtime.wrappers.require("react-markdown")

Based on: kotlin-fullstack-sample


In create-react-kotlin-app additionally faced the possibility of importing with @JsModule() annotation, while dependencies managing is handled in standard way via package.json:

// src/logo/Logo.kt (outcome of creating new app)
package logo

import react.*
import react.dom.*
import kotlinext.js.*
import kotlinx.html.style

@JsModule("src/logo/react.svg")
external val reactLogo: dynamic
@JsModule("src/logo/kotlin.svg")
external val kotlinLogo: dynamic

And can be also successfully used for JS libraries importing.

Another way would be to use kotlinext.js.*:

// index/index.kt

import kotlinext.js.*

fun main(args: Array<String>) {
    requireAll(require.context("src", true, js("/\\.css$/")))

    // ...
}

Which provides also require(module: String) function.

Xarvalus
  • 2,873
  • 1
  • 19
  • 29
  • But I'm not using Gradle. Isn't WebPack building all of this and converting Kotlin to JavaScript? – Alex Jul 19 '18 at 21:46
  • @alex AFAIK the importing should work exactly the same within Kotlin components, is there no possibility to work with `@material-ui/core/Button` eg like `import material-ui.core.Button`? (not exactly, but in such way) – Xarvalus Jul 19 '18 at 21:59
  • Something like `import material-ui.core.button` does not work. I get an error `Unresolved reference: material` – Alex Jul 19 '18 at 22:10
  • @alex I have found another, working way with `@JsModule`, please try this one :) (edited, on top of answer) – Xarvalus Jul 19 '18 at 22:14
  • Thank you for the update. I get an error in the browser now: `TypeError: $module$_material_ui_core_Button is not a function` – Alex Jul 19 '18 at 22:28
  • @alex You probably invoked it as function, it should be just `button { +"text" }`, without in `button()` brackets like in some other tags – Xarvalus Jul 19 '18 at 22:29
  • I have it exactly as you have it, no parentheses. – Alex Jul 19 '18 at 22:31
  • @alex Well.. that's a pity, I cannot reproduce your failed case. Nonetheless it is working in my local environment so I hope we are a bit closer to get it working in yours :) I get it working from `App.kt` file, simply by adding above lines to existing code, added `"@material-ui/core": "^1.4.0"` as `package.json` dependency with yarn, created with `create-react-kotlin-app@1.0.8`. Hope it will be helpful for solving your issue. – Xarvalus Jul 19 '18 at 22:37
  • I noticed you used a lowercase b. That presents the standard ReactElement HTML button, not the Material UI button. Try using an uppercase B. That should result in the error. – Alex Jul 19 '18 at 23:06
  • @alex Yes, unfortunately you are right.. I have found yet another way to importing with `require()/requireAll()` check the `index/index.kt` file, tried with eg `val buttonMaterial: dynamic = require("@material-ui/core/Button")` but it also does not work. – Xarvalus Jul 20 '18 at 06:53