Skip to content

Commit

Permalink
Merge pull request #5 from RTIInternational/default-values-and-valida…
Browse files Browse the repository at this point in the history
…tion-states

Default values and validation states
  • Loading branch information
michael-long88 authored Oct 6, 2023
2 parents e0d2161 + 251396f commit bef9bbe
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 13 deletions.
14 changes: 14 additions & 0 deletions docs/api/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The following API functions allow developers to interact with filters. For more
* [.isFilterDirty(key)](#module_pageStore.isFilterDirty)
* [.areFiltersDirty()](#module_pageStore.areFiltersDirty)
* [.getDirtyFilters()](#module_pageStore.getDirtyFilters)
* [.isFilterValid(key)](#module_pageStore.isFilterValid)
* [.setFilter(key, payload)](#module_pageStore.setFilter)
* [.setFilterLabel(key, payload)](#module_pageStore.setFilterLabel)
* [.setOptionsForFilter(key, payload, setOptionToDefault)](#module_pageStore.setOptionsForFilter)
Expand Down Expand Up @@ -165,6 +166,19 @@ Returns a boolean indicating whether or not any filters on the page have been se
Returns an array of filter keys for dirty filters

**Kind**: static method of [<code>pageStore</code>](#module_pageStore)
<a name="module_pageStore.isFilterValid"></a>

### pageStore.isFilterValid(key)
Returns a boolean indicating whether or not the value of state filter is valid
Validity is calculated using a specified valueType and/or valueValidator on the filter definition
A filter with neither always returns true

**Kind**: static method of [<code>pageStore</code>](#module_pageStore)

| Param | Type | Description |
| --- | --- | --- |
| key | <code>String</code> | a filter key |

<a name="module_pageStore.setFilter"></a>

### pageStore.setFilter(key, payload)
Expand Down
13 changes: 12 additions & 1 deletion docs/introduction/page-definitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ export default class ExamplePage {
],
afterSet(action, store) {
// do something after set!
},
valueType: "string",
valueValidator: (pageStore, value) => {
return value.includes("exampleOption");
}
},
}
Expand All @@ -121,7 +125,10 @@ For each filter in the page definition, Harness-Vue will create the following:
By default, the value of each filter is expected to be a `String`. However, if a filter is given the prop `multiple` set to `true`, then the value of the filter will be expected to be an `Array`.

### Defaults
Harness-Vue will attempt to find defaults for each filter at runtime, as well as whenever instructed to via the `intializeDefaults()` lifecycle hook. Harness-Vue will look for options with the `default` attribute set to `true`. If none exist, it will use the first option as a default.
Harness-Vue will attempt to find defaults for each filter at runtime, as well as whenever instructed to via the `intializeDefaults()` lifecycle hook. Harness-Vue will look for options with the `default` attribute set to `true`. If none exist, it will use the first option as a default. If a `defaultValue` is specified, that will take precendence over all other default values.

### Validation and Type Checking
Harness-Vue optionally allows developers to specify a `valueType` and/or a `valueValidator` function to calculate filter validity. This can be used for styling and function.

## Load Data
A page definition also includes an asynchronous function called `loadData`. This function is given two arguments, (`pageDefinition`, `pageStore`), and is expected to return an object with a key for each chart and a value representing the data each chart should update to include. For more on how this function is used, please see the [lifecycle tutorial](/usage/lifecycle).
Expand Down Expand Up @@ -189,6 +196,10 @@ export default class ExamplePage {
],
afterSet(action, store) {
// do something after set!
},
valueType: "string",
valueValidator: (pageStore, value) => {
return value.includes("exampleOption");
}
},
}
Expand Down
7 changes: 5 additions & 2 deletions docs/usage/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Filters in Harness-Vue are the unit of interactivity. Developers provide filters
* subscriptions: pinia action subscriptions will be set up for the beforeSet and afterSet functions provided by each chart

## Options and Defaults
Filters are often represented as HTML inputs, and often have multiple options. Options are stored separately from the filter value in Harness-Vue and given their own API functions for manipulation. Harness-Vue will set an initial default option by searching the provided options for a `default: true`, or use the first available option if none are set as default explicitly. Harness-Vue also provides an `initializeDefaults()` action, which can optionally take a subset of filter keys to set to their defaults.
Filters are often represented as HTML inputs, and often have multiple options. Options are stored separately from the filter value in Harness-Vue and given their own API functions for manipulation. Harness-Vue will set an initial default option by searching the provided options for a `default: true`, or use the first available option if none are set as default explicitly. Additionally, developers can specify a `defaultValue`, which takes precendence over all other methods. Harness-Vue also provides an `initializeDefaults()` action, which can optionally take a subset of filter keys to set to their defaults.

## Dynamic and Reusable Filters
Similar to the previous section on charts, Harness-Vue provides an API that allows a developer to dynamically reference filters by key. Using this API to refer to `getFilter(filterKey)` and `setFilter(filterKey, payload)` rather than specifying `getExampleSelectFilter`and `setExampleSelectFilter(payload)` allows developers to create reuseable HTML inputs to represent their filters. For a full list of features available for filter interaction, see [the filters API listing](/api/filters).
Expand All @@ -27,4 +27,7 @@ Given the common Harness-Vue lifecycle of running the loadData function on inter
The `afterSet` functionality and the `loadData` function both have access to the page definition and page store, and both can be leveraged to set dependent filters. For example, if the value of `exampleSelectFilter` should be used to determine what options are populated in `exampleRadioGroup`, a developer can add functionality in `afterSet` or `loadData` that determines the correct options and sets them using `setOptionsForFilter("exampleRadioGroup", filterOptions)`.

## Data-Driven Filter Options
Another common use case is for filter options to be set dynamically from the data retrieved in the `loadData` function. Similar to the dependent filters above, a developer can leverage the `afterLoadData` page definition hook or the `loadData` function itself to generate filter options and set them with `setOptionsForFilter`.
Another common use case is for filter options to be set dynamically from the data retrieved in the `loadData` function. Similar to the dependent filters above, a developer can leverage the `afterLoadData` page definition hook or the `loadData` function itself to generate filter options and set them with `setOptionsForFilter`.

## Validation States
Optionally, developers can provide filters with `valueType` and/or a `valueValidator` function that drive the `isFilterValid` API method. This method allows developers to style inputs and prevent `loadData` calls based on filter validity.
15 changes: 15 additions & 0 deletions src/store/defaultOption.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export default function getDefaultOption(filter, options = []) {
// if a defaultValue is specified
if (filter.defaultValue) {
return filter.default;
}

// if no default and has options
if (options.length) {
const defaultOption = options.filter((option) => option.default);
let filterDefault = null;
Expand All @@ -19,6 +25,15 @@ export default function getDefaultOption(filter, options = []) {
return filterDefault;
}

// if no default and has type
if (filter.valueType) {
if (typeof filter.valueType === "function") {
const out = new filter.valueType();
return out.valueOf();
}
}

// if no default, no type, return array or null
if (filter.props && filter.props.multiple === true) {
return [];
}
Expand Down
47 changes: 44 additions & 3 deletions src/store/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export function getFilterGetters() {
state._validFilterKey(key);
return state.getLabelForOptionKey(
state.filters[key],
state.getFilterDefault(key)
state.getFilterDefault(key),
);
};
},
Expand Down Expand Up @@ -173,7 +173,7 @@ export function getFilterGetters() {
}
return final;
}.bind(state),
false
false,
);
};
},
Expand All @@ -188,10 +188,51 @@ export function getFilterGetters() {
return Object.keys(state.filters).filter(
function (filter) {
return state.isFilterDirty(filter);
}.bind(state)
}.bind(state),
);
};
},

