Skip to main content
Version: Next 🚧

Kdu Integration

Supports: Kdu 3 • TypeScript 4.0+ • Rindo v2.9.0+

Rindo can generate Kdu component wrappers for your web components. This allows your Rindo components to be used within a Kdu 3 application. The benefits of using Rindo's component wrappers over the standard web components include:

  • Type checking with your components.
  • Integration with the router link and Kdu router.
  • Optionally, form control web components can be used with k-model.

Setup

Project Structure

We recommend using a monorepo structure for your component library with component wrappers. Your project workspace should contain your Rindo component library and the library for the generate Kdu component wrappers.

An example project set-up may look similar to:

top-most-directory/
└── packages/
├── kdu-library/
│ └── lib/
│ ├── plugin.ts
│ └── index.ts
└── rindo-library/
├── rindo.config.js
└── src/components

This guide uses Lerna for the monorepo, but you can use other solutions such as Nx, Turborepo, etc.

To use Lerna with this walk through, globally install Lerna:

npm install --global lerna

Creating a Monorepo

note

If you already have a monorepo, skip this section.

# From your top-most-directory/, initialize a workspace
lerna init

# install dependencies
npm install

# install typescript and node types
npm install typescript @types/node --save-dev

Creating a Rindo Component Library

note

If you already have a Rindo component library, skip this section.

cd packages/
npm init rindo components rindo-library
cd rindo-library
# Install dependencies
npm install

Creating a Kdu Component Library

note

If you already have a Kdu component library, skip this section.

The first time you want to create the component wrappers, you will need to have a Kdu library package to write to.

Using Lerna and Kdu's CLI, generate a workspace and a library for your Kdu component wrappers:

# From your top-most-directory/
lerna create kdu-library
# Follow the prompts and confirm
cd packages/kdu-library
# Install Kdu dependency
npm install kdu@3 --save-dev

Lerna does not ship with a TypeScript configuration. At the root of the workspace, create a tsconfig.json:

{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"sourceMap": true,
"lib": ["es6"]
},
"exclude": ["node_modules", "**/*.spec.ts", "**/__tests__/**"]
}

In your kdu-library project, create a project specific tsconfig.json that will extend the root config:

{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"lib": ["dom", "es2020"],
"module": "es2015",
"moduleResolution": "node",
"target": "es2017",
"skipLibCheck": true
},
"include": ["lib"],
"exclude": ["node_modules"]
}

Update the generated package.json in your kdu-library, adding the following options to the existing config:

{
- "main": "lib/kdu-library.js",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
"files": [
- 'lib'
+ 'dist'
],
"scripts": {
- "test": "echo \"Error: run tests from root\" && exit 1"
+ "test": "echo \"Error: run tests from root\" && exit 1",
+ "build": "npm run tsc",
+ "tsc": "tsc -p . --outDir ./dist"
- }
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "dependencies": {
+ "rindo-library": "*"
+ }
}
note

The rindo-library dependency is how Lerna knows to resolve the internal Rindo library dependency. See Lerna's documentation on package dependency management for more information.

Adding the Kdu Output Target

Install the @rindo/kdu-output-target dependency to your Rindo component library package.

# Install dependency (from `packages/rindo-library`)
npm install @rindo/kdu-output-target --save-dev

In your project's rindo.config.ts, add the kduOutputTarget configuration to the outputTargets array:

import { kduOutputTarget } from '@rindo/kdu-output-target';

export const config: Config = {
namespace: 'rindo-library',
outputTargets: [
// By default, the generated proxy components will
// leverage the output from the `dist` target, so we
// need to explicitly define that output alongside the
// Kdu target
{
type: 'dist',
esmLoaderPath: '../loader',
},
kduOutputTarget({
componentCorePackage: 'rindo-library',
proxiesFile: '../kdu-library/lib/components.ts',
}),
],
};
tip

The proxiesFile is the relative path to the file that will be generated with all the Kdu component wrappers. You will replace the file path to match your project's structure and respective names. You can generate any file name instead of components.ts.

The componentCorePackage should match the name field in your Rindo project's package.json.

You can now build your Rindo component library to generate the component wrappers.

# Build the library and wrappers (from `packages/rindo-library`)
npm run build

If the build is successful, you will now have contents in the file specified in proxiesFile.

Registering Custom Elements

To register your web components for the lazy-loaded (hydrated) bundle, you will need to create a new file for the Kdu plugin:

// packages/kdu-library/lib/plugin.ts

import { Plugin } from 'kdu';
import { applyPolyfills, defineCustomElements } from 'rindo-library/loader';

export const ComponentLibrary: Plugin = {
async install() {
applyPolyfills().then(() => {
defineCustomElements();
});
},
};

