Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions packages/core/src/form/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { useRef } from 'react';
import { SugarInner } from '../sugar';
import { Sugar, SugarValue, SugarGetResult } from '../sugar/types';
import {
Sugar,
SugarValue,
SugarGetResult,
SugarTemplateState,
} from '../sugar/types';

export interface UseFormResult<T extends SugarValue> {
sugar: Sugar<T>;
Expand All @@ -10,7 +15,7 @@ export interface UseFormResult<T extends SugarValue> {
export const useForm = <T extends SugarValue>({
template,
}: {
template?: T;
template?: SugarTemplateState<T>;
} = {}): UseFormResult<T> => {
const sugar = useRef<Sugar<T>>(undefined);
if (!sugar.current) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ export { useForm } from './form';
export type { UseFormResult } from './form';
export { TextInput } from './components/textInput';
export { NumberInput } from './components/numberInput';
export type { Sugar } from './sugar/types';
export type { Sugar, SugarTemplateState } from './sugar/types';
27 changes: 23 additions & 4 deletions packages/core/src/sugar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
SugarSetResult,
SugarSetter,
SugarTemplateSetter,
SugarTemplateState,
SugarValue,
SugarValueObject,
} from './types';
Expand All @@ -17,6 +18,7 @@
ValidationStage,
FailFn,
} from './useValidation';
import { useIsPending, SugarUseIsPending } from './useIsPending';

export class SugarInner<T extends SugarValue> {
// Sugarは、get/setができるようになるまでに、Reactのレンダリングを待つ必要があります。
Expand Down Expand Up @@ -63,12 +65,12 @@
status: 'unavailable';
};

template: T | undefined;
template: SugarTemplateState<T>;
private validators: Set<
(stage: ValidationStage, value: T) => Promise<boolean>
> = new Set();

constructor(template?: T) {
constructor(template?: SugarTemplateState<T>) {
const { promise: getPromise, resolve: resolveGetPromise } =
Promise.withResolvers<SugarGetResult<T>>();
const { promise: setPromise, resolve: resolveSetPromise } =
Expand Down Expand Up @@ -155,7 +157,7 @@
}

setTemplate(value: T, executeSet = true): Promise<SugarSetResult<T>> {
this.template = value;
this.template = { status: 'resolved', value };

switch (this.status.status) {
case 'unavailable':
Expand All @@ -181,6 +183,20 @@
}
}

setPendingTemplate(): void {
this.template = { status: 'pending' };

Check warning on line 187 in packages/core/src/sugar/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/index.ts#L186-L187

Added lines #L186 - L187 were not covered by tests
if (this.status.status === 'ready' && this.status.templateSetter) {
this.status.templateSetter(undefined as T, false);

Check warning on line 189 in packages/core/src/sugar/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/index.ts#L189

Added line #L189 was not covered by tests
}
}

private getTemplateValue(): T | undefined {
if (this.template?.status === 'resolved') {
return this.template.value;
}
return undefined;
}

private eventTarget: EventTarget = new EventTarget();

addEventListener<K extends keyof SugarEvent>(
Expand Down Expand Up @@ -214,7 +230,7 @@
const status = this.status;
status.lock = true;

const initial = status.recentValue ?? this.template;
const initial = status.recentValue ?? this.getTemplateValue();
if (initial !== undefined) {
status.resolveSetPromise(await setter(initial));
}
Expand Down Expand Up @@ -273,4 +289,7 @@
deps?: React.DependencyList
) =>
useValidation(this as Sugar<T>, validator, deps)) as SugarUseValidation<T>;

useIsPending: SugarUseIsPending = (() =>
useIsPending(this as Sugar<T>)) as SugarUseIsPending;
}
7 changes: 7 additions & 0 deletions packages/core/src/sugar/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
export type SugarValue = unknown;
export type SugarValueObject = SugarValue & Record<string, SugarValue>;

export type SugarTemplateState<T extends SugarValue> =
| { status: 'pending' }
| { status: 'resolved'; value: T }
| undefined;