/**
* Returns a boolean indicating whether or not the value of state filter is valid
* Validity is calculated using a specified valueType and/or valueValidator on the filter definition
* A filter with neither always returns true
*
* @param {String} key a filter key
* @memberof module:pageStore
*/
isFilterValid(state) {
return (key) => {
state._validFilterKey(key);
const filterDefinition = state.getFilterDefinition(key);
const filterValue = state.getFilter(key);
// if a type is specified and the value doesn't match the type
// return false
if (filterDefinition.valueType) {
// check for arrays differently because they evaluate to object
if (filterDefinition.valueType === "array") {
if (!Array.isArray(filterValue)) {
return false;
}
} else {
if (
typeof filterValue !== filterDefinition.valueType.toLowerCase()
) {
return false;
}
}
}
// if a validator function is present and fails, return false
if (
filterDefinition.valueValidator &&
!filterDefinition.valueValidator(state, filterValue)
) {
return false;
}

return true;
};
},
};
}

Expand Down
30 changes: 25 additions & 5 deletions tests/filters.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe("DV Filter functions", () => {
it("Can Get Filter Props", () => {
let hs = mockHs();
Object.keys(page.filters()).forEach((filterKey) => {
expect(hs.getFilterProps(filterKey)).toEqual({ test: true });
expect(hs.getFilterProps(filterKey).test).toEqual(true);
});
});
it("Can Get Filter Label", () => {
Expand All @@ -61,7 +61,7 @@ describe("DV Filter functions", () => {
let hs = mockHs();
Object.keys(page.filters()).forEach((filterKey) => {
expect(hs.getOptionsForFilter(filterKey)).toEqual(
page.filters()[filterKey].options
page.filters()[filterKey].options,
);
});
});
Expand All @@ -71,7 +71,7 @@ describe("DV Filter functions", () => {
let testOptions = [{ key: "test", label: "Test", default: true }];
hs.setOptionsForFilter(filterKey, testOptions);
expect(hs.getOptionsForFilter(filterKey)).not.toEqual(
page.filters()[filterKey].options
page.filters()[filterKey].options,
);
expect(hs.getOptionsForFilter(filterKey)).toEqual(testOptions);
});
Expand All @@ -84,6 +84,7 @@ describe("DV Filter functions", () => {
{ key: "test2", label: "Test 2", default: true },
];
hs.setOptionsForFilter(filterKey, testOptions, true);

expect(hs.getFilter(filterKey)).toEqual("test2");
});
});
Expand All @@ -92,7 +93,7 @@ describe("DV Filter functions", () => {
Object.keys(page.filters()).forEach((filterKey) => {
page.filters()[filterKey].options.forEach((option) => {
expect(hs.getLabelForOptionKey(filterKey, option.key)).toEqual(
option.label
option.label,
);
});
});
Expand All @@ -103,7 +104,7 @@ describe("DV Filter functions", () => {
page.filters()[filterKey].options.forEach((option) => {
hs.setFilter(filterKey, option.key);
expect(hs.getLabelForSelectedOption(filterKey, option.key)).toEqual(
option.label
option.label,
);
});
});
Expand Down Expand Up @@ -180,4 +181,23 @@ describe("DV Filter functions", () => {
hs.setFilter("filter1", "filter1option2");
expect(hs.getDirtyFilters()).toEqual(["filter1"]);
});

it("Can check filter validity", () => {
let hs = mockHs();
// filter1 is meant to be a string
hs.setFilter("filter1", 4);
expect(hs.isFilterValid("filter1")).toBeFalsy();
hs.setFilter("filter1", "4");
expect(hs.isFilterValid("filter1")).toBeTruthy();

// filter2 is meant to be an array of numbers
hs.setFilter("filter2", 4);
expect(hs.isFilterValid("filter2")).toBeFalsy();
hs.setFilter("filter2", []);
expect(hs.isFilterValid("filter2")).toBeTruthy();
hs.setFilter("filter2", ["a", 3, "b", 4]);
expect(hs.isFilterValid("filter2")).toBeFalsy();
hs.setFilter("filter2", [3, 4, 5, 5.5]);
expect(hs.isFilterValid("filter2")).toBeTruthy();
});
});
13 changes: 12 additions & 1 deletion tests/mocks/pages/TestPage1.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default class TestPage1 {
filter1: {
key: "filter1",
label: "Filter 1",
valueType: "string",
props: {
test: true,
},
Expand All @@ -35,7 +36,17 @@ export default class TestPage1 {
},
filter2: {
key: "filter2",
label: "Filter 1",
label: "Filter 2",
valueType: "array",
valueValidator: (state, value) => {
let ret = true;
value.forEach((v) => {
if (typeof v !== "number") {
ret = false;
}
});
return ret;
},
props: {
test: true,
},
Expand Down
5 changes: 4 additions & 1 deletion tests/store.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ describe("DV Helper Init", () => {
});
it("Has filters", () => {
let hs = mockHs();
expect(hs.filters).toEqual(page.filters());
expect(hs.filters).toBeTruthy();
// note that this no longer works with a value validator
// as it can't compare functions
// expect(hs.filters).toEqual(page.filters());
});
it("Maps chart getters", () => {
let hs = mockHs();
Expand Down

0 comments on commit bef9bbe

Please sign in to comment.