You can now finally export the generated component wrappers and the Kdu plugin for your component library to make them available to implementers. Export the plugin.ts file created in the previous step, as well as the file proxiesFile generated by the Kdu Output Target:

// packages/kdu-library/lib/index.ts
export * from './components';
export * from './plugin';
note

If you are using a monorepo tool (Lerna, Nx, etc.), skip this section.

Before you can successfully build a local version of your Kdu component library, you will need to link the Rindo package to the Kdu package.

From your Rindo project's directory, run the following command:

# Link the working directory
npm link

From your Kdu component library's directory, run the following command:

# Link the package name
npm link name-of-your-rindo-package

The name of your Rindo package should match the name property from the Rindo component library's package.json.

Your component libraries are now linked together. You can make changes in the Rindo component library and run npm run build to propagate the changes to the Kdu component library.

note

As an alternative to npm link, you can also run npm install with a relative path to your Rindo component library. This strategy, however, will modify your package.json so it is important to make sure you do not commit those changes.

Consumer Usage

Creating a Consumer Kdu App

From the packages/ directory, run the following command to generate a Kdu app:

npm init kdu@3 my-app

Follow the prompts and choose the options best for your project.

You'll also need to link your Kdu component library as a dependency. This step makes it so your Kdu app will be able to correctly resolve imports from your Kdu library. This is easily done by modifying your Kdu app's package.json to include the following:

"dependencies": {
"kdu-library": "*"
}

For more information, see the Lerna documentation on package dependency management.

Lastly, you'll want to update the generated wite.config.ts:

