Skip to content

Commit

Permalink
wip feat: add custom component example
Browse files Browse the repository at this point in the history
Related to #6
  • Loading branch information
Niklas Kiefer committed Oct 10, 2023
1 parent 0180d56 commit d878c0e
Show file tree
Hide file tree
Showing 16 changed files with 9,472 additions and 0 deletions.
6 changes: 6 additions & 0 deletions custom-components/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": [
"plugin:bpmn-io/node",
"plugin:bpmn-io/browser"
]
}
25 changes: 25 additions & 0 deletions custom-components/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
public
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
28 changes: 28 additions & 0 deletions custom-components/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# form-js Custom Components Example

This example uses [form-js](https://github.com/bpmn-io/form-js) to implement custom form components.

## About

This example is a node-style web application.

Todo: screenshot

Todo: what's about


## Building

You need a [NodeJS](http://nodejs.org) development stack with [npm](https://npmjs.org) installed to build the project.

To install all project dependencies execute

```
npm install
```

Spin up a development setup by executing

```
npm run dev
```
4 changes: 4 additions & 0 deletions custom-components/app/empty.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "default",
"components": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { get, set } from 'min-dash';

/*
* Import components and utilities from our extension API. Warning: for demo experiments only.
*/
import {
NumberFieldEntry,
isNumberFieldEntryEdited
} from '@bpmn-io/properties-panel';

/*
* This is a custom properties provider for the properties panel.
* It adds a new group `Range` with range specific properties.
*/
export class CustomPropertiesProvider {
constructor(propertiesPanel) {
propertiesPanel.registerProvider(this, 500);
}

/**
* Return the groups provided for the given field.
*
* @param {any} field
* @param {function} editField
*
* @return {(Object[]) => (Object[])} groups middleware
*/
getGroups(field, editField) {

/**
* We return a middleware that modifies
* the existing groups.
*
* @param {Object[]} groups
*
* @return {Object[]} modified groups
*/
return (groups) => {

if (field.type !== 'range') {
return groups;
}

const generalIdx = findGroupIdx(groups, 'general');

/* insert range group after general */
groups.splice(generalIdx + 1, 0, {
id: 'range',
label: 'Range',
entries: RangeEntries(field, editField)
});

return groups;
};
}
}

CustomPropertiesProvider.$inject = [ 'propertiesPanel' ];

/*
* collect range entries for our custom group
*/
function RangeEntries(field, editField) {

const onChange = (key) => {
return (value) => {
const range = get(field, [ 'range' ], {});

editField(field, [ 'range' ], set(range, [ key ], value));
};
};

const getValue = (key) => {
return () => {
return get(field, [ 'range', key ]);
};
};

return [

{
id: 'range-min',
component: Min,
getValue,
field,
isEdited: isNumberFieldEntryEdited,
onChange
},
{
id: 'range-max',
component: Max,
getValue,
field,
isEdited: isNumberFieldEntryEdited,
onChange
},
{
id: 'range-step',
component: Step,
getValue,
field,
isEdited: isNumberFieldEntryEdited,
onChange
}
];

}

function Min(props) {
const {
field,
getValue,
id,
onChange
} = props;

const debounce = (fn) => fn;

return NumberFieldEntry({
debounce,
element: field,
getValue: getValue('min'),
id,
label: 'Minimum',
setValue: onChange('min')
});
}

function Max(props) {
const {
field,
getValue,
id,
onChange
} = props;

const debounce = (fn) => fn;

return NumberFieldEntry({
debounce,
element: field,
getValue: getValue('max'),
id,
label: 'Maximum',
setValue: onChange('max')
});
}

function Step(props) {
const {
field,
getValue,
id,
onChange
} = props;

const debounce = (fn) => fn;

return NumberFieldEntry({
debounce,
element: field,
getValue: getValue('step'),
id,
min: 0,
label: 'Step',
setValue: onChange('step')
});
}

// helper //////////////////////

function findGroupIdx(groups, id) {
return groups.findIndex(g => g.id === id);
}
6 changes: 6 additions & 0 deletions custom-components/app/extension/propertiesPanel/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { CustomPropertiesProvider } from './CustomPropertiesProvider';

export default {
__init__: [ 'rangePropertiesProvider' ],
rangePropertiesProvider: [ 'type', CustomPropertiesProvider ]
};
128 changes: 128 additions & 0 deletions custom-components/app/extension/render/Range.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import classNames from 'classnames';

/*
* Import components and utilities from our extension API. Warning: for demo experiments only.
*/
import {
Errors,
FormContext,
Numberfield,
Description,
Label
} from '@bpmn-io/form-js';

import {
html,
useContext
} from 'diagram-js/lib/ui';

import './styles.css';

import RangeIcon from './range.svg';

export const rangeType = 'range';

/*
* This is the rendering part of the custom field. We use `htm` to
* to render our components without the need of extra JSX transpilation.
*/
export function RangeRenderer(props) {

const {
disabled,
errors = [],
field,
readonly,
value
} = props;

const {
description,
range = {},
id,
label
} = field;

const {
min,
max,
step
} = range;

const { formId } = useContext(FormContext);

const errorMessageId = errors.length === 0 ? undefined : `${prefixId(id, formId)}-error-message`;

const onChange = ({ target }) => {
props.onChange({
field,
value: Number(target.value)
});
};

return html`<div class=${ formFieldClasses(rangeType) }>
<${Label}
id=${ prefixId(id, formId) }
label=${ label } />
<div class="range-group">
<input
type="range"
disabled=${ disabled }
id=${ prefixId(id, formId) }
max=${ max }
min=${ min }
onInput=${ onChange }
readonly=${ readonly }
value=${ value }
step=${ step } />
<div class="range-value">${ value }</div>
</div>
<${Description} description=${ description } />
<${Errors} errors=${ errors } id=${ errorMessageId } />
</div>`;
}

/*
* This is the configuration part of the custom field. It defines
* the schema type, UI label and icon, palette group, properties panel entries
* and much more.
*/
RangeRenderer.config = {

/* we can extend the default configuration of existing fields */
...Numberfield.config,
type: rangeType,
label: 'Range',
iconUrl: `data:image/svg+xml,${ encodeURIComponent(RangeIcon) }`,
propertiesPanelEntries: [
'key',
'label',
'description',
'min',
'max',
'disabled',
'readonly'
]
};

// helper //////////////////////

function formFieldClasses(type, { errors = [], disabled = false, readonly = false } = {}) {
if (!type) {
throw new Error('type required');
}

return classNames('fjs-form-field', `fjs-form-field-${type}`, {
'fjs-has-errors': errors.length > 0,
'fjs-disabled': disabled,
'fjs-readonly': readonly
});
}

function prefixId(id, formId) {
if (formId) {
return `fjs-form-${ formId }-${ id }`;
}

return `fjs-form-${ id }`;
}
Loading

0 comments on commit d878c0e

Please sign in to comment.