Further type additons

This commit is contained in:
Josh Johnson 2019-12-15 21:18:23 +00:00
parent 5a47867a19
commit d93358e1e4
15 changed files with 240 additions and 129 deletions

View file

@ -1,7 +1,33 @@
import { Action } from 'redux';
import { ACTION_TYPES } from '../constants';
import { Choice } from '../interfaces';
export interface AddChoiceAction {
type: typeof ACTION_TYPES.ADD_CHOICE;
id: number;
value: string;
label: string;
groupId: number;
disabled: boolean;
elementId: number;
customProperties: object;
placeholder: boolean;
keyCode: number;
}
export interface FilterChoicesAction {
type: typeof ACTION_TYPES.FILTER_CHOICES;
results: Choice[];
}
export interface ActivateChoicesAction {
type: typeof ACTION_TYPES.ACTIVATE_CHOICES;
active: boolean;
}
export interface ClearChoicesAction {
type: typeof ACTION_TYPES.CLEAR_CHOICES;
}
export const addChoice = ({
value,
label,
@ -12,7 +38,7 @@ export const addChoice = ({
customProperties,
placeholder,
keyCode,
}): Action & Choice => ({
}): AddChoiceAction => ({
type: ACTION_TYPES.ADD_CHOICE,
value,
label,
@ -25,20 +51,16 @@ export const addChoice = ({
keyCode,
});
export const filterChoices = (
results: Choice[],
): Action & { results: Choice[] } => ({
export const filterChoices = (results: Choice[]): FilterChoicesAction => ({
type: ACTION_TYPES.FILTER_CHOICES,
results,
});
export const activateChoices = (
active = true,
): Action & { active: boolean } => ({
export const activateChoices = (active = true): ActivateChoicesAction => ({
type: ACTION_TYPES.ACTIVATE_CHOICES,
active,
});
export const clearChoices = (): Action => ({
export const clearChoices = (): ClearChoicesAction => ({
type: ACTION_TYPES.CLEAR_CHOICES,
});

View file

@ -1,13 +1,24 @@
import { Action } from 'redux';
import { ACTION_TYPES } from '../constants';
import { Group } from '../interfaces';
export interface AddGroupAction {
type: typeof ACTION_TYPES.ADD_GROUP;
id: number;
value: string;
active: boolean;
disabled: boolean;
}
export const addGroup = ({
value,
id,
active,
disabled,
}: Group): Action & Group => ({
}: {
id: number;
value: string;
active: boolean;
disabled: boolean;
}): AddGroupAction => ({
type: ACTION_TYPES.ADD_GROUP,
value,
id,

View file

@ -1,6 +1,28 @@
import { Action } from 'redux';
import { ACTION_TYPES } from '../constants';
import { Item } from '../interfaces';
export interface AddItemAction {
type: typeof ACTION_TYPES.ADD_ITEM;
id: number;
value: string;
label: string;
choiceId: number;
groupId: number;
customProperties: object;
placeholder: boolean;
keyCode: number;
}
export interface RemoveItemAction {
type: typeof ACTION_TYPES.REMOVE_ITEM;
id: number;
choiceId: number;
}
export interface HighlightItemAction {
type: typeof ACTION_TYPES.HIGHLIGHT_ITEM;
id: number;
highlighted: boolean;
}
export const addItem = ({
value,
@ -11,7 +33,16 @@ export const addItem = ({
customProperties,
placeholder,
keyCode,
}: Item): Action & Item => ({
}: {
id: number;
value: string;
label: string;
choiceId: number;
groupId: number;
customProperties: object;
placeholder: boolean;
keyCode: number;
}): AddItemAction => ({
type: ACTION_TYPES.ADD_ITEM,
value,
label,
@ -23,10 +54,7 @@ export const addItem = ({
keyCode,
});
export const removeItem = (
id: number,
choiceId: number,
): Action & { id: number; choiceId: number } => ({
export const removeItem = (id: number, choiceId: number): RemoveItemAction => ({
type: ACTION_TYPES.REMOVE_ITEM,
id,
choiceId,
@ -35,7 +63,7 @@ export const removeItem = (
export const highlightItem = (
id: number,
highlighted: boolean,
): Action & { id: number; highlighted: boolean } => ({
): HighlightItemAction => ({
type: ACTION_TYPES.HIGHLIGHT_ITEM,
id,
highlighted,

View file

@ -1,18 +1,30 @@
import { Action } from 'redux';
import { State } from '../interfaces';
import { ACTION_TYPES } from '../constants';
export const clearAll = (): Action => ({
type: 'CLEAR_ALL',
export interface ClearAllAction {
type: typeof ACTION_TYPES.CLEAR_ALL;
}
export interface ResetToAction {
type: typeof ACTION_TYPES.RESET_TO;
state: State;
}
export interface SetIsLoadingAction {
type: typeof ACTION_TYPES.SET_IS_LOADING;
isLoading: boolean;
}
export const clearAll = (): ClearAllAction => ({
type: ACTION_TYPES.CLEAR_ALL,
});
export const resetTo = (state: State): Action & { state: State } => ({
type: 'RESET_TO',
export const resetTo = (state: State): ResetToAction => ({
type: ACTION_TYPES.RESET_TO,
state,
});
export const setIsLoading = (
isLoading: boolean,
): Action & { isLoading: boolean } => ({
type: 'SET_IS_LOADING',
export const setIsLoading = (isLoading: boolean): SetIsLoadingAction => ({
type: ACTION_TYPES.SET_IS_LOADING,
isLoading,
});

View file

@ -370,7 +370,7 @@ class Choices {
}
highlightItem(item: Item, runEvent = true): this {
if (!item) {
if (!item || !item.id) {
return this;
}
@ -392,7 +392,7 @@ class Choices {
}
unhighlightItem(item: Item): this {
if (!item) {
if (!item || !item.id) {
return this;
}
@ -967,7 +967,9 @@ class Choices {
if (this._isTextElement) {
// Update the value of the hidden input
this.passedElement.value = items;
this.passedElement.value = items
.map(({ value }) => value)
.join(this.config.delimiter);
} else {
// Update the options of the hidden input
(this.passedElement as WrappedSelect).options = items;
@ -1912,22 +1914,20 @@ class Choices {
label = null,
choiceId = -1,
groupId = -1,
customProperties = null,
customProperties = {},
placeholder = false,
keyCode = null,
keyCode = -1,
}: {
value: string;
label?: string | null;
choiceId?: number;
groupId?: number;
customProperties?: object | null;
customProperties?: object;
placeholder?: boolean;
keyCode?: number | null;
keyCode?: number;
}): void {
let passedValue = typeof value === 'string' ? value.trim() : value;
const passedKeyCode = keyCode;
const passedCustomProperties = customProperties;
const { items } = this._store;
const passedLabel = label || passedValue;
const passedOptionId = choiceId || -1;
@ -1953,7 +1953,7 @@ class Choices {
groupId,
customProperties,
placeholder,
keyCode: passedKeyCode,
keyCode,
}),
);
@ -1966,9 +1966,9 @@ class Choices {
id,
value: passedValue,
label: passedLabel,
customProperties: passedCustomProperties,
customProperties,
groupValue: group && group.value ? group.value : null,
keyCode: passedKeyCode,
keyCode,
});
}
@ -1977,6 +1977,10 @@ class Choices {
const group =
groupId && groupId >= 0 ? this._store.getGroupById(groupId) : null;
if (!id || !choiceId) {
return;
}
this._store.dispatch(removeItem(id, choiceId));
this.passedElement.triggerEvent(EVENTS.removeItem, {
id,
@ -1993,18 +1997,18 @@ class Choices {
isSelected = false,
isDisabled = false,
groupId = -1,
customProperties = null,
customProperties = {},
placeholder = false,
keyCode = null,
keyCode = -1,
}: {
value: string;
label?: string | null;
isSelected?: boolean;
isDisabled?: boolean;
groupId?: number;
customProperties?: Record<string, any> | null;
customProperties?: Record<string, any>;
placeholder?: boolean;
keyCode?: number | null;
keyCode?: number;
}): void {
if (typeof value === 'undefined' || value === null) {
return;

View file

@ -22,11 +22,8 @@ export default class WrappedInput extends WrappedElement {
return this.element.value;
}
set value(items: Item[]): void {
const itemValues = items.map(({ value }) => value);
const joinedValues = itemValues.join(this.delimiter);
this.element.setAttribute('value', joinedValues);
this.element.value = joinedValues;
set value(value: string) {
this.element.setAttribute('value', value);
this.element.value = value;
}
}

View file

@ -106,6 +106,8 @@ export const ACTION_TYPES: Record<ActionType, ActionType> = {
REMOVE_ITEM: 'REMOVE_ITEM',
HIGHLIGHT_ITEM: 'HIGHLIGHT_ITEM',
CLEAR_ALL: 'CLEAR_ALL',
RESET_TO: 'RESET_TO',
SET_IS_LOADING: 'SET_IS_LOADING',
};
export const KEY_CODES: KeyCodeMap = {

View file

@ -29,6 +29,7 @@ export interface Choice {
selected?: boolean;
value: string;
score?: number;
choices?: Choice[];
}
export interface Group {
@ -39,7 +40,6 @@ export interface Group {
}
export interface Item extends Choice {
choiceId?: number;
keyCode?: number;
highlighted?: boolean;
}
@ -179,7 +179,9 @@ export type ActionType =
| 'ADD_ITEM'
| 'REMOVE_ITEM'
| 'HIGHLIGHT_ITEM'
| 'CLEAR_ALL';
| 'CLEAR_ALL'
| 'RESET_TO'
| 'SET_IS_LOADING';
export interface Templates {
containerOuter: (
@ -813,9 +815,9 @@ export interface Notice {
}
export interface State {
choices: Choice[];
groups: Group[];
items: Item[];
choices: object[];
groups: object[];
items: object[];
general: {
loading: boolean;
};

View file

@ -1,4 +1,4 @@
import { EventMap } from '../interfaces';
import { EventMap, Choice } from '../interfaces';
/* eslint-disable @typescript-eslint/no-explicit-any */
@ -126,10 +126,12 @@ export const sortByAlpha = (
numeric: true,
});
export const sortByScore = (
a: { score: number },
b: { score: number },
): number => a.score - b.score;
export const sortByScore = (a: Choice, b: Choice): number => {
const { score: scoreA = 0 } = a;
const { score: scoreB = 0 } = b;
return scoreA - scoreB;
};
export const dispatchEvent = (
element: HTMLElement,

View file

@ -1,5 +1,11 @@
import { Action } from 'redux';
import { Choice, Item } from '../interfaces';
import { Choice, State } from '../interfaces';
import {
AddChoiceAction,
FilterChoicesAction,
ActivateChoicesAction,
ClearChoicesAction,
} from '../actions/choices';
import { AddItemAction, RemoveItemAction } from '../actions/items';
export const defaultState = [];
@ -8,53 +14,53 @@ interface Result {
score: number;
}
type ActionTypes =
| AddChoiceAction
| FilterChoicesAction
| ActivateChoicesAction
| ClearChoicesAction
| AddItemAction
| RemoveItemAction;
export default function choices(
state: Choice[] = defaultState,
action: Action,
): Choice[] {
action: ActionTypes,
): State['choices'] {
switch (action.type) {
case 'ADD_CHOICE': {
const addChoiceAction = action as AddChoiceAction;
const choice = {
id: addChoiceAction.id,
elementId: addChoiceAction.elementId,
groupId: addChoiceAction.groupId,
value: addChoiceAction.value,
label: addChoiceAction.label || addChoiceAction.value,
disabled: addChoiceAction.disabled || false,
selected: false,
active: true,
score: 9999,
customProperties: addChoiceAction.customProperties,
placeholder: addChoiceAction.placeholder || false,
keyCode: null,
};
/*
A disabled choice appears in the choice dropdown but cannot be selected
A selected choice has been added to the passed input's value (added as an item)
An active choice appears within the choice dropdown
*/
return [
...state,
{
id: action.id,
elementId: action.elementId,
groupId: action.groupId,
value: action.value,
label: action.label || action.value,
disabled: action.disabled || false,
selected: false,
active: true,
score: 9999,
customProperties: action.customProperties,
placeholder: action.placeholder || false,
keyCode: null,
},
];
return [...state, choice];
}
case 'ADD_ITEM': {
// If all choices need to be activated
if (action.activateOptions) {
return state.map(obj => {
const choice = obj;
choice.active = action.active;
return choice;
});
}
const addItemAction = action as AddItemAction;
// When an item is added and it has an associated choice,
// we want to disable it so it can't be chosen again
if (action.choiceId > -1) {
if (addItemAction.choiceId > -1) {
return state.map(obj => {
const choice = obj;
if (choice.id === parseInt(action.choiceId, 10)) {
if (choice.id === parseInt(`${addItemAction.choiceId}`, 10)) {
choice.selected = true;
}
@ -66,12 +72,14 @@ export default function choices(
}
case 'REMOVE_ITEM': {
const removeItemAction = action as RemoveItemAction;
// When an item is removed and it has an associated choice,
// we want to re-enable it so it can be chosen again
if (action.choiceId && action.choiceId > -1) {
if (removeItemAction.choiceId && removeItemAction.choiceId > -1) {
return state.map(obj => {
const choice = obj;
if (choice.id === parseInt(`${action.choiceId}`, 10)) {
if (choice.id === parseInt(`${removeItemAction.choiceId}`, 10)) {
choice.selected = false;
}
@ -83,28 +91,34 @@ export default function choices(
}
case 'FILTER_CHOICES': {
const filterChoicesAction = action as FilterChoicesAction;
return state.map(obj => {
const choice = obj;
// Set active state based on whether choice is
// within filtered results
choice.active = action.results.some(({ item, score }: Result) => {
if (item.id === choice.id) {
choice.score = score;
choice.active = filterChoicesAction.results.some(
({ item, score }: Result) => {
if (item.id === choice.id) {
choice.score = score;
return true;
}
return true;
}
return false;
});
return false;
},
);
return choice;
});
}
case 'ACTIVATE_CHOICES': {
const activateChoicesAction = action as ActivateChoicesAction;
return state.map(obj => {
const choice = obj;
choice.active = action.active;
choice.active = activateChoicesAction.active;
return choice;
});

View file

@ -1,13 +1,16 @@
import { Action } from 'redux';
import { SetIsLoadingAction } from '../actions/misc';
import { State } from '../interfaces';
export const defaultState = {
loading: false,
};
type ActionTypes = SetIsLoadingAction;
const general = (
state = defaultState,
action: Action & { isLoading: boolean },
): { loading: boolean } => {
action: ActionTypes,
): State['general'] => {
switch (action.type) {
case 'SET_IS_LOADING': {
return {

View file

@ -1,21 +1,26 @@
import { Action } from 'redux';
import { Group } from '../interfaces';
import { Group, State } from '../interfaces';
import { AddGroupAction } from '../actions/groups';
import { ClearChoicesAction } from '../actions/choices';
export const defaultState = [];
type ActionTypes = AddGroupAction | ClearChoicesAction;
export default function groups(
state: Group[] = defaultState,
action: Action & Group,
): Group[] {
action: ActionTypes,
): State['groups'] {
switch (action.type) {
case 'ADD_GROUP': {
const addGroupAction = action as AddGroupAction;
return [
...state,
{
id: action.id,
value: action.value,
active: action.active,
disabled: action.disabled,
id: addGroupAction.id,
value: addGroupAction.value,
active: addGroupAction.active,
disabled: addGroupAction.disabled,
},
];
}

View file

@ -21,7 +21,7 @@ const appReducer = combineReducers({
general,
});
const rootReducer = (passedState, action) => {
const rootReducer = (passedState, action): object => {
let state = passedState;
// If we are clearing all items, groups and options we reassign
// state and then pass that state to our proper reducer. This isn't

View file

@ -16,7 +16,7 @@ describe('reducers/items', () => {
const customProperties = {
property: 'value',
};
const placeholder = 'This is a placeholder';
const placeholder = true;
const keyCode = 10;
describe('passing expected values', () => {

View file

@ -1,27 +1,34 @@
import { Action } from 'redux';
import { Item } from '../interfaces';
import { Item, State } from '../interfaces';
import {
AddItemAction,
RemoveItemAction,
HighlightItemAction,
} from '../actions/items';
export const defaultState = [];
type ActionTypes = AddItemAction | RemoveItemAction | HighlightItemAction;
export default function items(
state: Item[] = defaultState,
action: Action & Item,
): Item[] {
action: ActionTypes,
): State['items'] {
switch (action.type) {
case 'ADD_ITEM': {
const addItemAction = action as AddItemAction;
// Add object to items array
const newState = [
...state,
{
id: action.id,
choiceId: action.choiceId,
groupId: action.groupId,
value: action.value,
label: action.label,
id: addItemAction.id,
choiceId: addItemAction.choiceId,
groupId: addItemAction.groupId,
value: addItemAction.value,
label: addItemAction.label,
active: true,
highlighted: false,
customProperties: action.customProperties,
placeholder: action.placeholder || false,
customProperties: addItemAction.customProperties,
placeholder: addItemAction.placeholder || false,
keyCode: null,
},
];
@ -47,10 +54,12 @@ export default function items(
}
case 'HIGHLIGHT_ITEM': {
const highlightItemAction = action as HighlightItemAction;
return state.map(obj => {
const item = obj;
if (item.id === action.id) {
item.highlighted = action.highlighted;
if (item.id === highlightItemAction.id) {
item.highlighted = highlightItemAction.highlighted;
}
return item;