export default defineConfig({
- plugins: [kdu(), kduJsx()],
+ plugins: [
+ kdu({
+ template: {
+ compilerOptions: {
+ // treat all tags with a dash as custom elements
+ isCustomElement: (tag) => tag.includes('-'),
+ },
+ },
+ }),
+ kduJsx(),
+ ],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

This will prevent Kdu from logging a warning about failing to resolve components (e.g. "Failed to resolve component: my-component").

Consuming the Kdu Wrapper Components

This section covers how developers consuming your Kdu component wrappers will use your package and component wrappers.

Before you can use your Kdu proxy components, you'll need to build your Kdu component library. From packages/kdu-library simply run:

npm run build

In your main.js file, import your component library plugin and use it:

// src/main.js
import { ComponentLibrary } from 'kdu-library';

createApp(App).use(ComponentLibrary).mount('#app');

In your page or component, you can now import and use your component wrappers:

<template>
<my-component first="Your" last="Name"></my-component>
</template>

API

componentCorePackage

Optional

Default: The components.d.ts file in the Rindo project's package.json types field

Type: string

The name of the Rindo package where components are available for consumers (i.e. the value of the name property in your Rindo component library's package.json). This is used during compilation to write the correct imports for components.

For a starter Rindo project generated by running:

npm init rindo component my-component-lib

The componentCorePackage would be set to:

// rindo.config.ts

export const config: Config = {
...,
outputTargets: [
kduOutputTarget({
componentCorePackage: 'my-component-lib',
// ... additional config options
})
]
}

Which would result in an import path like:

import { defineCustomElement as defineMyComponent } from 'my-component-lib/components/my-component.js';
note

Although this field is optional, it is highly recommended that it always be defined to avoid potential issues with paths not being generated correctly when combining other API arguments.

componentModels

Optional

Default: []

Type: ComponentModelConfig[]

This option is used to define which components should be integrated with k-model. It allows you to set what the target prop is (i.e. value), which event will cause the target prop to change, and more.

const componentModels: ComponentModelConfig[] = [
{
elements: ['my-input', 'my-textarea'],
event: 'k-on-change',
externalEvent: 'on-change',
targetAttr: 'value',
},
];

export const config: Config = {
namespace: 'rindo-library',
outputTargets: [
kduOutputTarget({
componentCorePackage: 'component-library',
proxiesFile: '{path to your proxy file}',
componentModels: componentModels,
}),
],
};

customElementsDir

Optional

Default: 'dist/components'

Type: string

If includeImportCustomElements is true, this option can be used to specify the directory where the generated custom elements live. This value only needs to be set if the dir field on the dist-custom-elements output target was set to something other than the default directory.

excludeComponents

Optional

Default: []

Type: string[]

This lets you specify component tag names for which you don't want to generate Kdu wrapper components. This is useful if you need to write framework-specific versions of components. For instance, in Family Framework, this is used for routing components - like tabs - so that Family Framework can integrate better with Kdu's Router.

includeDefineCustomElements

Optional

Default: true

Type: boolean

If true, all Web Components will automatically be registered with the Custom Elements Registry. This can only be used when lazy loading Web Components and will not work when includeImportCustomElements is true.

includeImportCustomElements

Optional

Default: undefined

Type: boolean

If true, the output target will import the custom element instance and register it with the Custom Elements Registry when the component is imported inside of a user's app. This can only be used with the Custom Elements Bundle and will not work with lazy loaded components.

note

The configuration for the Custom Elements output target must set the export behavior to single-export-module for the wrappers to generate correctly.

includePolyfills

Optional

Default: true

Type: boolean

If true, polyfills will automatically be imported and the applyPolyfills function will be called in your proxies file. This can only be used when lazy loading Web Components and will not work when includeImportCustomElements is enabled.

loaderDir

Optional

Default: /dist/loader

Type: string

The path to where the defineCustomElements helper method exists within the built project. This option is only used when includeDefineCustomElements is enabled.

proxiesFile

Required

Type: string

This parameter allows you to name the file that contains all the component wrapper definitions produced during the compilation process. This is the first file you should import in your Kdu project.

FAQ

Do I have to use the dist output target?

No! By default, this output target will look to use the dist output, but the output from dist-custom-elements can be used alternatively.

To do so, simply set the includeImportCustomElements option in the output target's config and ensure the custom elements output target is added to the Rindo config's output target array:

// rindo.config.ts

export const config: Config = {
...,
outputTargets: [
// Needs to be included
{
type: 'dist-custom-elements'
},
kduOutputTarget({
componentCorePackage: 'component-library',
proxiesFile: '{path to your proxy file}',
// This is what tells the target to use the custom elements output
includeImportCustomElements: true
})
]
}

Now, all generated imports will point to the default directory for the custom elements output. If you specified a different directory using the dir property for dist-custom-elements, you need to also specify that directory for the Kdu output target. See the API section for more information.

In addition, all the Web Components will be automatically defined as the generated component modules are bootstrapped.

TypeError: Cannot read properties of undefined (reading 'isProxied')

If you encounter this error when running the Kdu application consuming your proxy components, you can set the enableImportInjection flag on the Rindo config's extras object. Once set, this will require you to rebuild the Rindo component library and the Kdu component library.

Kdu warns "Failed to resolve component: my-component"

Lazy loaded bundle

If you are using Kdu CLI, update your kdu.config.js to match your custom element selector as a custom element:

const { defineConfig } = require('@kdujs/cli-service');
module.exports = defineConfig({
transpileDependencies: true,
chainWebpack: (config) => {
config.module
.rule('kdu')
.use('kdu-loader')
.tap((options) => {
options.compilerOptions = {
...options.compilerOptions,
// The rindo-library components start with "my-"
isCustomElement: (tag) => tag.startsWith('my-'),
};
return options;
});
},
});

Custom elements bundle

If you see this warning, then it is likely you did not import your component from your Kdu library: kdu-library. By default, all Kdu components are locally registered, meaning you need to import them each time you want to use them.

Without importing the component, you will only get the underlying Web Component, and Kdu-specific features such as k-model will not work.

To resolve this issue, you need to import the component from kdu-library (your package name) and provide it to your Kdu component:

<template>
<my-component first="Your" last="Name"></my-component>
</template>

<script lang="ts">
import { MyComponent } from 'kdu-library';
import { defineComponent } from 'kdu';

export default defineComponent({
components: { MyComponent },
});
</script>

Kdu warns: "slot attributes are deprecated kdu/no-deprecated-slot-attribute"

The slots that are used in Rindo are Web Component slots, which are different than the slots used in Kdu 2. Unfortunately, the APIs for both are very similar, and your linter is likely getting the two confused.

You will need to update your lint rules in .eslintrc.js to ignore this warning:

module.exports = {
rules: {
'kdu/no-deprecated-slot-attribute': 'off',
},
};

Method on component is not a function

In order to access a method on a Rindo component in Kdu, you will need to access the underlying Web Component instance first:

// ✅ This is correct
myComponentRef.value.$el.someMethod();

// ❌ This is incorrect and will result in an error.
myComponentRef.value.someMethod();

Output commonjs bundle for Node environments

First, install rollup and rimraf as dev dependencies:

npm i -D rollup rimraf

Next, create a rollup.config.js in /packages/kdu-library/:

const external = ['kdu', 'kdu-router'];

export default {
input: 'dist-transpiled/index.js',
output: [
{
dir: 'dist/',
entryFileNames: '[name].esm.js',
chunkFileNames: '[name]-[hash].esm.js',
format: 'es',
sourcemap: true,
},
{
dir: 'dist/',
format: 'commonjs',
preferConst: true,
sourcemap: true,
},
],
external: (id) => external.includes(id) || id.startsWith('rindo-library'),
};
info

Update the external list for any external dependencies. Update the rindo-library to match your Rindo library's package name.

Next, update your package.json to include the scripts for rollup:

{
"scripts": {
"build": "npm run clean && npm run tsc && npm run bundle",
"bundle": "rollup --config rollup.config.js",
"clean": "rimraf dist dist-transpiled"
}
}