Skip to content

Commit

Permalink
Cannot address both string or number
Browse files Browse the repository at this point in the history
Add `string` and `number` types support for built-in Base Components.

Instead of passing the event target value to the `onChange` functions we extract the values from the options passed to the component. 

This way we do not have to care about the type of the value. Internally the value of each option is cast into a `String`  for comparison, so values must still be stringable.
  • Loading branch information
widoz authored Feb 12, 2024
1 parent 96b9eb0 commit d625df0
Show file tree
Hide file tree
Showing 15 changed files with 125 additions and 40 deletions.
Binary file modified .yarn/install-state.gz
Binary file not shown.
1 change: 1 addition & 0 deletions @types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ declare namespace EntitiesSearch {
type Entities<V> = Set<V>;
type Kind<V> = Set<V>;
type Options<V> = Set<ControlOption<V>>;
type Value = string | number;

interface QueryArguments<V>
extends Partial<
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@
"yarn": "^1.22.19"
},
"dependencies": {
"@types/lodash": "^4",
"@wordpress/api-fetch": "~6.21.0",
"@wordpress/components": "~23.1.0",
"@wordpress/compose": "^6.23.0",
"@wordpress/core-data": "~6.12.0",
"@wordpress/hooks": "^3.49.0",
"@wordpress/i18n": "~4.24.0",
"classnames": "^2.3.2",
"lodash": "^4.17.21",
"react": "~18.2.0"
},
"scripts": {
Expand Down
33 changes: 19 additions & 14 deletions sources/client/src/components/plural-select-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Set } from '../vo/set';
import { NoOptionsMessage } from './no-options-message';

