diff --git a/src/scripts/actions/choices.ts b/src/scripts/actions/choices.ts index 7d027d7..7d6568d 100644 --- a/src/scripts/actions/choices.ts +++ b/src/scripts/actions/choices.ts @@ -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, }); diff --git a/src/scripts/actions/groups.ts b/src/scripts/actions/groups.ts index 44c0f42..9c4a24e 100644 --- a/src/scripts/actions/groups.ts +++ b/src/scripts/actions/groups.ts @@ -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, diff --git a/src/scripts/actions/items.ts b/src/scripts/actions/items.ts index 599080c..e7ec8a4 100644 --- a/src/scripts/actions/items.ts +++ b/src/scripts/actions/items.ts @@ -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, diff --git a/src/scripts/actions/misc.ts b/src/scripts/actions/misc.ts index 285aeb1..24a6f5a 100644 --- a/src/scripts/actions/misc.ts +++ b/src/scripts/actions/misc.ts @@ -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, }); diff --git a/src/scripts/choices.ts b/src/scripts/choices.ts index 1ec78b9..eddda52 100644 --- a/src/scripts/choices.ts +++ b/src/scripts/choices.ts @@ -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 | null; + customProperties?: Record; placeholder?: boolean; - keyCode?: number | null; + keyCode?: number; }): void { if (typeof value === 'undefined' || value === null) { return; diff --git a/src/scripts/components/wrapped-input.ts b/src/scripts/components/wrapped-input.ts index 6d1ccdb..888d4a4 100644 --- a/src/scripts/components/wrapped-input.ts +++ b/src/scripts/components/wrapped-input.ts @@ -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; } } diff --git a/src/scripts/constants.ts b/src/scripts/constants.ts index 56c306d..e833807 100644 --- a/src/scripts/constants.ts +++ b/src/scripts/constants.ts @@ -106,6 +106,8 @@ export const ACTION_TYPES: Record = { 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 = { diff --git a/src/scripts/interfaces.ts b/src/scripts/interfaces.ts index 9fe1f28..7bbbc23 100644 --- a/src/scripts/interfaces.ts +++ b/src/scripts/interfaces.ts @@ -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; }; diff --git a/src/scripts/lib/utils.ts b/src/scripts/lib/utils.ts index 2f0d14e..4972d28 100644 --- a/src/scripts/lib/utils.ts +++ b/src/scripts/lib/utils.ts @@ -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, diff --git a/src/scripts/reducers/choices.ts b/src/scripts/reducers/choices.ts index 5559514..9a666ab 100644 --- a/src/scripts/reducers/choices.ts +++ b/src/scripts/reducers/choices.ts @@ -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; }); diff --git a/src/scripts/reducers/general.ts b/src/scripts/reducers/general.ts index aaa1ca6..db149e2 100644 --- a/src/scripts/reducers/general.ts +++ b/src/scripts/reducers/general.ts @@ -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 { diff --git a/src/scripts/reducers/groups.ts b/src/scripts/reducers/groups.ts index d639a1a..c845e91 100644 --- a/src/scripts/reducers/groups.ts +++ b/src/scripts/reducers/groups.ts @@ -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, }, ]; } diff --git a/src/scripts/reducers/index.ts b/src/scripts/reducers/index.ts index 6345944..d4d5c7a 100644 --- a/src/scripts/reducers/index.ts +++ b/src/scripts/reducers/index.ts @@ -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 diff --git a/src/scripts/reducers/items.test.ts b/src/scripts/reducers/items.test.ts index 4041a3b..c695e73 100644 --- a/src/scripts/reducers/items.test.ts +++ b/src/scripts/reducers/items.test.ts @@ -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', () => { diff --git a/src/scripts/reducers/items.ts b/src/scripts/reducers/items.ts index 7aa4150..fb8432a 100644 --- a/src/scripts/reducers/items.ts +++ b/src/scripts/reducers/items.ts @@ -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;