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 { ACTION_TYPES } from '../constants';
import { Choice } from '../interfaces'; 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 = ({ export const addChoice = ({
value, value,
label, label,
@ -12,7 +38,7 @@ export const addChoice = ({
customProperties, customProperties,
placeholder, placeholder,
keyCode, keyCode,
}): Action & Choice => ({ }): AddChoiceAction => ({
type: ACTION_TYPES.ADD_CHOICE, type: ACTION_TYPES.ADD_CHOICE,
value, value,
label, label,
@ -25,20 +51,16 @@ export const addChoice = ({
keyCode, keyCode,
}); });
export const filterChoices = ( export const filterChoices = (results: Choice[]): FilterChoicesAction => ({
results: Choice[],
): Action & { results: Choice[] } => ({
type: ACTION_TYPES.FILTER_CHOICES, type: ACTION_TYPES.FILTER_CHOICES,
results, results,
}); });
export const activateChoices = ( export const activateChoices = (active = true): ActivateChoicesAction => ({
active = true,
): Action & { active: boolean } => ({
type: ACTION_TYPES.ACTIVATE_CHOICES, type: ACTION_TYPES.ACTIVATE_CHOICES,
active, active,
}); });
export const clearChoices = (): Action => ({ export const clearChoices = (): ClearChoicesAction => ({
type: ACTION_TYPES.CLEAR_CHOICES, type: ACTION_TYPES.CLEAR_CHOICES,
}); });

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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