diff --git a/docs/api/filters.md b/docs/api/filters.md
index 63d7c1b..22481a5 100644
--- a/docs/api/filters.md
+++ b/docs/api/filters.md
@@ -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)
@@ -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 [pageStore
](#module_pageStore)
+
+
+### 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 [pageStore
](#module_pageStore)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| key | String
| a filter key |
+
### pageStore.setFilter(key, payload)
diff --git a/docs/introduction/page-definitions.md b/docs/introduction/page-definitions.md
index 645c11a..76253ee 100644
--- a/docs/introduction/page-definitions.md
+++ b/docs/introduction/page-definitions.md
@@ -98,6 +98,10 @@ export default class ExamplePage {
],
afterSet(action, store) {
// do something after set!
+ },
+ valueType: "string",
+ valueValidator: (pageStore, value) => {
+ return value.includes("exampleOption");
}
},
}
@@ -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).
@@ -189,6 +196,10 @@ export default class ExamplePage {
],
afterSet(action, store) {
// do something after set!
+ },
+ valueType: "string",
+ valueValidator: (pageStore, value) => {
+ return value.includes("exampleOption");
}
},
}
diff --git a/docs/usage/filters.md b/docs/usage/filters.md
index e0968b0..190ad8d 100644
--- a/docs/usage/filters.md
+++ b/docs/usage/filters.md
@@ -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).
@@ -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`.
\ No newline at end of file
+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.
\ No newline at end of file
diff --git a/src/store/defaultOption.js b/src/store/defaultOption.js
index 2376e62..34d9abc 100644
--- a/src/store/defaultOption.js
+++ b/src/store/defaultOption.js
@@ -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;
@@ -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 [];
}
diff --git a/src/store/filters.js b/src/store/filters.js
index 14847ea..1228eb3 100644
--- a/src/store/filters.js
+++ b/src/store/filters.js
@@ -138,7 +138,7 @@ export function getFilterGetters() {
state._validFilterKey(key);
return state.getLabelForOptionKey(
state.filters[key],
- state.getFilterDefault(key)
+ state.getFilterDefault(key),
);
};
},
@@ -173,7 +173,7 @@ export function getFilterGetters() {
}
return final;
}.bind(state),
- false
+ false,
);
};
},
@@ -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;
+ };
+ },
};
}
diff --git a/tests/filters.spec.js b/tests/filters.spec.js
index 5e7056c..0e28ce5 100644
--- a/tests/filters.spec.js
+++ b/tests/filters.spec.js
@@ -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", () => {
@@ -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,
);
});
});
@@ -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);
});
@@ -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");
});
});
@@ -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,
);
});
});
@@ -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,
);
});
});
@@ -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();
+ });
});
diff --git a/tests/mocks/pages/TestPage1.js b/tests/mocks/pages/TestPage1.js
index 4981b3f..f62b88a 100644
--- a/tests/mocks/pages/TestPage1.js
+++ b/tests/mocks/pages/TestPage1.js
@@ -17,6 +17,7 @@ export default class TestPage1 {
filter1: {
key: "filter1",
label: "Filter 1",
+ valueType: "string",
props: {
test: true,
},
@@ -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,
},
diff --git a/tests/store.spec.js b/tests/store.spec.js
index 50520f3..6ff8aca 100644
--- a/tests/store.spec.js
+++ b/tests/store.spec.js
@@ -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();