├── starter
├── README.md
├── viewer.html
└── editor.html
├── custom-components
├── app
│ ├── empty.json
│ ├── style.css
│ ├── extension
│ │ ├── render
│ │ │ ├── styles.css
│ │ │ ├── index.js
│ │ │ ├── range.svg
│ │ │ └── Range.js
│ │ └── propertiesPanel
│ │ │ ├── index.js
│ │ │ └── CustomPropertiesProvider.js
│ ├── index.html
│ └── index.js
├── .eslintrc
├── docs
│ └── screenshot.png
├── .gitignore
├── package.json
├── webpack.config.js
└── README.md
├── custom-button
├── app
│ ├── extension
│ │ ├── render
│ │ │ ├── styles.css
│ │ │ ├── index.js
│ │ │ ├── feedback.svg
│ │ │ └── FeedbackButton.js
│ │ └── propertiesPanel
│ │ │ ├── index.js
│ │ │ └── CustomPropertiesProvider.js
│ ├── style.css
│ ├── index.html
│ ├── form.json
│ └── index.js
├── .eslintrc
├── docs
│ └── screenshot.png
├── .gitignore
├── package.json
├── README.md
└── webpack.config.js
├── README.md
├── .github
└── workflows
│ └── ADD_TO_HTO_PROJECT.yml
└── custom-properties
├── README.md
├── customProperties.js
└── viewer.html
/starter/README.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/custom-components/app/empty.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "default",
3 | "components": []
4 | }
--------------------------------------------------------------------------------
/custom-button/app/extension/render/styles.css:
--------------------------------------------------------------------------------
1 | .feedback-button-container {
2 | width: 100%;
3 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # form-js-examples
2 |
3 | Examples that illustrate uses of [form-js](https://github.com/bpmn-io/form-js).
4 |
--------------------------------------------------------------------------------
/custom-button/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "plugin:bpmn-io/node",
4 | "plugin:bpmn-io/browser"
5 | ]
6 | }
--------------------------------------------------------------------------------
/custom-button/docs/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bpmn-io/form-js-examples/HEAD/custom-button/docs/screenshot.png
--------------------------------------------------------------------------------
/custom-components/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "plugin:bpmn-io/node",
4 | "plugin:bpmn-io/browser"
5 | ]
6 | }
--------------------------------------------------------------------------------
/custom-components/docs/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bpmn-io/form-js-examples/HEAD/custom-components/docs/screenshot.png
--------------------------------------------------------------------------------
/custom-button/app/style.css:
--------------------------------------------------------------------------------
1 | body, html {
2 | height: 100%;
3 | margin: 0;
4 | padding: 0;
5 | font-family: sans-serif;
6 | }
7 |
8 | #form {
9 | max-width: 100%;
10 | height: 100%;
11 | }
--------------------------------------------------------------------------------
/custom-components/app/style.css:
--------------------------------------------------------------------------------
1 | body, html {
2 | height: 100%;
3 | margin: 0;
4 | padding: 0;
5 | font-family: sans-serif;
6 | }
7 |
8 | #form {
9 | max-width: 100%;
10 | height: 100%;
11 | }
--------------------------------------------------------------------------------
/custom-components/app/extension/render/styles.css:
--------------------------------------------------------------------------------
1 | .range-group {
2 | width: 100%;
3 | display: flex;
4 | flex-direction: row;
5 | }
6 |
7 | .range-group input {
8 | width: 100%;
9 | }
10 | .range-group .range-value {
11 | margin-left: 4px;
12 | }
--------------------------------------------------------------------------------
/custom-components/app/extension/propertiesPanel/index.js:
--------------------------------------------------------------------------------
1 | import { CustomPropertiesProvider } from './CustomPropertiesProvider';
2 |
3 | export default {
4 | __init__: [ 'rangePropertiesProvider' ],
5 | rangePropertiesProvider: [ 'type', CustomPropertiesProvider ]
6 | };
--------------------------------------------------------------------------------
/custom-button/app/extension/propertiesPanel/index.js:
--------------------------------------------------------------------------------
1 | import { CustomPropertiesProvider } from './CustomPropertiesProvider';
2 |
3 | export default {
4 | __init__: [ 'feedbackButtonPropertiesProvider' ],
5 | feedbackButtonPropertiesProvider: [ 'type', CustomPropertiesProvider ]
6 | };
--------------------------------------------------------------------------------
/custom-button/app/extension/render/index.js:
--------------------------------------------------------------------------------
1 | import { FeedbackButtonRenderer, type } from './FeedbackButton';
2 |
3 | class CustomFormFields {
4 | constructor(formFields) {
5 | formFields.register(type, FeedbackButtonRenderer);
6 | }
7 | }
8 |
9 |
10 | export default {
11 | __init__: [ 'feedbackButton' ],
12 | feedbackButton: [ 'type', CustomFormFields ]
13 | };
--------------------------------------------------------------------------------
/custom-button/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | form-js Custom Button example
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/custom-components/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | form-js Custom Components example
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/custom-button/app/form.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "default",
3 | "components": [
4 | {
5 | "type": "textarea",
6 | "key": "message",
7 | "label": "Provide feedback"
8 | },
9 | {
10 | "type": "feedbackButton",
11 | "label": "Send feedback",
12 | "endpoint": "https://www.example.com/feedback",
13 | "message": "=message"
14 | }
15 | ]
16 | }
--------------------------------------------------------------------------------
/custom-button/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | public
14 | *.local
15 |
16 | # Editor directories and files
17 | .vscode/*
18 | !.vscode/extensions.json
19 | .idea
20 | .DS_Store
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 |
--------------------------------------------------------------------------------
/custom-components/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | public
14 | *.local
15 |
16 | # Editor directories and files
17 | .vscode/*
18 | !.vscode/extensions.json
19 | .idea
20 | .DS_Store
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 |
--------------------------------------------------------------------------------
/custom-components/app/extension/render/index.js:
--------------------------------------------------------------------------------
1 | import { RangeRenderer, rangeType } from './Range';
2 |
3 | /*
4 | * This is the module definition of the custom field. This goes
5 | * into the Form instance via `additionalModules`.
6 | */
7 | class CustomFormFields {
8 | constructor(formFields) {
9 | formFields.register(rangeType, RangeRenderer);
10 | }
11 | }
12 |
13 |
14 | export default {
15 | __init__: [ 'rangeField' ],
16 | rangeField: [ 'type', CustomFormFields ]
17 | };
--------------------------------------------------------------------------------
/custom-button/app/extension/render/feedback.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/custom-components/app/extension/render/range.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/custom-button/app/index.js:
--------------------------------------------------------------------------------
1 | import { FormPlayground } from '@bpmn-io/form-js';
2 |
3 | import FeedbackButtonRenderExtension from './extension/render';
4 | import FeedbackButtonPropertiesPanelExtension from './extension/propertiesPanel';
5 |
6 | import '@bpmn-io/form-js/dist/assets/form-js.css';
7 | import '@bpmn-io/form-js/dist/assets/form-js-editor.css';
8 | import '@bpmn-io/form-js/dist/assets/form-js-playground.css';
9 |
10 | import './style.css';
11 |
12 | import schema from './form.json';
13 |
14 | new FormPlayground({
15 | container: document.querySelector('#form'),
16 | schema: schema,
17 | data: {},
18 | additionalModules: [
19 | FeedbackButtonRenderExtension
20 | ],
21 | editorAdditionalModules: [
22 | FeedbackButtonPropertiesPanelExtension
23 | ]
24 | });
25 |
--------------------------------------------------------------------------------
/custom-components/app/index.js:
--------------------------------------------------------------------------------
1 | import { FormPlayground } from '@bpmn-io/form-js';
2 |
3 | import RenderExtension from './extension/render';
4 | import PropertiesPanelExtension from './extension/propertiesPanel';
5 |
6 | import '@bpmn-io/form-js/dist/assets/form-js.css';
7 | import '@bpmn-io/form-js/dist/assets/form-js-editor.css';
8 | import '@bpmn-io/form-js/dist/assets/form-js-playground.css';
9 |
10 | import './style.css';
11 |
12 | import schema from './empty.json';
13 |
14 | new FormPlayground({
15 | container: document.querySelector('#form'),
16 | schema: schema,
17 | data: {},
18 |
19 | // load rendering extension
20 | additionalModules: [
21 | RenderExtension
22 | ],
23 |
24 | // load properties panel extension
25 | editorAdditionalModules: [
26 | PropertiesPanelExtension
27 | ]
28 | });
29 |
--------------------------------------------------------------------------------
/custom-button/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "custom-button",
3 | "private": true,
4 | "version": "0.0.0",
5 | "scripts": {
6 | "all": "run-s build",
7 | "build": "webpack",
8 | "start": "run-s build serve",
9 | "dev": "run-p \"build -- --watch\" serve",
10 | "serve": "sirv public --dev"
11 | },
12 | "devDependencies": {
13 | "copy-webpack-plugin": "^11.0.0",
14 | "css-loader": "^6.8.1",
15 | "eslint": "^8.51.0",
16 | "eslint-plugin-bpmn-io": "^1.0.0",
17 | "npm-run-all": "^4.1.5",
18 | "raw-loader": "^4.0.2",
19 | "sirv-cli": "^2.0.2",
20 | "style-loader": "^3.3.3",
21 | "webpack": "^5.88.2",
22 | "webpack-cli": "^5.1.4"
23 | },
24 | "dependencies": {
25 | "@bpmn-io/form-js": "^1.17.0",
26 | "@bpmn-io/properties-panel": "^3.12.0",
27 | "classnames": "^2.3.2",
28 | "diagram-js": "^12.7.0",
29 | "min-dash": "^4.1.1"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/custom-components/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "custom-components",
3 | "private": true,
4 | "version": "0.0.0",
5 | "scripts": {
6 | "all": "run-s build",
7 | "build": "webpack",
8 | "start": "run-s build serve",
9 | "dev": "run-p \"build -- --watch\" serve",
10 | "serve": "sirv public --dev"
11 | },
12 | "devDependencies": {
13 | "copy-webpack-plugin": "^11.0.0",
14 | "css-loader": "^6.8.1",
15 | "eslint": "^8.51.0",
16 | "eslint-plugin-bpmn-io": "^1.0.0",
17 | "npm-run-all": "^4.1.5",
18 | "raw-loader": "^4.0.2",
19 | "sirv-cli": "^2.0.2",
20 | "style-loader": "^3.3.3",
21 | "webpack": "^5.88.2",
22 | "webpack-cli": "^5.1.4"
23 | },
24 | "dependencies": {
25 | "@bpmn-io/form-js": "^1.17.0",
26 | "@bpmn-io/properties-panel": "^3.12.0",
27 | "classnames": "^2.3.2",
28 | "diagram-js": "^12.7.0",
29 | "min-dash": "^4.1.1"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/custom-button/README.md:
--------------------------------------------------------------------------------
1 | # form-js Custom Button Example
2 |
3 | This example uses [form-js](https://github.com/bpmn-io/form-js) to implement a custom button component.
4 |
5 | 
6 |
7 | ## About
8 |
9 | This example builds on the general [custom components example](./../custom-components).
10 |
11 | It demonstrates how to implement a custom feedback button component that triggers a custom behavior on click. Furthermore, it shows how to use the form-js [FEEL tooling](https://docs.camunda.io/docs/components/modeler/feel/what-is-feel/) to make the feedback message dynamic.
12 |
13 | ## Building
14 |
15 | You need a [NodeJS](http://nodejs.org) development stack with [npm](https://npmjs.org) installed to build the project.
16 |
17 | To install all project dependencies execute
18 |
19 | ```
20 | npm install
21 | ```
22 |
23 | Spin up a development setup by executing
24 |
25 | ```
26 | npm run dev
27 | ```
--------------------------------------------------------------------------------
/.github/workflows/ADD_TO_HTO_PROJECT.yml:
--------------------------------------------------------------------------------
1 | name: ADD_TO_HTO_PROJECT
2 |
3 | on:
4 | issues:
5 | types:
6 | - opened
7 | - transferred
8 | pull_request:
9 | types:
10 | - opened
11 |
12 | jobs:
13 | add-to-project:
14 | name: Add issue to project
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/add-to-project@0da8e46333d7b6e01d0e857452a1e99cb47be205
18 | name: Add to project
19 | id: add-project
20 | with:
21 | project-url: ${{ secrets.HTO_PROJECT_URL }}
22 | github-token: ${{ secrets.ADD_TO_HTO_PROJECT_PAT }}
23 | - name: Define project column
24 | run: echo "COLUMN=${{ github.event_name == 'pull_request' && 'Needs Review' || 'Inbox' }}" >> $GITHUB_ENV
25 | - uses: titoportas/update-project-fields@421a54430b3cdc9eefd8f14f9ce0142ab7678751
26 | name: Update project column
27 | id: update-column
28 | with:
29 | project-url: ${{ secrets.HTO_PROJECT_URL }}
30 | github-token: ${{ secrets.ADD_TO_HTO_PROJECT_PAT }}
31 | item-id: ${{ steps.add-project.outputs.itemId }}
32 | field-keys: Status
33 | field-values: ${{ env.COLUMN }}
34 |
--------------------------------------------------------------------------------
/custom-button/app/extension/render/FeedbackButton.js:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | useTemplateEvaluation
4 | } from '@bpmn-io/form-js';
5 |
6 | import {
7 | html,
8 | useCallback
9 | } from 'diagram-js/lib/ui';
10 |
11 | import './styles.css';
12 |
13 | import FeedbackIcon from './feedback.svg';
14 |
15 | export const type = 'feedbackButton';
16 |
17 | export function FeedbackButtonRenderer(props) {
18 |
19 | const {
20 | disabled,
21 | readonly,
22 | field
23 | } = props;
24 |
25 | const {
26 | endpoint,
27 | message
28 | } = field;
29 |
30 | const evaluatedMessage = useTemplateEvaluation(message, { debug: true, strict: true });
31 |
32 | const onClick = useCallback(() => {
33 |
34 | if (disabled || readonly) {
35 | return;
36 | }
37 |
38 | // send the message to the configured endpoint
39 | alert(`Send message to ${ endpoint }:\n\n${ evaluatedMessage }`);
40 | }, [ disabled, readonly, endpoint, evaluatedMessage ]);
41 |
42 | return html`
43 | <${Button}
44 | {...props}
45 | field=${{
46 | ...field,
47 | acton: 'feedback'
48 | }}>${Button}>
49 |
`;
50 | }
51 |
52 | FeedbackButtonRenderer.config = {
53 | ...Button.config,
54 | type: type,
55 | label: 'Feedback',
56 | iconUrl: `data:image/svg+xml,${ encodeURIComponent(FeedbackIcon) }`,
57 | propertiesPanelEntries: [
58 | 'label'
59 | ]
60 | };
--------------------------------------------------------------------------------
/custom-button/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const {
4 | NormalModuleReplacementPlugin
5 | } = require('webpack');
6 |
7 | const CopyPlugin = require('copy-webpack-plugin');
8 |
9 | module.exports = {
10 | output: {
11 | path: path.join(__dirname, 'public'),
12 | filename: 'index.js',
13 | },
14 | mode: 'development',
15 | entry: './app/index.js',
16 | devtool: 'source-map',
17 | module: {
18 | rules: [
19 | {
20 | test: /\.css$/,
21 | use: [ 'style-loader', 'css-loader' ]
22 | },
23 | {
24 | test: /\.svg$/,
25 | use: 'raw-loader'
26 | }
27 | ]
28 | },
29 | plugins: [
30 | new CopyPlugin({
31 | patterns: [
32 | { from: 'app/index.html', to: '.' }
33 | ]
34 | }),
35 | new NormalModuleReplacementPlugin(
36 | /^(..\/preact|preact)(\/[^/]+)?$/,
37 | function(resource) {
38 |
39 | const replMap = {
40 | 'preact/hooks': path.resolve('node_modules/preact/hooks/dist/hooks.module.js'),
41 | 'preact/jsx-runtime': path.resolve('node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js'),
42 | 'preact': path.resolve('node_modules/preact/dist/preact.module.js'),
43 | '../preact/hooks': path.resolve('node_modules/preact/hooks/dist/hooks.module.js'),
44 | '../preact/jsx-runtime': path.resolve('node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js'),
45 | '../preact': path.resolve('node_modules/preact/dist/preact.module.js')
46 | };
47 |
48 | const replacement = replMap[resource.request];
49 |
50 | if (!replacement) {
51 | return;
52 | }
53 |
54 | resource.request = replacement;
55 | }
56 | )
57 | ]
58 | };
--------------------------------------------------------------------------------
/custom-components/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const {
4 | NormalModuleReplacementPlugin
5 | } = require('webpack');
6 |
7 | const CopyPlugin = require('copy-webpack-plugin');
8 |
9 | module.exports = {
10 | output: {
11 | path: path.join(__dirname, 'public'),
12 | filename: 'index.js',
13 | },
14 | mode: 'development',
15 | entry: './app/index.js',
16 | devtool: 'source-map',
17 | module: {
18 | rules: [
19 | {
20 | test: /\.css$/,
21 | use: [ 'style-loader', 'css-loader' ]
22 | },
23 | {
24 | test: /\.svg$/,
25 | use: 'raw-loader'
26 | }
27 | ]
28 | },
29 | plugins: [
30 | new CopyPlugin({
31 | patterns: [
32 | { from: 'app/index.html', to: '.' }
33 | ]
34 | }),
35 | new NormalModuleReplacementPlugin(
36 | /^(..\/preact|preact)(\/[^/]+)?$/,
37 | function(resource) {
38 |
39 | const replMap = {
40 | 'preact/hooks': path.resolve('node_modules/preact/hooks/dist/hooks.module.js'),
41 | 'preact/jsx-runtime': path.resolve('node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js'),
42 | 'preact': path.resolve('node_modules/preact/dist/preact.module.js'),
43 | '../preact/hooks': path.resolve('node_modules/preact/hooks/dist/hooks.module.js'),
44 | '../preact/jsx-runtime': path.resolve('node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js'),
45 | '../preact': path.resolve('node_modules/preact/dist/preact.module.js')
46 | };
47 |
48 | const replacement = replMap[resource.request];
49 |
50 | if (!replacement) {
51 | return;
52 | }
53 |
54 | resource.request = replacement;
55 | }
56 | )
57 | ]
58 | };
--------------------------------------------------------------------------------
/custom-properties/README.md:
--------------------------------------------------------------------------------
1 | # form-js custom properties
2 |
3 | An example that showcases how to use custom properties in [form-js](https://github.com/bpmn-io/form-js) to customize the Viewer.
4 |
5 | ## About
6 |
7 | This example shows how to define and use custom properties to populate a select or radio field, by relying on an external API.
8 |
9 | ## Usage summary
10 |
11 | ### Define custom property
12 |
13 | When defining the schema, use a custom property (`externalData`) to define the API endpoint to be used in retrieving the data.
14 |
15 | ```json
16 | {
17 | ...
18 | "components": [
19 | {
20 | "label": "Field label",
21 | "type": "select",
22 | "id": "Field_1",
23 | "key": "fielKey",
24 | "values": [],
25 | "properties": {
26 | "externalData": "https://..."
27 | }
28 | }
29 | ]
30 | ...
31 | }
32 | ```
33 |
34 | ### Use custom property to populate field
35 |
36 | After loading the form, you can check which fields require external data and use the API endpoint defined for each to retrieve it. Then, the field's options can be updated in the schema.
37 |
38 | ```javascript
39 | if (type === "select" && properties && properties.externalData) {
40 |
41 | // use "externalData" to get API endpoint
42 | const endpoint = properties.externalData;
43 | const res = await fetch(endpoint, "GET");
44 |
45 | // map API response to field values
46 | field.values = res.map(option => ({
47 | value: option.id,
48 | label: option.name
49 | }));
50 | }
51 | ```
52 |
53 | After the schema is updated accordingly, you may re-import the schema to re-render.
54 |
55 | ```javascript
56 | await form.importSchema(schema);
57 | ```
58 |
59 | ## Run this Example
60 | Download the example and open it in a web browser.
61 |
--------------------------------------------------------------------------------
/custom-properties/customProperties.js:
--------------------------------------------------------------------------------
1 | // (1) load form
2 | const schema = JSON.parse(
3 | document.querySelector('[type="application/form-schema"]').textContent
4 | );
5 |
6 | const container = document.querySelector('#form');
7 |
8 | const loadForm = FormViewer.createForm({
9 | container,
10 | schema
11 | });
12 |
13 | // (2) populate form fields with external data
14 | loadForm.then(async (form) => {
15 |
16 | const {
17 | components
18 | } = schema;
19 |
20 | for (let i = 0; i < components.length; i++) {
21 |
22 | const field = components[i];
23 | const {
24 | properties,
25 | id,
26 | type
27 | } = field;
28 |
29 | if (type === "select" && hasExternalData(field)) {
30 |
31 | // (2.1) use custom property "externalData" to get external API endpoint
32 | const endpoint = properties.externalData;
33 | const res = await preloadData(endpoint, "GET");
34 |
35 | // (2.2) map request response to field values
36 | field.values = res.map(option => ({
37 | value: option.id,
38 | label: option.name
39 | }));
40 |
41 | }
42 |
43 | }
44 |
45 | // (3) re-render form with updated schema
46 | form.importSchema(schema);
47 |
48 | form.on('submit', (event) => {
49 | console.log(event.data, event.errors);
50 | alert("Form submitted!")
51 | });
52 | })
53 |
54 |
55 | // helpers
56 |
57 | function hasExternalData(field) {
58 | const {
59 | properties
60 | } = field;
61 | return properties && properties.externalData;
62 | }
63 |
64 | async function preloadData(url, method) {
65 | // mock api response
66 | return [{
67 | name: "John Smith",
68 | id: "johnSmith"
69 | },
70 | {
71 | name: "Jane Doe",
72 | id: "janeDoe"
73 | }
74 | ];
75 | }
--------------------------------------------------------------------------------
/starter/viewer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
12 |
13 |
14 |
15 |
16 |
17 |
21 |
37 |
38 |
41 |
42 |
43 |
55 |
56 |
--------------------------------------------------------------------------------
/custom-properties/viewer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
12 |
13 | Custom properties form-js example
14 |
15 |
16 |
17 |
18 |
19 |
23 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/starter/editor.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
20 |
21 |
22 |
23 |
24 |
25 |
29 |
45 |
46 |
49 |
50 |
51 |
63 |
64 |
--------------------------------------------------------------------------------
/custom-button/app/extension/propertiesPanel/CustomPropertiesProvider.js:
--------------------------------------------------------------------------------
1 | import { get } from 'min-dash';
2 |
3 | import { useVariables } from '@bpmn-io/form-js';
4 |
5 | import {
6 | FeelTemplatingEntry,
7 | isFeelEntryEdited,
8 | isTextFieldEntryEdited,
9 | TextFieldEntry
10 | } from '@bpmn-io/properties-panel';
11 |
12 |
13 | export class CustomPropertiesProvider {
14 | constructor(propertiesPanel) {
15 | propertiesPanel.registerProvider(this, 500);
16 | }
17 |
18 | /**
19 | * @param {any} field
20 | * @param {function} editField
21 | *
22 | * @return {(Object[]) => (Object[])} groups middleware
23 | */
24 | getGroups(field, editField) {
25 |
26 | /**
27 | * @param {Object[]} groups
28 | *
29 | * @return {Object[]} modified groups
30 | */
31 | return (groups) => {
32 |
33 | if (field.type !== 'feedbackButton') {
34 | return groups;
35 | }
36 |
37 | const generalIdx = findGroupIdx(groups, 'general');
38 |
39 | /* insert group after general */
40 | groups.splice(generalIdx + 1, 0, {
41 | id: 'feedback',
42 | label: 'Feedback',
43 | entries: Entries(field, editField)
44 | });
45 |
46 | return groups;
47 | };
48 | }
49 | }
50 |
51 | CustomPropertiesProvider.$inject = [ 'propertiesPanel' ];
52 |
53 | function Entries(field, editField) {
54 |
55 | const onChange = (key) => {
56 | return (value) => {
57 | editField(field, [ key ], value);
58 | };
59 | };
60 |
61 | const getValue = (key) => {
62 | return () => {
63 | return get(field, [ key ]);
64 | };
65 | };
66 |
67 | return [
68 |
69 | {
70 | id: 'endpoint',
71 | component: Endpoint,
72 | getValue,
73 | field,
74 | isEdited: isTextFieldEntryEdited,
75 | onChange
76 | },
77 | {
78 | id: 'message',
79 | component: Message,
80 | getValue,
81 | field,
82 | isEdited: isFeelEntryEdited,
83 | onChange
84 | }
85 | ];
86 |
87 | }
88 |
89 | function Endpoint(props) {
90 | const {
91 | field,
92 | getValue,
93 | id,
94 | onChange
95 | } = props;
96 |
97 | const debounce = (fn) => fn;
98 |
99 | return TextFieldEntry({
100 | debounce,
101 | element: field,
102 | getValue: getValue('endpoint'),
103 | id,
104 | label: 'Endpoint',
105 | setValue: onChange('endpoint')
106 | });
107 | }
108 |
109 | function Message(props) {
110 | const {
111 | field,
112 | getValue,
113 | id,
114 | onChange
115 | } = props;
116 |
117 | const debounce = (fn) => fn;
118 |
119 | const variables = useVariables().map(name => ({ name }));
120 |
121 | return FeelTemplatingEntry({
122 | debounce,
123 | element: field,
124 | getValue: getValue('message'),
125 | id,
126 | label: 'Message',
127 | setValue: onChange('message'),
128 | variables
129 | });
130 | }
131 |
132 | // helper //////////////////////
133 |
134 | function findGroupIdx(groups, id) {
135 | return groups.findIndex(g => g.id === id);
136 | }
--------------------------------------------------------------------------------
/custom-components/app/extension/render/Range.js:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 |
3 | /*
4 | * Import components and utilities from our extension API. Warning: for demo experiments only.
5 | */
6 | import {
7 | Errors,
8 | FormContext,
9 | Numberfield,
10 | Description,
11 | Label
12 | } from '@bpmn-io/form-js';
13 |
14 | import {
15 | html,
16 | useContext
17 | } from 'diagram-js/lib/ui';
18 |
19 | import './styles.css';
20 |
21 | import RangeIcon from './range.svg';
22 |
23 | export const rangeType = 'range';
24 |
25 | /*
26 | * This is the rendering part of the custom field. We use `htm` to
27 | * to render our components without the need of extra JSX transpilation.
28 | */
29 | export function RangeRenderer(props) {
30 |
31 | const {
32 | disabled,
33 | errors = [],
34 | field,
35 | readonly,
36 | value
37 | } = props;
38 |
39 | const {
40 | description,
41 | range = {},
42 | id,
43 | label
44 | } = field;
45 |
46 | const {
47 | min,
48 | max,
49 | step
50 | } = range;
51 |
52 | const { formId } = useContext(FormContext);
53 |
54 | const errorMessageId = errors.length === 0 ? undefined : `${prefixId(id, formId)}-error-message`;
55 |
56 | const onChange = ({ target }) => {
57 | props.onChange({
58 | field,
59 | value: Number(target.value)
60 | });
61 | };
62 |
63 | return html`
64 | <${Label}
65 | id=${ prefixId(id, formId) }
66 | label=${ label } />
67 |
68 |
78 |
${ value }
79 |
80 | <${Description} description=${ description } />
81 | <${Errors} errors=${ errors } id=${ errorMessageId } />
82 |
`;
83 | }
84 |
85 | /*
86 | * This is the configuration part of the custom field. It defines
87 | * the schema type, UI label and icon, palette group, properties panel entries
88 | * and much more.
89 | */
90 | RangeRenderer.config = {
91 |
92 | /* we can extend the default configuration of existing fields */
93 | ...Numberfield.config,
94 | type: rangeType,
95 | label: 'Range',
96 | iconUrl: `data:image/svg+xml,${ encodeURIComponent(RangeIcon) }`,
97 | propertiesPanelEntries: [
98 | 'key',
99 | 'label',
100 | 'description',
101 | 'min',
102 | 'max',
103 | 'disabled',
104 | 'readonly'
105 | ]
106 | };
107 |
108 | // helper //////////////////////
109 |
110 | function formFieldClasses(type, { errors = [], disabled = false, readonly = false } = {}) {
111 | if (!type) {
112 | throw new Error('type required');
113 | }
114 |
115 | return classNames('fjs-form-field', `fjs-form-field-${type}`, {
116 | 'fjs-has-errors': errors.length > 0,
117 | 'fjs-disabled': disabled,
118 | 'fjs-readonly': readonly
119 | });
120 | }
121 |
122 | function prefixId(id, formId) {
123 | if (formId) {
124 | return `fjs-form-${ formId }-${ id }`;
125 | }
126 |
127 | return `fjs-form-${ id }`;
128 | }
--------------------------------------------------------------------------------
/custom-components/app/extension/propertiesPanel/CustomPropertiesProvider.js:
--------------------------------------------------------------------------------
1 | import { get, set } from 'min-dash';
2 |
3 | /*
4 | * Import components and utilities from our extension API. Warning: for demo experiments only.
5 | */
6 | import {
7 | NumberFieldEntry,
8 | isNumberFieldEntryEdited
9 | } from '@bpmn-io/properties-panel';
10 |
11 | /*
12 | * This is a custom properties provider for the properties panel.
13 | * It adds a new group `Range` with range specific properties.
14 | */
15 | export class CustomPropertiesProvider {
16 | constructor(propertiesPanel) {
17 | propertiesPanel.registerProvider(this, 500);
18 | }
19 |
20 | /**
21 | * Return the groups provided for the given field.
22 | *
23 | * @param {any} field
24 | * @param {function} editField
25 | *
26 | * @return {(Object[]) => (Object[])} groups middleware
27 | */
28 | getGroups(field, editField) {
29 |
30 | /**
31 | * We return a middleware that modifies
32 | * the existing groups.
33 | *
34 | * @param {Object[]} groups
35 | *
36 | * @return {Object[]} modified groups
37 | */
38 | return (groups) => {
39 |
40 | if (field.type !== 'range') {
41 | return groups;
42 | }
43 |
44 | const generalIdx = findGroupIdx(groups, 'general');
45 |
46 | /* insert range group after general */
47 | groups.splice(generalIdx + 1, 0, {
48 | id: 'range',
49 | label: 'Range',
50 | entries: RangeEntries(field, editField)
51 | });
52 |
53 | return groups;
54 | };
55 | }
56 | }
57 |
58 | CustomPropertiesProvider.$inject = [ 'propertiesPanel' ];
59 |
60 | /*
61 | * collect range entries for our custom group
62 | */
63 | function RangeEntries(field, editField) {
64 |
65 | const onChange = (key) => {
66 | return (value) => {
67 | const range = get(field, [ 'range' ], {});
68 |
69 | editField(field, [ 'range' ], set(range, [ key ], value));
70 | };
71 | };
72 |
73 | const getValue = (key) => {
74 | return () => {
75 | return get(field, [ 'range', key ]);
76 | };
77 | };
78 |
79 | return [
80 |
81 | {
82 | id: 'range-min',
83 | component: Min,
84 | getValue,
85 | field,
86 | isEdited: isNumberFieldEntryEdited,
87 | onChange
88 | },
89 | {
90 | id: 'range-max',
91 | component: Max,
92 | getValue,
93 | field,
94 | isEdited: isNumberFieldEntryEdited,
95 | onChange
96 | },
97 | {
98 | id: 'range-step',
99 | component: Step,
100 | getValue,
101 | field,
102 | isEdited: isNumberFieldEntryEdited,
103 | onChange
104 | }
105 | ];
106 |
107 | }
108 |
109 | function Min(props) {
110 | const {
111 | field,
112 | getValue,
113 | id,
114 | onChange
115 | } = props;
116 |
117 | const debounce = (fn) => fn;
118 |
119 | return NumberFieldEntry({
120 | debounce,
121 | element: field,
122 | getValue: getValue('min'),
123 | id,
124 | label: 'Minimum',
125 | setValue: onChange('min')
126 | });
127 | }
128 |
129 | function Max(props) {
130 | const {
131 | field,
132 | getValue,
133 | id,
134 | onChange
135 | } = props;
136 |
137 | const debounce = (fn) => fn;
138 |
139 | return NumberFieldEntry({
140 | debounce,
141 | element: field,
142 | getValue: getValue('max'),
143 | id,
144 | label: 'Maximum',
145 | setValue: onChange('max')
146 | });
147 | }
148 |
149 | function Step(props) {
150 | const {
151 | field,
152 | getValue,
153 | id,
154 | onChange
155 | } = props;
156 |
157 | const debounce = (fn) => fn;
158 |
159 | return NumberFieldEntry({
160 | debounce,
161 | element: field,
162 | getValue: getValue('step'),
163 | id,
164 | min: 0,
165 | label: 'Step',
166 | setValue: onChange('step')
167 | });
168 | }
169 |
170 | // helper //////////////////////
171 |
172 | function findGroupIdx(groups, id) {
173 | return groups.findIndex(g => g.id === id);
174 | }
--------------------------------------------------------------------------------
/custom-components/README.md:
--------------------------------------------------------------------------------
1 | # form-js Custom Components Example
2 |
3 | This example uses [form-js](https://github.com/bpmn-io/form-js) to implement custom form components.
4 |
5 | 
6 |
7 | ## About
8 |
9 | In this example we extend form-js with a custom component that allows users to select a number from a range. To achieve that we will walk through the following steps:
10 |
11 | * Add a custom form component renderer
12 | * Add custom styles for the range component
13 | * Add custom properties panel entries to specify the min, max and step of the range
14 |
15 | An example schema of the range component looks like this:
16 |
17 | ```json
18 | {
19 | "type": "range",
20 | "label": "Range",
21 | "min": 0,
22 | "max": 100,
23 | "step": 1
24 | }
25 | ```
26 |
27 | ### Add a custom form component renderer
28 |
29 | The first step is to add a custom form component renderer.
30 |
31 | The renderer is responsible for rendering the component in the form editor and the form preview. It also handles the interaction with the component, e.g. when the value changes or validation.
32 |
33 | We create the [`RangeRenderer`](./app/extension/render/Range.js) which defines a couple of things
34 |
35 | * a [preact](https://preactjs.com/) component that renders the component in the form editor and preview by re-using existing components like `Label`, `Errors` and `Description`
36 |
37 | ```js
38 | import {
39 | Errors,
40 | FormContext,
41 | Description,
42 | Label
43 | } from '@bpmn-io/form-js';
44 |
45 | import {
46 | html,
47 | useContext
48 | } from 'diagram-js/lib/ui';
49 |
50 | export function RangeRenderer(props) {
51 |
52 | const {
53 | disabled,
54 | errors = [],
55 | field,
56 | readonly,
57 | value
58 | } = props;
59 |
60 | const {
61 | description,
62 | range = {},
63 | id,
64 | label
65 | } = field;
66 |
67 | const {
68 | min,
69 | max,
70 | step
71 | } = range;
72 |
73 | const { formId } = useContext(FormContext);
74 |
75 | const errorMessageId = errors.length === 0 ? undefined : `${prefixId(id, formId)}-error-message`;
76 |
77 | const onChange = ({ target }) => {
78 | props.onChange({
79 | field,
80 | value: Number(target.value)
81 | });
82 | };
83 |
84 | return html`
85 | <${Label}
86 | id=${ prefixId(id, formId) }
87 | label=${ label } />
88 |
89 |
99 |
${ value }
100 |
101 | <${Description} description=${ description } />
102 | <${Errors} errors=${ errors } id=${ errorMessageId } />
103 |
`;
104 | }
105 | ```
106 |
107 | * a component `config` that extends the base `Numberfield` configuration and adds customizations as the icon, a custom label and the default properties panel entries to show
108 |
109 | ```js
110 | import { Numberfield } from '@bpmn-io/form-js';
111 |
112 | RangeRenderer.config = {
113 | ...Numberfield.config,
114 | type: rangeType,
115 | label: 'Range',
116 | iconUrl: `data:image/svg+xml,${ encodeURIComponent(RangeIcon) }`,
117 | propertiesPanelEntries: [
118 | 'key',
119 | 'label',
120 | 'description',
121 | 'min',
122 | 'max',
123 | 'disabled',
124 | 'readonly'
125 | ]
126 | };
127 | ```
128 |
129 | ### Register the custom renderer
130 |
131 | We use the `formFields` service to register our custom renderer for the `range` type.
132 |
133 | ```js
134 | class CustomFormFields {
135 | constructor(formFields) {
136 | formFields.register('range', RangeRenderer);
137 | }
138 | }
139 |
140 |
141 | export default {
142 | __init__: [ 'rangeField' ],
143 | rangeField: [ 'type', CustomFormFields ]
144 | };
145 | ```
146 |
147 | ### Add custom styles
148 |
149 | We define custom styles for the range component by adding a simple CSS file [`styles.css`](./app/extension/render/styles.css). For the example we import the styles directly to the component as we have a bundler ([webpack](https://webpack.js.org/)) in place that adds the styles to the application.
150 |
151 | ```css
152 | .range-group {
153 | width: 100%;
154 | display: flex;
155 | flex-direction: row;
156 | }
157 |
158 | .range-group input {
159 | width: 100%;
160 | }
161 | .range-group .range-value {
162 | margin-left: 4px;
163 | }
164 | ```
165 |
166 | ### Add custom properties panel entries
167 |
168 | With `config.propertiesPanelEntries` we define the default properties panel entries to show for the component. We can also add custom entries to the properties panel.
169 |
170 | We add a [`CustomPropertiesProvider`](./app/extension/properties-panel/CustomPropertiesProvider.js) that allows users to specify the min, max and step of the range component. We place the group right after the general group.
171 |
172 | ```js
173 | export class CustomPropertiesProvider {
174 | constructor(propertiesPanel) {
175 | propertiesPanel.registerProvider(this, 500);
176 | }
177 |
178 | getGroups(field, editField) {
179 |
180 | ...
181 | return (groups) => {
182 |
183 | if (field.type !== 'range') {
184 | return groups;
185 | }
186 |
187 | const generalIdx = findGroupIdx(groups, 'general');
188 |
189 | groups.splice(generalIdx + 1, 0, {
190 | id: 'range',
191 | label: 'Range',
192 | entries: RangeEntries(field, editField)
193 | });
194 |
195 | return groups;
196 | };
197 | }
198 | }
199 | ```
200 |
201 | The [`RangeEntries`](./app/extension/properties-panel/CustomPropertiesProvider.js) function returns the entries to show for the range component. Check out the full provider to gather more insights.
202 |
203 | ```js
204 | function RangeEntries(field, editField) {
205 |
206 | const onChange = (key) => {
207 | return (value) => {
208 | const range = get(field, [ 'range' ], {});
209 |
210 | editField(field, [ 'range' ], set(range, [ key ], value));
211 | };
212 | };
213 |
214 | const getValue = (key) => {
215 | return () => {
216 | return get(field, [ 'range', key ]);
217 | };
218 | };
219 |
220 | return [
221 |
222 | {
223 | id: 'range-min',
224 | component: Min,
225 | getValue,
226 | field,
227 | isEdited: isNumberFieldEntryEdited,
228 | onChange
229 | },
230 | {
231 | id: 'range-max',
232 | component: Max,
233 | getValue,
234 | field,
235 | isEdited: isNumberFieldEntryEdited,
236 | onChange
237 | },
238 | {
239 | id: 'range-step',
240 | component: Step,
241 | getValue,
242 | field,
243 | isEdited: isNumberFieldEntryEdited,
244 | onChange
245 | }
246 | ];
247 | }
248 | ```
249 |
250 | ### Plugging Everything together
251 |
252 | To embed the customizations into the form-js we need to plug everything together. We do that by including the custom renderer into both editor and preview via `additionalModules` and registering the custom properties provider to the editor via `editorAdditionalModules`.
253 |
254 | ```js
255 | import { FormPlayground } from '@bpmn-io/form-js';
256 |
257 | import RenderExtension from './extension/render';
258 | import PropertiesPanelExtension from './extension/propertiesPanel';
259 |
260 | import '@bpmn-io/form-js/dist/assets/form-js.css';
261 | import '@bpmn-io/form-js/dist/assets/form-js-editor.css';
262 | import '@bpmn-io/form-js/dist/assets/form-js-playground.css';
263 |
264 | new FormPlayground({
265 | container,
266 | schema,
267 | data,
268 | additionalModules: [
269 | RenderExtension
270 | ],
271 | editorAdditionalModules: [
272 | PropertiesPanelExtension
273 | ]
274 | });
275 | ```
276 |
277 | ## Building
278 |
279 | You need a [NodeJS](http://nodejs.org) development stack with [npm](https://npmjs.org) installed to build the project.
280 |
281 | To install all project dependencies execute
282 |
283 | ```
284 | npm install
285 | ```
286 |
287 | Spin up a development setup by executing
288 |
289 | ```
290 | npm run dev
291 | ```
--------------------------------------------------------------------------------