<bmx-decision-table>
Business rules as a DMN decision table: edit the rules in a grid, try inputs and see which rule fired and why, check for gaps and overlaps, keep test cases beside the rules, and move tables in and out as DMN 1.3 XML.
5 properties · 2 events · 11 methods · 5 parts
Example
Pick values under “Try it” to see which rule fires and why. “Check” finds the case no rule covers.
The table as DMN 1.3 XML
Show markup
<div class="row" style="display: block">
<bmx-decision-table id="ex-dt" style="max-inline-size: 72rem"></bmx-decision-table>
</div>
<p class="note" id="ex-dt-out" role="status">Pick values under “Try it” to see which rule fires and why. “Check” finds the case no rule covers.</p>
<details style="margin-block-start: 12px">
<summary>The table as DMN 1.3 XML</summary>
<pre id="ex-dt-dmn" style="max-block-size: 20rem; overflow: auto; font-size: 12px"></pre>
</details>
<script type="module">
await customElements.whenDefined('bmx-decision-table');
const table = document.getElementById('ex-dt');
const out = document.getElementById('ex-dt-out');
const dmn = document.getElementById('ex-dt-dmn');
table.table = {
name: 'Loan pre-approval',
hitPolicy: 'unique',
inputs: [
{ key: 'score', label: 'Credit score', type: 'integer', description: 'From the credit bureau, 300 to 850.' },
{ key: 'employment', label: 'Employment', values: ['Employed', 'Self-employed', 'Retired', 'Unemployed'] },
{ key: 'ratio', label: 'Loan to income', type: 'number', expression: '=amount / income', description: 'The amount asked for over the yearly income.' },
],
outputs: [
{ key: 'decision', label: 'Decision', values: ['Decline', 'Refer', 'Approve'], default: 'Refer' },
{ key: 'rate', label: 'Rate', type: 'number' },
],
rules: [
{ inputs: ['< 580', '-', '-'], outputs: ['Decline', ''], note: 'Below the lending floor' },
{ inputs: ['[580..670)', '"Employed", "Self-employed"', '<= 3'], outputs: ['Refer', '0.089'] },
{ inputs: ['[580..670)', '"Employed", "Self-employed"', '> 3'], outputs: ['Decline', ''] },
{ inputs: ['>= 670', '"Employed"', '<= 4'], outputs: ['Approve', '=IF(score >= 760, 0.049, 0.059)'], note: 'Best rate for 760 and up' },
{ inputs: ['>= 670', '"Self-employed"', '<= 3'], outputs: ['Approve', '0.065'] },
{ inputs: ['>= 670', '"Employed", "Self-employed"', '> 4'], outputs: ['Refer', '0.072'] },
{ inputs: ['>= 670', '"Self-employed"', '(3..4]'], outputs: ['Refer', '0.069'] },
{ inputs: ['>= 580', '"Retired"', '<= 2'], outputs: ['Approve', '0.061'] },
{ inputs: ['>= 580', '"Unemployed"', '-'], outputs: ['Decline', ''] },
],
tests: [
{ name: 'Prime borrower', inputs: { score: 790, employment: 'Employed', amount: 90000, income: 60000 }, expect: { decision: 'Approve', rate: 0.049 } },
{ name: 'Thin margin', inputs: { score: 600, employment: 'Self-employed', amount: 200000, income: 50000 }, expect: { decision: 'Decline' } },
{ name: 'Retired, small loan', inputs: { score: 700, employment: 'Retired', amount: 30000, income: 25000 }, expect: { decision: 'Approve', rate: 0.061 } },
],
};
await table.tryValuesOf({ score: 720, employment: 'Employed', amount: 120000, income: 45000 });
const showDmn = async () => (dmn.textContent = await table.exportDmn());
showDmn();
table.addEventListener('bmxDecisionChange', showDmn);
table.addEventListener('bmxDecisionEvaluate', e => {
const { result } = e.detail;
out.textContent = result.ok ? `Answer: ${JSON.stringify(result.output)} (rule ${result.fired.map(r => r + 1).join(', ') || 'none'})` : result.error;
});
</script>
<bmx-decision-table id="rules"></bmx-decision-table>
<script>
rules.table = {
name: 'Discount',
hitPolicy: 'unique',
inputs: [
{ key: 'tier', label: 'Customer tier', values: ['Gold', 'Silver', 'Bronze'] },
{ key: 'amount', label: 'Order amount', type: 'number' },
],
outputs: [{ key: 'discount', label: 'Discount', type: 'number', default: 0 }],
rules: [
{ inputs: ['"Gold"', '>= 1000'], outputs: ['0.15'] },
{ inputs: ['"Gold"', '< 1000'], outputs: ['0.1'] },
],
};
const { output } = await rules.evaluate({ tier: 'Gold', amount: 1200 });
</script>
Input cells are tests in the syntax of DMN's simple unary tests - < 18,
[18..65), "UK", "IE", not("UK"), - - or BMX formulas (=...) with
the inputs as names. Output cells are values or formulas. All seven DMN hit
policies, with collect's sum, min, max and count. Nothing runs eval.
The element also works as an evaluator out of sight: hidden, with
evaluate and evaluateMany. With the source, the same evaluator runs
without it (evaluateDecision in core/decision-model), on a server or in
a worker.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
label |
label |
string |
— | The table's accessible name. Default: its name, or "Decision table". |
panel |
panel |
BmxDecisionTablePanel |
'try' |
The panel shown under the grid, or none to hide the panels. |
readonly |
readonly |
boolean |
false |
Shows the rules without letting them be changed. Try it, Check and Tests still work. |
strings |
strings |
Partial<BmxDecisionTableStrings> | string |
— | Words to show instead of the English ones, as an object or JSON. |
table |
table |
BmxDecisionTableData | string |
{ inputs: [], outputs: [], rules: [] } |
The decision table, as an object or JSON. |
Events
| Event | Detail | Description |
|---|---|---|
bmxDecisionChange |
BmxDecisionChangeDetail |
Fired after every change, with the whole table. |
bmxDecisionEvaluate |
BmxDecisionEvaluateDetail |
Fired when Try it works the table out. |
Methods
| Method | Signature | Description |
|---|---|---|
check |
check() => Promise<{ problems: BmxDecisionProblem[]; analysis: BmxDecisionAnalysis; }> |
Problems with the cells, and the gaps, overlaps and rules that never fire. |
evaluate |
evaluate(context: Record<string, unknown>, options?: BmxDecisionEvaluateOptions) => Promise<BmxDecisionResult> |
Works the table out for a context: values by input key, and whatever else formulas read. |
evaluateMany |
evaluateMany(contexts: Record<string, unknown>[]) => Promise<BmxDecisionResult[]> |
Works the table out for many contexts at once. |
explain |
explain(context: Record<string, unknown>) => Promise<string[]> |
Why the table answers what it does for a context, in sentences. |
exportDmn |
exportDmn() => Promise<string> |
The table as DMN 1.3 XML. |
getTable |
getTable() => Promise<BmxDecisionTableData> |
The table as it stands. |
importDmn |
importDmn(xml: string) => Promise<string[]> |
Reads a DMN file's first decision table into the element. Resolves with what could not be carried across. |
redo |
redo() => Promise<boolean> |
Redoes the last undone change. |
runTests |
runTests() => Promise<BmxDecisionTestResult[]> |
Runs the table's test cases. |
tryValuesOf |
tryValuesOf(context: Record<string, unknown>) => Promise<BmxDecisionResult> |
Fills Try it with values and shows the answer. |
undo |
undo() => Promise<boolean> |
Undoes the last change. Resolves whether there was one. |
CSS shadow parts
| Part | Description |
|---|---|
cell |
A rule's cell; also cell-problem. |
dialog |
The column editor. |
grid |
The rules grid. |
panels |
The Try it, Check and Tests area. |
toolbar |
The row of name, hit policy and actions. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-decision-table-cell-width |
The narrowest a rule cell is drawn. |
--bmx-decision-table-fired |
The colour that marks the rule that fired. |
--bmx-decision-table-input |
The tint of the input columns' headings. |
--bmx-decision-table-max-height |
The tallest the rules grid grows before it scrolls. |
--bmx-decision-table-output |
The tint of the output columns' headings. |
--bmx-decision-table-problem |
The colour that marks a problem. |