Skip to content

Commit

Permalink
chore: Refactor inputs, add better types. (#686)
Browse files Browse the repository at this point in the history
* chore: Refactor inputs, add better types.

* chore: Update Table extends annotation.
  • Loading branch information
jheer authored Feb 14, 2025
1 parent 76bbfaf commit 12f0c1d
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 50 deletions.
46 changes: 34 additions & 12 deletions packages/inputs/src/Menu.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,46 @@
import { MosaicClient, Param, isParam, isSelection, clausePoint } from '@uwdata/mosaic-core';
import { Param, Selection, isParam, isSelection, clausePoint } from '@uwdata/mosaic-core';
import { Query } from '@uwdata/mosaic-sql';
import { input } from './input.js';
import { Input, input } from './input.js';

const isObject = v => {
return v && typeof v === 'object' && !Array.isArray(v);
};

/**
* Create a new menu input instance.
* @param {object} [options] Options object
* @param {HTMLElement} [options.element] The parent DOM element in which to
* place the menu elements. If undefined, a new `div` element is created.
* @param {Selection} [options.filterBy] A selection to filter the database
* table indicated by the *from* option.
* @param {Param} [options.as] The output param or selection. A selection
* clause is added for the currently selected menu option.
* @param {string} [options.field] The database column name to use within
* generated selection clause predicates. Defaults to the *column* option.
* @param {(any | { value: any, label?: string })[]} [options.options] An
* array of menu options, as literal values or option objects. Option
* objects have a `value` property and an optional `label` property. If no
* label or *format* function is provided, the string-coerced value is used.
* @param {(value: any) => string} [options.format] A format function that
* takes an option value as input and generates a string label. The format
* function is not applied when an explicit label is provided in an option
* object.
* @param {*} [options.value] The initial selected menu value.
* @param {string} [options.from] The name of a database table to use as a data
* source for this widget. Used in conjunction with the *column* option.
* @param {string} [options.column] The name of a database column from which
* to pull menu options. The unique column values are used as menu options.
* Used in conjunction with the *from* option.
* @param {string} [options.label] A text label for this input.
* @returns {HTMLElement} The container element for a menu input.
*/
export const menu = options => input(Menu, options);

/**
* A HTML select based dropdown menu input.
*
* @import {Activatable} from '@uwdata/mosaic-core'
* @implements {Activatable}
* A HTML <select>-based dropdown menu input.
* @extends {Input}
*/
export class Menu extends MosaicClient {
export class Menu extends Input {
/**
* Create a new menu input.
* @param {object} [options] Options object
Expand Down Expand Up @@ -54,17 +80,13 @@ export class Menu extends MosaicClient {
value,
field = column
} = {}) {
super(filterBy);
super(filterBy, element);
this.from = from;
this.column = column;
this.format = format;
this.field = field;
const selection = this.selection = as;

this.element = element ?? document.createElement('div');
this.element.setAttribute('class', 'input');
Object.defineProperty(this.element, 'value', { value: this });

const lab = document.createElement('label');
lab.innerText = label || column;
this.element.appendChild(lab);
Expand Down
44 changes: 32 additions & 12 deletions packages/inputs/src/Search.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
import { MosaicClient, Param, isParam, isSelection, clauseMatch } from '@uwdata/mosaic-core';
import { Param, Selection, isParam, isSelection, clauseMatch } from '@uwdata/mosaic-core';
import { Query } from '@uwdata/mosaic-sql';
import { input } from './input.js';
import { Input, input } from './input.js';

let _id = 0;

/**
* Create a new text search input instance.
* @param {object} [options] Options object
* @param {HTMLElement} [options.element] The parent DOM element in which to
* place the search elements. If undefined, a new `div` element is created.
* @param {Selection} [options.filterBy] A selection to filter the database
* table indicated by the *from* option.
* @param {Param} [options.as] The output param or selection. A selection
* clause is added based on the current text search query.
* @param {string} [options.field] The database column name to use within
* generated selection clause predicates. Defaults to the *column* option.
* @param {'contains' | 'prefix' | 'suffix' | 'regexp'} [options.type] The
* type of text search query to perform. One of:
* - `"contains"` (default): the query string may appear anywhere in the text
* - `"prefix"`: the query string must appear at the start of the text
* - `"suffix"`: the query string must appear at the end of the text
* - `"regexp"`: the query string is a regular expression the text must match
* @param {string} [options.from] The name of a database table to use as an
* autocomplete data source for this widget. Used in conjunction with the
* *column* option.
* @param {string} [options.column] The name of a database column from which
* to pull valid search results. The unique column values are used as search
* autocomplete values. Used in conjunction with the *from* option.
* @param {string} [options.label] A text label for this input.
* @returns {HTMLElement} The container element for a text search input.
*/
export const search = options => input(Search, options);

/**
* A HTML input based text search input.
*
* @import {Activatable} from '@uwdata/mosaic-core'
* @implements {Activatable}
* A HTML text search input.
* @extends {Input}
*/
export class Search extends MosaicClient {
export class Search extends Input {
/**
* Create a new text search input.
* @param {object} [options] Options object
Expand Down Expand Up @@ -48,18 +72,14 @@ export class Search extends MosaicClient {
field = column,
as
} = {}) {
super(filterBy);
super(filterBy, element);
this.id = 'search_' + (++_id);
this.type = type;
this.from = from;
this.column = column;
this.selection = as;
this.field = field;

this.element = element ?? document.createElement('div');
this.element.setAttribute('class', 'input');
Object.defineProperty(this.element, 'value', { value: this });

if (label) {
const lab = document.createElement('label');
lab.setAttribute('for', this.id);
Expand Down
49 changes: 37 additions & 12 deletions packages/inputs/src/Slider.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
import { MosaicClient, Param, clauseInterval, clausePoint, isParam, isSelection } from '@uwdata/mosaic-core';
import { Param, Selection, clauseInterval, clausePoint, isParam, isSelection } from '@uwdata/mosaic-core';
import { Query, max, min } from '@uwdata/mosaic-sql';
import { input } from './input.js';
import { Input, input } from './input.js';

let _id = 0;

/**
* Create a new slider input instance.
* @param {object} [options] Options object
* @param {HTMLElement} [options.element] The parent DOM element in which to
* place the slider elements. If undefined, a new `div` element is created.
* @param {Selection} [options.filterBy] A selection to filter the database
* table indicated by the *from* option.
* @param {Param} [options.as] The output param or selection. A selection
* clause is added based on the currently selected slider option.
* @param {string} [options.field] The database column name to use within
* generated selection clause predicates. Defaults to the *column* option.
* @param {'point' | 'interval'} [options.select] The type of selection clause
* predicate to generate if the **as** option is a Selection. If `'point'`
* (the default), the selection predicate is an equality check for the slider
* value. If `'interval'`, the predicate checks an interval from the minimum
* to the current slider value.
* @param {number} [options.min] The minimum slider value.
* @param {number} [options.max] The maximum slider value.
* @param {number} [options.step] The slider step, the amount to increment
* between consecutive values.
* @param {number} [options.value] The initial slider value.
* @param {string} [options.from] The name of a database table to use as a data
* source for this widget. Used in conjunction with the *column* option.
* The minimum and maximum values of the column determine the slider range.
* @param {string} [options.column] The name of a database column whose values
* determine the slider range. Used in conjunction with the *from* option.
* The minimum and maximum values of the column determine the slider range.
* @param {string} [options.label] A text label for this input.
* @param {number} [options.width] The width of the slider in screen pixels.
* @returns {HTMLElement} The container element for a slider input.
*/
export const slider = options => input(Slider, options);

/**
* A HTML range based slider input.
*
* @import {Activatable} from '@uwdata/mosaic-core'
* @implements {Activatable}
* A HTML range-based slider input.
* @extends {Input}
*/
export class Slider extends MosaicClient {
export class Slider extends Input {
/**
* Create a new slider input.
* @param {object} [options] Options object
Expand Down Expand Up @@ -58,7 +87,7 @@ export class Slider extends MosaicClient {
field = column,
width
} = {}) {
super(filterBy);
super(filterBy, element);
this.id = 'slider_' + (++_id);
this.from = from;
this.column = column || 'value';
Expand All @@ -69,10 +98,6 @@ export class Slider extends MosaicClient {
this.max = max;
this.step = step;

this.element = element || document.createElement('div');
this.element.setAttribute('class', 'input');
Object.defineProperty(this.element, 'value', { value: this });

if (label) {
const desc = document.createElement('label');
desc.setAttribute('for', this.id);
Expand Down
77 changes: 66 additions & 11 deletions packages/inputs/src/Table.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,77 @@
import { MosaicClient, clausePoints, coordinator, isParam, isSelection, toDataColumns } from '@uwdata/mosaic-core';
import { Selection, clausePoints, coordinator, isParam, isSelection, toDataColumns } from '@uwdata/mosaic-core';
import { Query, desc } from '@uwdata/mosaic-sql';
import { formatDate, formatLocaleAuto, formatLocaleNumber } from './util/format.js';
import { input } from './input.js';
import { Input, input } from './input.js';

let _id = -1;

/**
* Create a new table input instance.
* @param {object} options Options object
* @param {HTMLElement} [options.element] The parent DOM element in which to
* place the table element. If undefined, a new `div` element is created.
* @param {Selection} [options.filterBy] A selection to filter the database
* table indicated by the *from* option.
* @param {Selection} [options.as] The output selection. A selection
* clause is added for the currently selected table row.
* @param {{ [name: string]: 'left' | 'right' | 'center' }} [options.align]
* An object that maps column names to horiztonal text alignment values. If
* unspecified, alignment is determined based on the column data type.
* @param {{ [name: string]: (value: any) => string }} [options.format] An
* object that maps column names to format functions to use for that
* column's data. Each format function takes a value as input and generates
* formatted text to show in the table.
* @param {string} [options.from] The name of a database table to use as a data
* source for this widget. Used in conjunction with the *columns* option.
* @param {string[]} [options.columns] The name of database columns to include
* in the table component. If unspecified, all columns are included.
* Used in conjunction with the *from* option.
* @param {number | { [name: string]: number }} [options.width] If a number,
* sets the desired width of the table, in pixels. If an object, is used to
* set explicit pixel widts for each named column included in the object.
* @param {number} [options.maxWidth] The maximum width of the table, in pixels.
* @param {number} [options.height] The desired height of the table, in pixels.
* @param {number} [options.rowBatch] The number of rows to request per query
* batch. The batch size will be used to prefetch data beyond the currently
* visible range.
* @returns {HTMLElement} The container element for a table component.
*/
export const table = options => input(Table, options);


/**
* A HTML table based table component.
*
* @import {Activatable} from '@uwdata/mosaic-core'
* @implements {Activatable}
* @extends {Input}
*/
export class Table extends MosaicClient {
export class Table extends Input {
/**
* Create a new Table instance.
* @param {object} options Options object
* @param {HTMLElement} [options.element] The parent DOM element in which to
* place the table element. If undefined, a new `div` element is created.
* @param {Selection} [options.filterBy] A selection to filter the database
* table indicated by the *from* option.
* @param {Selection} [options.as] The output selection. A selection
* clause is added for the currently selected table row.
* @param {{ [name: string]: 'left' | 'right' | 'center' }} [options.align]
* An object that maps column names to horiztonal text alignment values. If
* unspecified, alignment is determined based on the column data type.
* @param {{ [name: string]: (value: any) => string }} [options.format] An
* object that maps column names to format functions to use for that
* column's data. Each format function takes a value as input and generates
* formatted text to show in the table.
* @param {string} [options.from] The name of a database table to use as a data
* source for this widget. Used in conjunction with the *columns* option.
* @param {string[]} [options.columns] The name of database columns to include
* in the table component. If unspecified, all columns are included.
* Used in conjunction with the *from* option.
* @param {number | { [name: string]: number }} [options.width] If a number,
* sets the desired width of the table, in pixels. If an object, is used to
* set explicit pixel widts for each named column included in the object.
* @param {number} [options.maxWidth] The maximum width of the table, in pixels.
* @param {number} [options.height] The desired height of the table, in pixels.
* @param {number} [options.rowBatch] The number of rows to request per query
* batch. The batch size will be used to prefetch data beyond the currently
* visible range.
*/
constructor({
element,
Expand All @@ -32,8 +86,11 @@ export class Table extends MosaicClient {
rowBatch = 100,
as
} = {}) {
super(filterBy);
super(filterBy, element, null);

this.id = `table-${++_id}`;
this.element.setAttribute('id', this.id);

this.from = from;
this.columns = columns;
this.format = format;
Expand All @@ -56,9 +113,6 @@ export class Table extends MosaicClient {
this.sortColumn = null;
this.sortDesc = false;

this.element = element || document.createElement('div');
this.element.setAttribute('id', this.id);
Object.defineProperty(this.element, 'value', { value: this });
if (typeof width === 'number') this.element.style.width = `${width}px`;
if (maxWidth) this.element.style.maxWidth = `${maxWidth}px`;
this.element.style.maxHeight = `${height}px`;
Expand All @@ -67,6 +121,7 @@ export class Table extends MosaicClient {
let prevScrollTop = -1;
this.element.addEventListener('scroll', evt => {
const { isPending, loaded } = this;
// @ts-ignore
const { scrollHeight, scrollTop, clientHeight } = evt.target;

const back = scrollTop < prevScrollTop;
Expand Down
39 changes: 36 additions & 3 deletions packages/inputs/src/input.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,40 @@
import { coordinator } from '@uwdata/mosaic-core';
import { coordinator, MosaicClient } from '@uwdata/mosaic-core';

export function input(InputClass, options) {
const input = new InputClass(options);
/**
* Instantiate an input, register it with the coordinator, and
* return the corresponding HTML element.
* @template {new (...args: any) => Input} T
* @param {T} InputClass
* @param {ConstructorParameters<T>} params
* @returns {HTMLElement} The container element of the input.
*/
export function input(InputClass, ...params) {
const input = new InputClass(...params);
coordinator().connect(input);
return input.element;
}

/**
* Base class for input components.
* @import {Activatable} from '@uwdata/mosaic-core'
* @implements {Activatable}
*/
export class Input extends MosaicClient {
/**
* Create a new input instance.
* @param {import('@uwdata/mosaic-core').Selection} [filterBy] A selection
* with which to filter backing data that parameterizes this input.
* @param {HTMLElement} [element] Optional container HTML element to use.
* @param {string} [className] A class name to set on the container element.
*/
constructor(filterBy, element, className = 'input') {
super(filterBy);
this.element = element || document.createElement('div');
if (className) this.element.setAttribute('class', className);
Object.defineProperty(this.element, 'value', { value: this });
}

activate() {
// subclasses should override
}
}

0 comments on commit 12f0c1d

Please sign in to comment.