The main reason xinjs-ui
exists is that I wanted to have a really good table component I could just plug into projects. It would be easier to just use something off the shelf, but virtual table components aren’t actually widely available, and those that are are not free, quite heavy, and tend to come with a lot of stack assumptions and dependencies.
Also, being able to implement a really good table is an acid-test for a framework.
As soon as you build a good table, you’re going to need to filter it (i.e. search) and sort it. So xinjs-ui
has a really slick <filter-builder>
and a simple but powerful makeSorter
function for creating complex callbacks for Array.sort
.

The original <filter-builder>
was driven by a syntax inspired from google search (in essence instead of tokens like site:foo.com it allowed haystack:needle, but provided alternatives to the ‘:’ allowing comparisons such as ‘<‘ (less than), ‘>>’ (after), ‘=’ (equals, vs contains) and so on.
While this seemed very powerful and clever and efficient to myself and my colleagues, the users struggled with it, and so I provided extensive online-help, automatic hints, and added features such as support for quoted strings with spaces, and so on.
Yesterday, I tore all this up and replaced it with a graphical user interface inspired by Apple’s Finder (but simpler and more explicit than that). While it took me a couple of months to figure out exactly how to implement this, the code is now actually simpler and has fewer failure modes. Instead of parsing a simple grammar, the user interface enforces sensible inputs.
The old contains
FilterMaker
looked like this:
{
hint: 'field:value',
explanation: 'field contains value, ignoring case',
description: (field: string, value: string) =>
`${field} contains "${value}"`,
token: /^([^\s]+?):(.+)$/,
makeFilter: (field: string, value: string) => {
value = value.toLocaleLowerCase()
return (obj: any) =>
String(obj[field]).toLocaleLowerCase().includes(value)
},
}
The new version looks like this:
contains: {
caption: 'contains',
negative: 'does not contain',
makeTest: (value: string) => {
value = value.toLocaleLowerCase()
return (obj: any) => String(obj).toLocaleLowerCase().includes(value)
},
}
The new <filter-builder>
is simpler to extend because writing a new FilterMaker
doesn’t involve writing a function to describe the resulting test function, regular expressions to parse the token, and most filters can easily be configured to provide both positive (“contains”) and negative (“does not contain”) variations. The makeFilter
function is simpler because it doesn’t look at objects, just values. The order in which FilterMakers
are evaluated no longer matters, which means I don’t need to explain that in the documentation.
As usual, it seems like a really good design decision is a virtuous circle: easier to use, less complex to implement, and easier to extend. As of writing, filter-builder.ts
is 434 lines including 100 lines of inline documentation with live example. The old version was 384 lines without documentation.
Post Script
I just realized the simplification win was further understated because the amount of CSS required to make this all work actually got smaller, and the new component is self-contained with respect to CSS while the old one was not!