nrl-rule-builder
v0.1.0
Published
Natural-rule-language RuleBuilder React component. If/Then/Else + Set-to rule authoring UI with JSON output. Zero runtime dependencies — styled with a prebuilt stylesheet, no antd/tailwind required in the consumer.
Maintainers
Readme
nrl-rule-builder
Natural-rule-language RuleBuilder React component — an If / Then / Else + "Set to" rule-authoring UI that reads like a sentence and serializes to JSON.
Users compose rules like:
If
Amountis greater than1000andReferencestarts withINVthen set toApproved, else set toReview
…and you get back a machine-readable RuleJSON payload plus a human-readable description, live as they edit.
- Zero runtime dependencies. No antd, no dayjs, no uuid — selects, inputs, and date pickers are lightweight built-in components. Only
react/react-domare peer dependencies (>=17). - No state library required. Schemas and persistence are plain props/callbacks — no Redux, no context gymnastics.
- Self-contained styling. One prebuilt, minified stylesheet; the consumer does not need Tailwind.
- ESM + CJS + TypeScript. Ships
dist/index.js(ESM),dist/index.cjs(CJS), and full.d.tstypes. - Round-trippable. Saved JSON can be parsed back into the editor with
parseRuleFromJSONfor later editing.
Install
npm install nrl-rule-builderQuick start
'use client';
import { RuleBuilder, useRuleBuilder } from 'nrl-rule-builder';
import 'nrl-rule-builder/styles.css';
const leftSchema = {
schema_title: 'PPG',
columns: [
// datatype_uuid drives which columns appear in number/text field pickers
{ alias: 'Amount', uuid: 'col-amount', datatype_uuid: '...', isactive: true, parentuuid: null },
{ alias: 'Reference', uuid: 'col-ref', datatype_uuid: '...', isactive: true, parentuuid: null },
],
};
const rightSchema = { schema_title: 'Bank', columns: [/* … */] };
export function MyRulePanel({ selectedColumn }) {
const handlers = useRuleBuilder({
selectedSchema: selectedColumn, // .uuid becomes target_field on Set actions
leftSchema,
rightSchema,
// initialRuleJson: savedJson, // resume editing a previously saved rule
onSave: ({ json, description }) => {
// persist however you like (API call, redux dispatch, …)
console.log(description, json);
},
onCancel: () => {/* close the panel */},
onDelete: () => {/* soft-delete the rule */},
});
return (
<RuleBuilder
selected_column={selectedColumn}
props={handlers}
leftSchema={leftSchema}
rightSchema={rightSchema}
/>
);
}useRuleBuilder produces the exact props bundle the component consumes. Call handlers.handleSave() from your own Save button; handlers.generatedJson / handlers.generatedDescription update live as the user edits.
Next.js note: the components are client components (they ship
'use client'banners), so you can import them directly from a server component tree — but render them inside a client boundary as shown above. SSR-rendering the markup is supported (the smoke test SSR-renders the builder).
Concepts
A rule is a tree of a few node kinds:
| Node | Meaning |
| --- | --- |
| Primary rule (type: 'primary') | An If <conditions> Then <action> Else <action> block. The Else branch may itself be another primary rule, giving you else if chains of any depth. |
| Group (type: 'group') | The condition list of a primary rule — conditions joined by AND / OR operators (click the connector pill to toggle). |
| Condition (type: 'condition') | <left operand> <operator> <right operand>. |
| Set action (type: 'set') | "Set to <value>" — writes a value to the target column (target_field). A rule can also be just a Set action with no conditions. |
| Value (type: 'value') | A leaf operand: a field reference, literal text/number/boolean/date, an arithmetic expression, or a text-extraction expression. |
| Placeholder | The dashed "Select…" pill shown for any part the user hasn't filled in yet. |
When the builder mounts with no rule, the user first picks If (start a conditional rule) or Set (a plain assignment).
Condition operators
is equal to · is not equal to · is greater than · is less than · is greater than or equal to · is less than or equal to · is one of · is not one of · is between · contains · contains one of · does not contain · does not contain one of · starts with · starts with one of · does not start with · does not start with any of · ends with · ends with one of · does not end with · does not end with any of
Value types
Operands and Set values can be any of:
| Value type | Description |
| --- | --- |
| field | A column from the left/right schema. |
| text, number, boolean, date, date-range | Literals (date pickers are built in). |
| absolute | Absolute value of a numeric field. |
| first_embedded_number | First number embedded in a text field (e.g. INV-1042 → 1042). |
| field_as_number | Cast a text field to a number. |
| text_before_something / text_after_something | Substring before/after a delimiter. |
| text_between_something_and_something | Substring between two delimiters. |
| First/Last N characters, remove leading characters | Character-level text extraction. |
| something_plus_something, something_minus_something, something_into_something, something_by_something | Arithmetic (+, −, ×, ÷) between two operands — operands can themselves be fields, numbers, or nested expressions. |
Generated JSON
generateJSON(rule, selectedSchema) (called automatically by the hook) produces a nested RuleJSON:
{
"AND": [
{
"row_id": "3f6c…",
"operator": "is greater than",
"operands": {
"left_something": { "type": "FIELD", "FIELD": { "value": "col-amount" } },
"right_something": { "type": "NUMBER", "NUMBER": { "value": "1000" } }
}
},
{
"row_id": "9a1b…",
"operator": "starts with",
"operands": {
"left_something": { "type": "FIELD", "FIELD": { "value": "col-ref" } },
"right_something": { "type": "TEXT", "TEXT": { "value": "INV" } }
}
}
],
"set": { "target_field": "col-status", "type": "TEXT", "TEXT": { "value": "Approved" } },
"else": { "target_field": "col-status", "type": "TEXT", "TEXT": { "value": "Review" } }
}- Conditions nest right-associatively under
AND/ORkeys, mirroring the connector pills. - Each operand is
{ type: <TYPE>, <TYPE>: { value: … } }where<TYPE>is the uppercase mapping of the value type (FIELD,TEXT,NUMBER,DATE,SOMETHING_PLUS_SOMETHING, …). Arithmetic operands nest their own left/right operands recursively. setcarries the action;elseis present only when the Else branch is filled, and is itself a fullRuleJSONwhen the Else branch is another If-rule (else if).- A conditionless rule serializes to just
{ "set": { … } }.
The same shape feeds back in through initialRuleJson / parseRuleFromJSON to resume editing.
API reference
<RuleBuilder />
| Prop | Type | Description |
| --- | --- | --- |
| selected_column | { uuid: string } | Column the rule targets. Its uuid becomes target_field on Set actions. |
| props | RuleBuilderHandlers | State + handler bundle (from useRuleBuilder, or your own with the same shape). |
| leftSchema | Schema | Left-side schema backing the field dropdowns. |
| rightSchema | Schema | Right-side schema backing the field dropdowns. |
| dataTypeUuids | Partial<DataTypeUuids> | Optional override of the datatype-UUID catalog used to filter number/text columns (see below). |
| fieldSide | 'left' \| 'right' \| null | Optional: scope field pickers to one schema side (for filter-rule builders). |
Schema is { schema_title: string, columns: SchemaColumn[] } and SchemaColumn is { alias, uuid, datatype_uuid?, isactive?, parentuuid? }. Only columns with isactive: true (and parentuuid: null for the general field picker) are offered.
useRuleBuilder(options): RuleBuilderHandlers
| Option | Type | Description |
| --- | --- | --- |
| selectedSchema | { uuid: string } | Column the rule writes to; .uuid becomes target_field. |
| leftSchema / rightSchema | Schema | Schemas used for JSON parsing and description generation. |
| initialRule | Rule | Start from an existing in-memory rule tree… |
| initialRuleJson | RuleJSON | …or from previously saved JSON (parsed via parseRuleFromJSON). |
| onSave | ({ json, description }) => void | Called by handleSave with the generated JSON + description. |
| onCancel | () => void | Called by handleCancel after the rule resets to its initial state. |
| onDelete | () => void | Called by handleDelete. |
Returned RuleBuilderHandlers:
| Key | Description |
| --- | --- |
| rule / setRule | Current Rule tree (null shows the initial "If / Set" selector). |
| handleReplace(path, newNode) | Immutably replaces a node in the rule tree (keeps group operators in sync). |
| handleInitialSelection(value, selected_column) | Creates the initial rule from the 'if' / 'set' choice. |
| handleSave / handleCancel / handleDelete | Wired to your onSave / onCancel / onDelete callbacks. |
| generatedJson, generatedDescription | Live serialized JSON + human-readable sentence. |
Providers
If you render several builders, wrap once instead of passing schemas to each:
import { RuleBuilderProvider } from 'nrl-rule-builder';
<RuleBuilderProvider leftSchema={leftSchema} rightSchema={rightSchema} dataTypeUuids={overrides}>
<RuleBuilder selected_column={col} props={handlers} />
</RuleBuilderProvider>FilterFieldSideProvider (with useFilterFieldSide) scopes nested field pickers to one schema side — the provider equivalent of the fieldSide prop.
Datatype catalog
Field pickers filter columns into "number fields" / "text fields" lists by datatype_uuid. The defaults (DEFAULT_DATA_TYPE_UUIDS) match the original host platform's catalog — pass your own IDs via the dataTypeUuids prop on RuleBuilder or RuleBuilderProvider:
const overrides = { AMOUNT: 'your-amount-uuid', TEXT_50: 'your-text-uuid', /* … */ };Keys: DATE, DATETIME, TEXT_200, TEXT_50, CURRENCY, NUMBER_INT, NUMBER_DECIMAL, AMOUNT, BOOLEAN.
Utilities
All pure rule-JSON helpers are exported:
| Function | Description |
| --- | --- |
| generateJSON(rule, selectedSchema) | Serialize a Rule tree to RuleJSON. |
| parseRuleFromJSON(json, leftSchema?, rightSchema?) | Inverse of generateJSON — rebuild an editable Rule tree from saved JSON. |
| generateRuleDescription(json, leftSchema?, rightSchema?) | Human-readable sentence for a RuleJSON (field UUIDs resolved to aliases). |
| buildRuleOutline(json, leftSchema?, rightSchema?) | Indented outline (RuleOutlineLine[]) for previews/summaries. |
| findEmptyFields(json) | Validation — lists unfilled parts of a rule (useful to gate Save). |
| toggleImmediateOperator(rule, path) | Flip an AND ↔ OR connector at a path. |
| generateOperand / generateAction / generateRowId | Lower-level serialization helpers. |
Constants: CASE_TYPE (per-value-type pill color catalog), TEXT_BETWEEN_SEPARATOR, DEFAULT_DATA_TYPE_UUIDS.
All TypeScript types are exported too: Rule, PrimaryRule, SetAction, Condition, Group, Value, Placeholder, RuleJSON, ConditionJSON, OperandJSON, ActionJSON, Schema, SchemaColumn, SelectedColumn, DataTypeUuids, RuleBuilderProps, RuleBuilderHandlers, UseRuleBuilderOptions, …
UI primitives
The built-in controls are exported for consumers who want matching UI around the builder: Select (with option groups), Input, Button, DatePicker, DateRangePicker, Pill, InlineEditable, plus EditModeProvider / useEditMode.
Styling
Import the prebuilt stylesheet once, anywhere in your app:
import 'nrl-rule-builder/styles.css';It is generated from Tailwind at build time and minified, but your app does not need Tailwind — it's plain CSS. The package marks CSS as sideEffects, so bundlers won't tree-shake the import away.
Development
npm install
npm run dev # local playground at http://localhost:4321 (live reload)
npm run typecheck # tsc --noEmit
npm run build # tsup (ESM + CJS + d.ts) + tailwind → dist/styles.css
npm run smoke # SSR render + JSON round-trip checks against dist/npm run dev serves demo/ — a playground wired to sample schemas with live panels for the generated JSON, description, and validation issues. It imports straight from src/, so component edits reload instantly (set PORT=xxxx to change the port).
Publishing runs the full pipeline automatically: npm publish triggers prepublishOnly → typecheck + build + smoke.