export function PluralSelectControl(
props: EntitiesSearch.BaseControl<string> & {
props: EntitiesSearch.BaseControl<EntitiesSearch.Value> & {
className?: string;
}
): JSX.Element {
Expand All @@ -17,6 +17,22 @@ export function PluralSelectControl(
'wz-select-control--plural'
);

const onChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
if (event.target.selectedOptions.length <= 0) {
props.onChange(new Set());
}

const selectedOptions = Array.from(event.target.selectedOptions).map(
(option) => option.value
);
const selectedValues = props.options
.filter((option) => selectedOptions.includes(String(option.value)))
.map((option) => option.value);

setSelected(selectedValues);
props.onChange(selectedValues);
};

if (props.options.length() <= 0) {
return <NoOptionsMessage />;
}
Expand All @@ -25,19 +41,8 @@ export function PluralSelectControl(
<select
multiple
className={className}
value={selected.toArray()}
onChange={(event) => {
if (event.target.selectedOptions.length <= 0) {
props.onChange(new Set());
}

const selectedValues = Array.from(
event.target.selectedOptions
).map((option) => option.value);

setSelected(new Set(selectedValues));
props.onChange(new Set(selectedValues));
}}
value={selected.toArray() as Array<string>}
onChange={onChange}
>
{props.options.map((option) => (
<option
Expand Down
21 changes: 17 additions & 4 deletions sources/client/src/components/radio-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,25 @@ import classnames from 'classnames';
import React, { JSX } from 'react';

export function RadioControl(
props: EntitiesSearch.SingularControl<string> & { className?: string }
props: EntitiesSearch.SingularControl<EntitiesSearch.Value> & {
className?: string;
}
): JSX.Element {
const className = classnames(props.className, 'wz-radio-control');

const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { target } = event;
const valueOption = props.options.find(
(option) => String(option.value) === target.value
);

if (!valueOption) {
return;
}

props.onChange(valueOption.value);
};

return (
<div className={className}>
{props.options.map((option) => (
Expand All @@ -22,9 +37,7 @@ export function RadioControl(
id={`wz-radio-control-item__input-${option.value}`}
checked={props.value === option.value}
value={option.value}
onChange={(event) =>
props.onChange(event.target.value)
}
onChange={onChange}
/>
{option.label}
</label>
Expand Down
19 changes: 14 additions & 5 deletions sources/client/src/components/singular-select-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,21 @@ export function SingularSelectControl(
return <NoOptionsMessage />;
}

const onChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const { target } = event;
const valueOption = props.options.find(
(option) => String(option.value) === target.value
);

if (!valueOption) {
return;
}

props.onChange(valueOption.value);
};

return (
<select
className={className}
value={props?.value}
onChange={(event) => props.onChange(event.target.value)}
>
<select className={className} value={props?.value} onChange={onChange}>
{props.options.map((option) => (
<option
key={option.value}
Expand Down
22 changes: 16 additions & 6 deletions sources/client/src/components/toggle-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { slugifyOptionLabel } from '../utils/slugify-option-label';
import { NoOptionsMessage } from './no-options-message';

export function ToggleControl(
props: EntitiesSearch.BaseControl<string> & { className?: string }
props: EntitiesSearch.BaseControl<EntitiesSearch.Value> & {
className?: string;
}
): JSX.Element {
const className = classnames(props.className, 'wz-toggle-control');

Expand All @@ -16,27 +18,35 @@ export function ToggleControl(

const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { target } = event;
const valueOption = props.options.find(
(option) => String(option.value) === target.value
);

if (!valueOption) {
return;
}

if (target.checked && !props.value.has(target.value)) {
props.onChange(props.value.add(target.value));
if (target.checked) {
props.onChange(props.value.add(valueOption.value));
return;
}

props.onChange(props.value.delete(target.value));
props.onChange(props.value.delete(valueOption.value));
};

return (
<div className={className}>
{props.options.map((option) => {
const value = String(option.value);
const id = idByControlOption(option);
return (
<div key={option.value} className="wz-toggle-control-item">
<div key={value} className="wz-toggle-control-item">
<label htmlFor={id}>
<input
type="checkbox"
id={id}
checked={props.value?.has(option.value)}
value={option.value}
value={value}
onChange={onChange}
/>
{option.label}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import { Set } from '../vo/set';
import { makeControlOption } from './make-control-option';

export function convertEntitiesToControlOptions<
V,
EntitiesFields extends { [p: string]: any }
>(
entities: Set<EntitiesFields>,
labelKey: string,
valueKey: string
): Set<EntitiesSearch.ControlOption<string>> {
): Set<EntitiesSearch.ControlOption<V>> {
return entities.map((entity) => {
const label = entity[labelKey];
const value = entity[valueKey];
labelKeyIsString(label);
return makeControlOption(label, String(value));
return makeControlOption(label, value);
});
}

Expand Down
18 changes: 14 additions & 4 deletions sources/client/src/vo/set.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isEqual as _isEqual } from 'lodash';

export class Set<T> {
readonly #data: ReadonlyArray<T>;

Expand All @@ -18,19 +20,19 @@ export class Set<T> {
return this;
}

return new Set(this.#data.filter((item) => item !== value));
return new Set(this.#data.filter((item) => !this.isEqual(item, value)));
}

public has(value: T): boolean {
return this.#data.includes(value);
return this.#data.some((current) => this.isEqual(current, value));
}

public map<R = T>(fn: (value: T) => R): Set<R> {
return new Set(this.#data.map(fn));
}

public toArray(): Array<T> {
return [...this.#data];
public toArray(): ReadonlyArray<T> {
return Object.freeze([...this.#data]);
}

public forEach(fn: (value: T) => void): void {
Expand All @@ -49,6 +51,10 @@ export class Set<T> {
return new Set(this.#data.filter(fn));
}

public find(fn: (value: T) => boolean): T | undefined {
return this.#data.slice(0).find(fn);
}

public first(): T | undefined {
return this.#data.slice(0)[0];
}
Expand Down Expand Up @@ -84,4 +90,8 @@ export class Set<T> {
yield value;
}
}

private isEqual(a: unknown, b: unknown): boolean {
return _isEqual(a, b);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ document.addEventListener('DOMContentLoaded', () => {
const {
Set,
searchEntities,
SingularSelectControl,
PluralSelectControl,
RadioControl,
ToggleControl,
SearchControl,
CompositeEntitiesByKind,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('EntitiesToggleControl', () => {
<ToggleControl
className="test-class"
options={options}
value={new Set()}
value={new Set([])}
onChange={onChange}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('Convert Entities To Control Options', () => {
'id'
).map((option) => option.value);
for (const entity of entities) {
expect(options.has(`${entity.id}`)).toEqual(true);
expect(options.has(entity.id)).toEqual(true);
}
});

Expand Down
29 changes: 26 additions & 3 deletions tests/client/unit/vo/set.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,35 @@ describe('Set', () => {
expect(set.add(obj).add(obj).length()).toBe(1);
});

it('Should add the same object again if it is a different reference', () => {
it('Should not add the same object again if it is a different reference', () => {
const obj = { a: 1 };
const obj2 = { a: 1 };
const set = new Set<any>();
expect(set.add(obj).add(obj2).length()).toBe(2);
});
expect(set.add(obj).add(obj2).length()).toBe(1);
});

it.each([
[[1, 2, 3, 4, 5], 3, true],
[[1, 2, 3, 4, 5], '4', false],
[['3', '4', '5'], 4, false],
[[{ a: 1 }, { b: 2 }, { c: 3 }], { b: 2 }, true],
[
[
{ label: 'Label 1', value: 1 },
{ label: 'Label 2', value: 2 },
{ label: 'Label 3', value: 3 },
],
{ label: 'Label 2', value: 2 },
true,
],
[[{ a: 1 }, { b: 2 }, { c: 3 }], 'b', false],
])(
'Should return true if a given value is the same in shape of an existing one',
(collection, given, expected) => {
const set = new Set<any>(collection);
expect(set.has(given)).toBe(expected);
}
);

it('Return the first element', () => {
const set = new Set<number>();
Expand Down
9 changes: 9 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3672,6 +3672,13 @@ __metadata:
languageName: node
linkType: hard

"@types/lodash@npm:^4":
version: 4.14.202
resolution: "@types/lodash@npm:4.14.202"
checksum: 6064d43c8f454170841bd67c8266cc9069d9e570a72ca63f06bceb484cb4a3ee60c9c1f305c1b9e3a87826049fd41124b8ef265c4dd08b00f6766609c7fe9973
languageName: node
linkType: hard

"@types/mime@npm:*":
version: 3.0.1
resolution: "@types/mime@npm:3.0.1"
Expand Down Expand Up @@ -16434,6 +16441,7 @@ __metadata:
"@testing-library/user-event": "npm:^14.5.1"
"@total-typescript/shoehorn": "npm:^0.1.0"
"@trivago/prettier-plugin-sort-imports": "npm:^4.0.0"
"@types/lodash": "npm:^4"
"@wordpress/api-fetch": "npm:~6.21.0"
"@wordpress/components": "npm:~23.1.0"
"@wordpress/compose": "npm:^6.23.0"
Expand All @@ -16448,6 +16456,7 @@ __metadata:
eslint-import-resolver-typescript: "npm:^3.5.5"
jest: "npm:^29.4.3"
jest-environment-jsdom: "npm:^29.5.0"
lodash: "npm:^4.17.21"
prettier: "npm:^2.8.1"
react: "npm:~18.2.0"
ts-jest: "npm:^29.0.5"
Expand Down

0 comments on commit d625df0

Please sign in to comment.