Skip to content

Installation

KForm consists of:

A typical project using KForm contains three modules:

  • A shared Kotlin Multiplatform module, containing the definition of the form, compiling to both JVM and JavaScript.
  • The React client module, consuming the shared module’s JavaScript target and using KForm to render and manage the form.
  • The JVM server module, consuming the shared module’s JVM target and using KForm to validate submitted forms.

Let us assume that all three modules are within the same Gradle build with paths :shared, :react-app, and :server.

Add the Maven Central repository to your Gradle build if not already present:

settings.gradle.kts
dependencyResolutionManagement { repositories { mavenCentral() } }

Shared/KMP module

Add the core dependency to your :shared Kotlin Multiplatform project’s commonMain dependencies, and the JavaScript bindings to the jsMain dependencies. JavaScript compilation should target ES2015 and represent Longs as bigints (which currently requires the -Xes-long-as-bigint compiler option):

shared/build.gradle.kts
plugins {
    kotlin("multiplatform")
    // …
}

kotlin {
    // …
    jvm { testRuns["test"].executionTask.configure { useJUnitPlatform() } }
    js {
        compilerOptions {
            target = "es2015"
            freeCompilerArgs.add("-Xes-long-as-bigint")
        }
        binaries.library()
        browser()
        generateTypeScriptDefinitions()
    }

    sourceSets {
        commonMain.dependencies {
            api("tech.ostack:kform:0.33.0")
            // …
        }
        jsMain.dependencies {
            implementation("tech.ostack:kform-js-bindings:0.33.0")
            // …
        }
        // …
    }
}

For consuming the shared module in your React application, you might want to create a task to pack the compiled JavaScript. The following example uses the node-gradle plugin to achieve this:

shared/build.gradle.kts
plugins {
    // …
    id("com.github.node-gradle.node") version "…"
}

val packJsPackage by
    tasks.registering(NpmTask::class) {
        group = "build"
        dependsOn("jsBrowserProductionLibraryDistribution")

        val libraryDir = "build/dist/js/productionLibrary"
        npmCommand = listOf("pack", libraryDir, "--pack-destination", "build")

        inputs.dir(libraryDir)
        outputs.file("build/your-form-shared-$version.tgz")
    }

Note

your-form should typically be replaced with the name of your root Gradle project.

Client/React application module

These instructions will be using Vite as the build tool for the React application in conjunction with Gradle via node-gradle. Other frontend bundling tools should work similarly.

Configure Gradle to consume the shared module’s JavaScript target and set up tasks to develop and build the React application:

react-app/build.gradle.kts
plugins {
    base
    id("com.github.node-gradle.node") version "…"
}

val sharedPackage = project(":shared").file("build/your-form-shared-$version.tgz")

val npmInstallShared by
    tasks.registering(NpmTask::class) {
        group = "build"
        dependsOn(":shared:packJsPackage")
        npmCommand = listOf("install", "your-form-shared@file:$sharedPackage")

        inputs.file(sharedPackage)
        outputs.dir("node_modules/your-form-shared")
    }

tasks.npmInstall { dependsOn(npmInstallShared) }

val npmRunDev by
    tasks.registering(NpmTask::class) {
        group = "development"
        dependsOn(tasks.npmInstall)
        npmCommand = listOf("run", "dev")
    }

val npmRunBuild by
    tasks.registering(NpmTask::class) {
        group = "build"
        dependsOn(tasks.npmInstall)
        npmCommand = listOf("run", "build")

        inputs.dir("src")
        inputs.file(sharedPackage)
        inputs.file("index.html")
        inputs.file("package.json")
        inputs.file("tsconfig.json")
        // …
        inputs.file("vite.config.ts")
        outputs.dir("dist/")
    }

tasks.assemble { dependsOn(npmRunBuild) }

Due to how Kotlin Multiplatform compiles code to JavaScript, consuming multiple Kotlin/JS modules from JavaScript can be a bit tricky. As such, we won’t use the published @ostack.tech/kform package from npmjs and instead consume it from the :shared module directly. We still install @ostack.tech/kform for its type definitions by installing it “renamed” as @types/ostack.tech__kform:

react-app/package.json
{
    // …
    "scripts": {
        "dev": "vite",
        "build": "tsc -b && vite build"
        // …
    },
    "dependencies": {
        "@ostack.tech/kform-react": "^0.33.0",
        "react": "…",
        "react-dom": "…"
        // …
    },
    "devDependencies": {
        "@types/ostack.tech__kform": "npm:@ostack.tech/kform@^0.33.0",
        "@types/react": "…",
        "@types/react-dom": "…",
        "@vitejs/plugin-react": "…",
        "typescript": "…",
        "vite": "…"
        // …
    }
}

To consume @ostack.tech/kform from the :shared module, we use a Vite alias targetting the ostack-kform.mjs file, like so:

react-app/vite.config.ts
import { resolve } from "node:path";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
    plugins: [react()],
    resolve: {
        alias: {
            "@ostack.tech/kform": resolve(
                import.meta.dirname,
                "node_modules/your-form-shared/ostack-kform.mjs",
            ),
        },
    },
});

Server/JVM module

In your server module, add the dependency to your shared KMP module (if exposing the core KForm library by declaring the dependency via api):

server/build.gradle.kts
dependencies {
    implementation(project(":shared"))
    // …
}

If required, you may also add a dependency to KForm’s core JVM module directly:

server/build.gradle.kts
dependencies {
    implementation("tech.ostack:kform-jvm:0.33.0")
    // …
}

You can add a task to copy the built React application to the server’s resources. E.g. for a Spring Boot application:

server/build.gradle.kts
val copyBuiltReactApp by
    tasks.registering(Copy::class) {
        from(tasks.getByPath(":react-app:npmRunBuild"))
        into("build/resources/main/static")
    }

tasks.processResources { dependsOn(copyBuiltReactApp) }