Skip to content

Filter builder โ€‹

A universal, inline OData filter builder. controlMonoFilterBuilder owns the filter; <mono-filter-builder :control-filter-builder.prop="โ€ฆ"> renders it as nested rule rows and writes edits straight back.

Control โ€‹

controlMonoFilterBuilder owns the filter tree; bind it with :control-filter-builder and read it back as an OData $filter string whenever it notifies.

It also owns the element's own props. Declare them in the controller's props block and <mono-filter-builder> needs nothing but the controller binding โ€” the same arrangement as controlMonoTable({ props }), minus the per-element nesting, since this family has a single element.

ts
const filter = controlMonoFilterBuilder({
  fields,
  props: { size: 'sm', width: '100%', maxHeight: 260 },
})

filter.props()                  // { size: 'sm', width: '100%', maxHeight: 260 }
filter.setProps({ size: 'lg' }) // merges, notifies, the element re-applies

props() returns one object with a stable identity, so a controller can be handed around and stay in sync. setProps is a merge โ€” keys you omit keep their current value. A grid-owned builder takes the same block: controlMonoTable({ filterBuilder: { props: โ€ฆ } }).

Setting props on the element still works

Writing size / width / max-height directly on <mono-filter-builder> is still supported. Where both declare the same key, the controller wins; keys the controller never mentions are left to the template.

Basic โ€‹

Reading the filter โ€‹

ts
const filter = controlMonoFilterBuilder({ fields, filter: ['Name', 'contains', 'Andy'] })

filter.original({ type: 'array' }) // the filter as first supplied โ€” never changes
filter.changed({ type: 'array' })  // as currently edited; a devextreme expr โ€” feed to a DataSource
filter.changed({ type: 'string' }) // an OData $filter โ€” "contains(Name,'Andy')"
filter.changed()                   // omit `type` to get back the shape you passed in

subscribe(cb) fires on every edit; the element also emits change (with array and string in the detail), plus apply / clear from the action row.

filter takes either shape โ€‹

There is no filterType flag โ€” a string is parsed, an array is normalised, decided by typeof. Input and output shapes are independent, so you can load a string and read back an array.

Parsing covers the subset this builder emits

The string parser handles parentheses, and / or / not, the six comparisons, contains / startswith / endswith, in (โ€ฆ), null, and quoted / numeric / boolean literals. Anything else โ€” a lambda like Job/any(d: d/X eq 1), arithmetic, other functions โ€” warns once and yields an empty builder rather than a filter that silently means something different. Array input has no such limit.

Fields โ€‹

fields is the source of truth for the field dropdown โ€” dataType decides which operators and value editor a column gets, values renders a select instead of free text, and a dotted field maps a nav column (Job.Name โ†’ Job/Name). Pass dataGrid: table instead (or as well) to derive fields from a table's props.th; explicit fields win.

Operators โ€‹

Only OData-expressible operators are offered, filtered by dataType โ€” pick a field type in the demo to see each set. Group operators are and / or / notAnd / notOr (the last two wrap the group in not (โ€ฆ)). in expands to an OR of equalities, between to ge โ€ฆ and le โ€ฆ, and isblank / isnotblank to eq null / ne null. Values are quoted properly โ€” O'Brien serialises as 'O''Brien', numbers and booleans stay bare.

Localisation โ€‹

Every user-facing string is overridable through texts; anything omitted keeps its English default.

ts
controlMonoFilterBuilder({
  fields,
  texts: {
    matchPrefix: 'Cocokkan', matchSuffix: 'dari aturan berikut:',
    and: 'semua', or: 'apapun',
    contains: 'mengandung', eq: 'adalah sama dengan', isblank: 'diatur',
    addRule: 'Peraturan Baru', apply: 'Pencarian', clear: 'Buang',
  },
})

Apply / Clear โ€‹

The action row renders above the rules. It's on by default; pass actions: false when the surrounding modal supplies its own buttons and you'd rather read changed() yourself.

Custom serializer โ€‹

type: 'string' uses a built-in serializer. To route it through your own โ€” e.g. dxFilterToString from mono-utils โ€” pass toODataString; it must be synchronous, and it only affects the string form.

ts
const { dxFilterToString } = useMonoUtility()
controlMonoFilterBuilder({ fields, toODataString: (f) => dxFilterToString({ filter: f }) })

CSS Variables โ€‹

Themed through --mono-filter-* (read via private --_mono-filter-* resolvers, so they inherit and pierce the shadow boundary). The builder scroll area uses the shared --mono-scrollbar-* tokens.

The controls borrow their shape from <mono-input>, <mono-select> and <mono-button> through those components' public knobs โ€” --mono-input-outline-radius, --mono-button-<size>-radius, --mono-control-height-<size> โ€” which is exactly what a flavour writes. So an xs builder matches an xs field, and maia's pill, sera's square edge and lyra's text-xs reach the builder with no flavour deltas of its own.

VariableDefaultControls
--mono-filter-primary--primaryThe Apply button's fill
--mono-filter-on-primary--primary-foregroundInk on the Apply button
--mono-filter-accent--primaryThe add links and the group operator (.btn[data-variant='link'] is text-primary)
--mono-filter-text--foregroundControl and label ink
--mono-filter-muted--muted-foregroundGroup sentence, placeholders, icon buttons at rest
--mono-filter-border--inputControl edge (border-input)
--mono-filter-guide--borderThe dashed guide down a nested group
--mono-filter-surface--mono-mode-surfaceControl fill โ€” transparent in light, the mode's wash in dark
--mono-filter-ring--ringFocus ring on every control and button
--mono-filter-danger--destructiveThe trash button on hover
--mono-filter-indent1.25remNested-group indent
--mono-filter-radius--mono-input-outline-radiusControl corner
--mono-filter-height-<size>--mono-control-height-<size>Control / button height per size
--mono-filter-font-<size>--mono-text-xs โ€ฆ -baseType size per size
--mono-filter-btn-radius-<size>--mono-button-<size>-radiusApply / Clear / icon-button corner per size
--mono-filter-icon-<size>0.7rem โ€ฆ 1.375remIcon-button glyph per size
--mono-filter-gap0.5remRow and group gap

Types โ€‹

controlMonoFilterBuilder is also exported as monoFilterBuilder, and :control-filter-builder is also accepted as :data-filter โ€” the older spellings still work.

Importimport { MonoFilterBuilderProps } from 'mono-helper'
PropValueDefaultDescription
dataFilterMonoFilterControllerโ€”The controller, bound with `:data-filter.prop` / `:dataFilter`.
controlFilterBuilderMonoFilterControllerโ€”Renamed โ€” `:control-filter-builder` / `:controlFilterBuilder` alias `dataFilter`.
size'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'โ€”Visual size of the inner controls (default `sm`).
widthstring | numberโ€”Fixed builder width (CSS length or px number).
heightstring | numberโ€”Fixed builder height โ€” when set, the rules scroll between the pinned Apply/Clear row and the Add rule/group row.
minWidthstring | numberโ€”โ€”
maxWidthstring | numberโ€”โ€”
minHeightstring | numberโ€”โ€”
maxHeightstring | numberโ€”Caps the builder height so a long rule list scrolls instead of growing.