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
- Yarn
- pnpm
npm install --global lerna
yarn global add lerna
pnpm add --global lerna
Creating a Monorepo
If you already have a monorepo, skip this section.
- npm
- Yarn
- pnpm
# 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
# From your top-most-directory/, initialize a workspace
lerna init
# install dependencies
yarn install
# install typescript and node types
yarn add typescript @types/node --dev
# From your top-most-directory/, initialize a workspace
lerna init
# install dependencies
pnpm install
# install typescript and node types
pnpm add typescript @types/node --save-dev
Creating a Rindo Component Library
If you already have a Rindo component library, skip this section.
- npm
- Yarn
- pnpm
cd packages/
npm init rindo components rindo-library
cd rindo-library
# Install dependencies
npm install
cd packages/
yarn create rindo components rindo-library
cd rindo-library
# Install dependencies
yarn install
cd packages/
pnpm create rindo components rindo-library
cd rindo-library
# Install dependencies
pnpm install
Creating a Kdu Component Library
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:
- npm
- Yarn
- pnpm
# 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
# From your top-most-directory/
lerna create kdu-library
# Follow the prompts and confirm
cd packages/kdu-library
# Install Kdu dependency
yarn add kdu@3 --dev
# From your top-most-directory/
lerna create kdu-library
# Follow the prompts and confirm
cd packages/kdu-library
# Install Kdu dependency
pnpm add 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": "*"
+ }
}
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.
- npm
- Yarn
- pnpm
# Install dependency (from `packages/rindo-library`)
npm install @rindo/kdu-output-target --save-dev
# Install dependency (from `packages/rindo-library`)
yarn add @rindo/kdu-output-target --dev
# Install dependency (from `packages/rindo-library`)
pnpm add @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',
}),
],
};
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.
- npm
- Yarn
- pnpm
# Build the library and wrappers (from `packages/rindo-library`)
npm run build
# Build the library and wrappers (from `packages/rindo-library`)
yarn build
# Build the library and wrappers (from `packages/rindo-library`)
pnpm 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';
Link Your Packages (Optional)
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:
- npm
- Yarn
- pnpm
# Link the working directory
npm link
# Link the working directory
yarn link
# Link the working directory
pnpm link
From your Kdu component library's directory, run the following command:
- npm
- Yarn
- pnpm
# Link the package name
npm link name-of-your-rindo-package
# Link the package name
yarn link name-of-your-rindo-package
# Link the package name
pnpm 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.
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
- Yarn
- pnpm
npm init kdu@3 my-app
yarn create kdu@3 my-app
pnpm create 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
- Yarn
- pnpm
npm run build
yarn build
pnpm 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
- Yarn
- pnpm
npm init rindo component my-component-lib
yarn create rindo component my-component-lib
pnpm create 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';
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.
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
experimentalImportInjection
flag on the Rindi config's extras
object. Once set, this will
require you to rebuild the Rindi 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
- Yarn
- pnpm
npm i -D rollup rimraf
yarn add --dev rollup rimraf
pnpm add -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'),
};
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"
}
}