export type SugarGetResult<T extends SugarValue> =
| {
result: 'success';
Expand Down Expand Up @@ -34,6 +39,7 @@ export type SugarTemplateSetter<T extends SugarValue> = (

import type { SugarUseObject } from './useObject';
import type { SugarUseValidation } from './useValidation';
import type { SugarUseIsPending } from './useIsPending';

type SugarType<T extends SugarValue> = {
get: SugarGetter<T>;
Expand All @@ -47,6 +53,7 @@ type SugarType<T extends SugarValue> = {
destroy: () => void;
useObject: SugarUseObject<T>;
useValidation: SugarUseValidation<T>;
useIsPending: SugarUseIsPending;
addEventListener: <K extends keyof SugarEvent>(
type: K,
listener: CustomEventListener<SugarEvent[K]>
Expand Down
48 changes: 48 additions & 0 deletions packages/core/src/sugar/useIsPending.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useEffect, useState } from 'react';
import { SugarInner } from '.';
import { Sugar, SugarValue } from './types';

export type SugarUseIsPending = () => boolean;

export function useIsPending<T extends SugarValue>(sugar: Sugar<T>): boolean {
const [isPending, setIsPending] = useState<boolean>(() => {
const sugarInner = sugar as unknown as SugarInner<T>;
return sugarInner.template?.status === 'pending';
});

useEffect(() => {
const checkPendingState = () => {
const sugarInner = sugar as unknown as SugarInner<T>;
const newIsPending = sugarInner.template?.status === 'pending';
setIsPending(newIsPending);
};

checkPendingState();

const originalSetTemplate = sugar.setTemplate.bind(sugar);
sugar.setTemplate = async (value: T, executeSet = true) => {
const result = await originalSetTemplate(value, executeSet);
checkPendingState();
return result;
};

const sugarInner = sugar as unknown as SugarInner<T>;
const originalSetPendingTemplate =
sugarInner.setPendingTemplate?.bind(sugarInner);
if (originalSetPendingTemplate) {
sugarInner.setPendingTemplate = () => {
originalSetPendingTemplate();
checkPendingState();

Check warning on line 35 in packages/core/src/sugar/useIsPending.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/useIsPending.ts#L34-L35

Added lines #L34 - L35 were not covered by tests
};
}

return () => {
sugar.setTemplate = originalSetTemplate;
if (originalSetPendingTemplate) {
sugarInner.setPendingTemplate = originalSetPendingTemplate;
}
};
}, [sugar]);

return isPending;
}
118 changes: 95 additions & 23 deletions packages/core/src/sugar/useObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Sugar,
SugarGetResult,
SugarSetResult,
SugarTemplateState,
SugarValue,
SugarValueObject,
} from './types';
Expand Down Expand Up @@ -36,7 +37,33 @@
{
get: (target: Record<string, SugarInner<unknown>>, prop: string, _) => {
if (!(prop in target)) {
const s = new SugarInner((sugar as SugarInner<T>).template?.[prop]);
const parentTemplate = (sugar as SugarInner<T>).template;
let childTemplate: SugarTemplateState<unknown>;

if (parentTemplate?.status === 'pending') {
childTemplate = { status: 'pending' };
} else if (parentTemplate?.status === 'resolved') {
const parentValue = parentTemplate.value as Record<
string,
unknown
>;
if (
parentValue &&
typeof parentValue === 'object' &&
prop in parentValue
) {
childTemplate = {
status: 'resolved',
value: parentValue[prop],
};
} else {
childTemplate = undefined;

Check warning on line 60 in packages/core/src/sugar/useObject.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/useObject.ts#L59-L60

Added lines #L59 - L60 were not covered by tests
}
} else {
childTemplate = undefined;
}

const s = new SugarInner(childTemplate);
sugarInitializer.current.forEach((initializer) => {
s.addEventListener('change', initializer.dispatchChange);
s.addEventListener('blur', initializer.dispatchBlur);
Expand All @@ -63,6 +90,7 @@
sugar.addEventListener('change', dispatchChange);
sugar.addEventListener('blur', dispatchBlur);
});

sugarInitializer.current.push(initializer);

sugar.ready(
Expand Down Expand Up @@ -157,30 +185,74 @@
// return { result: 'unavailable' };
// }

const results: [string, SugarSetResult<unknown>][] = await Promise.all(
Object.entries(fields.current!).map(async ([key, s]) => {
if (key in value) {
const nestedValue = (value as Record<string, unknown>)[key];
const result = await s.setTemplate(nestedValue, executeSet);
return [key, result];
}
return [key, { result: 'success' as const }];
})
);
const parentTemplate = (sugar as SugarInner<T>).template;

const unavailables = results.filter(
([_, value]) => value.result === 'unavailable'
);
if (unavailables.length > 0) {
console.error(
`Setting template for useObject sugar: ${unavailables
.map(([key, _]) => key)
.join(', ')} is unavailable.`
if (parentTemplate?.status === 'pending') {
await Promise.all(
Object.entries(fields.current!).map(async ([_, s]) => {
(s as SugarInner<unknown>).setPendingTemplate();

Check warning on line 193 in packages/core/src/sugar/useObject.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/useObject.ts#L191-L193

Added lines #L191 - L193 were not covered by tests
})
);
return { result: 'unavailable' };
}
return { result: 'success' };

Check warning on line 196 in packages/core/src/sugar/useObject.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/useObject.ts#L196

Added line #L196 was not covered by tests
} else if (parentTemplate?.status === 'resolved') {
const parentValue = parentTemplate.value as Record<string, unknown>;
const results: [string, SugarSetResult<unknown>][] =
await Promise.all(
Object.entries(fields.current!).map(async ([key, s]) => {
if (
parentValue &&
typeof parentValue === 'object' &&
key in parentValue
) {
const result = await s.setTemplate(
parentValue[key],
executeSet
);
return [key, result];
}
return [key, { result: 'success' as const }];

Check warning on line 213 in packages/core/src/sugar/useObject.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/useObject.ts#L213

Added line #L213 was not covered by tests
})
);

return { result: 'success' };
const unavailables = results.filter(
([_, value]) => value.result === 'unavailable'
);
if (unavailables.length > 0) {
console.error(

Check warning on line 221 in packages/core/src/sugar/useObject.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/useObject.ts#L221

Added line #L221 was not covered by tests
`Setting template for useObject sugar: ${unavailables
.map(([key, _]) => key)

Check warning on line 223 in packages/core/src/sugar/useObject.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/useObject.ts#L223

Added line #L223 was not covered by tests
.join(', ')} is unavailable.`
);
return { result: 'unavailable' };

Check warning on line 226 in packages/core/src/sugar/useObject.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/useObject.ts#L226

Added line #L226 was not covered by tests
}

return { result: 'success' };
} else {

Check warning on line 230 in packages/core/src/sugar/useObject.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/useObject.ts#L230

Added line #L230 was not covered by tests
const results: [string, SugarSetResult<unknown>][] =
await Promise.all(
Object.entries(fields.current!).map(async ([key, s]) => {
const result = await s.setTemplate(

Check warning on line 234 in packages/core/src/sugar/useObject.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/useObject.ts#L232-L234

Added lines #L232 - L234 were not covered by tests
undefined as unknown,
executeSet
);
return [key, result];

Check warning on line 238 in packages/core/src/sugar/useObject.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/useObject.ts#L238

Added line #L238 was not covered by tests
})
);

const unavailables = results.filter(
([_, value]) => value.result === 'unavailable'

Check warning on line 243 in packages/core/src/sugar/useObject.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/useObject.ts#L242-L243

Added lines #L242 - L243 were not covered by tests
);
if (unavailables.length > 0) {
console.error(

Check warning on line 246 in packages/core/src/sugar/useObject.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/useObject.ts#L246

Added line #L246 was not covered by tests
`Setting template for useObject sugar: ${unavailables
.map(([key, _]) => key)

Check warning on line 248 in packages/core/src/sugar/useObject.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/useObject.ts#L248

Added line #L248 was not covered by tests
.join(', ')} is unavailable.`
);
return { result: 'unavailable' };

Check warning on line 251 in packages/core/src/sugar/useObject.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/useObject.ts#L251

Added line #L251 was not covered by tests
}

return { result: 'success' };

Check warning on line 254 in packages/core/src/sugar/useObject.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/sugar/useObject.ts#L254

Added line #L254 was not covered by tests
}
}
);

Expand All @@ -189,7 +261,7 @@
sugar.destroy();
if (fields.current) {
Object.values(fields.current).forEach((sugar) => {
sugar.removeEventListener('change', dispatchEvent);
sugar.removeEventListener('change', dispatchChange);
sugar.removeEventListener('blur', dispatchBlur);
});
sugarInitializer.current = sugarInitializer.current.filter(
Expand Down
10 changes: 6 additions & 4 deletions tests/core-unittest/src/collect.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { describeWithStrict } from '../util/describeWithStrict';
describeWithStrict('useForm#collect', () => {
test('collect method should be equivalent to sugar.get(true)', async () => {
const { result } = renderHook(() =>
useForm<string>({ template: 'initial' })
useForm({ template: { status: 'resolved', value: 'initial' } })
);

render(<TextInput sugar={result.current.sugar} />);
Expand All @@ -23,7 +23,9 @@ describeWithStrict('useForm#collect', () => {
});

test('collect method should trigger validation like sugar.get(true)', async () => {
const { result } = renderHook(() => useForm({ template: { a: '' } }));
const { result } = renderHook(() =>
useForm({ template: { status: 'resolved', value: { a: '' } } })
);
const { result: obj } = renderHook(() => result.current.sugar.useObject());

const validate = async (
Expand All @@ -48,8 +50,8 @@ describeWithStrict('useForm#collect', () => {

test('collect method should return the same type as sugar.get(true)', async () => {
const { result } = renderHook(() =>
useForm<{ name: string; age: number }>({
template: { name: 'John', age: 25 },
useForm({
template: { status: 'resolved', value: { name: 'John', age: 25 } },
})
);
const { result: obj } = renderHook(() => result.current.sugar.useObject());
Expand Down
8 changes: 6 additions & 2 deletions tests/core-unittest/src/component.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ type Component = {
describeWithStrict('Component requirements', () => {
describe.each<Component>(Components)('$name', (c) => {
test('Component should be ready after render', async () => {
const { result } = renderHook(() => useForm({ template: c.template }));
const { result } = renderHook(() =>
useForm({ template: { status: 'resolved', value: c.template } })
);
const get = result.current.sugar.get();

expect(await checkPending(get)).toStrictEqual({ resolved: false });
Expand All @@ -39,7 +41,9 @@ describeWithStrict('Component requirements', () => {
});

test('Sugar should be destroyed after unmount', async () => {
const { result } = renderHook(() => useForm({ template: c.template }));
const { result } = renderHook(() =>
useForm({ template: { status: 'resolved', value: c.template } })
);
const { unmount } = render(<c.Component sugar={result.current.sugar} />);
await expect(result.current.sugar.get()).resolves.toStrictEqual({
result: 'success',
Expand Down
Loading