mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(forms): add onError callback to validateHttp for HTTP errors
Adds onError callback inside validateHttp validator in signal forms
PR-Close: #63949
(cherry picked from commit a5678f6f2b)
This commit is contained in:
parent
04dd75ba94
commit
bd780366e8
6 changed files with 91 additions and 29 deletions
|
|
@ -61,8 +61,9 @@ export type AsyncValidationResult<E extends ValidationError = ValidationError> =
|
|||
|
||||
// @public
|
||||
export interface AsyncValidatorOptions<TValue, TParams, TResult, TPathKind extends PathKind = PathKind.Root> {
|
||||
readonly errors: MapToErrorsFn<TValue, TResult, TPathKind>;
|
||||
readonly factory: (params: Signal<TParams | undefined>) => ResourceRef<TResult | undefined>;
|
||||
readonly onError: (error: unknown, ctx: FieldContext<TValue, TPathKind>) => TreeValidationResult;
|
||||
readonly onSuccess: MapToErrorsFn<TValue, TResult, TPathKind>;
|
||||
readonly params: (ctx: FieldContext<TValue, TPathKind>) => TParams;
|
||||
}
|
||||
|
||||
|
|
@ -233,7 +234,8 @@ export function hidden<TValue, TPathKind extends PathKind = PathKind.Root>(path:
|
|||
|
||||
// @public
|
||||
export interface HttpValidatorOptions<TValue, TResult, TPathKind extends PathKind = PathKind.Root> {
|
||||
readonly errors: MapToErrorsFn<TValue, TResult, TPathKind>;
|
||||
readonly onError: (error: unknown, ctx: FieldContext<TValue, TPathKind>) => TreeValidationResult;
|
||||
readonly onSuccess: MapToErrorsFn<TValue, TResult, TPathKind>;
|
||||
readonly options?: HttpResourceOptions<TResult, unknown>;
|
||||
readonly request: ((ctx: FieldContext<TValue, TPathKind>) => string | undefined) | ((ctx: FieldContext<TValue, TPathKind>) => HttpResourceRequest | undefined);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,11 @@ export interface AsyncValidatorOptions<
|
|||
* @returns A reference to the constructed resource.
|
||||
*/
|
||||
readonly factory: (params: Signal<TParams | undefined>) => ResourceRef<TResult | undefined>;
|
||||
|
||||
/**
|
||||
* A function to handle errors thrown by httpResource (HTTP errors, network errors, etc.).
|
||||
* Receives the error and the field context, returns a list of validation errors.
|
||||
*/
|
||||
readonly onError: (error: unknown, ctx: FieldContext<TValue, TPathKind>) => TreeValidationResult;
|
||||
/**
|
||||
* A function that takes the resource result, and the current field context and maps it to a list
|
||||
* of validation errors.
|
||||
|
|
@ -83,7 +87,7 @@ export interface AsyncValidatorOptions<
|
|||
* A targeted error will show up as an error on its target field rather than the field being validated.
|
||||
* If a field is not given, the error is assumed to apply to the field being validated.
|
||||
*/
|
||||
readonly errors: MapToErrorsFn<TValue, TResult, TPathKind>;
|
||||
readonly onSuccess: MapToErrorsFn<TValue, TResult, TPathKind>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -120,8 +124,13 @@ export interface HttpValidatorOptions<TValue, TResult, TPathKind extends PathKin
|
|||
* A targeted error will show up as an error on its target field rather than the field being validated.
|
||||
* If a field is not given, the error is assumed to apply to the field being validated.
|
||||
*/
|
||||
readonly errors: MapToErrorsFn<TValue, TResult, TPathKind>;
|
||||
readonly onSuccess: MapToErrorsFn<TValue, TResult, TPathKind>;
|
||||
|
||||
/**
|
||||
* A function to handle errors thrown by httpResource (HTTP errors, network errors, etc.).
|
||||
* Receives the error and the field context, returns a list of validation errors.
|
||||
*/
|
||||
readonly onError: (error: unknown, ctx: FieldContext<TValue, TPathKind>) => TreeValidationResult;
|
||||
/**
|
||||
* The options to use when creating the httpResource.
|
||||
*/
|
||||
|
|
@ -163,6 +172,7 @@ export function validateAsync<TValue, TParams, TResult, TPathKind extends PathKi
|
|||
|
||||
pathNode.logic.addAsyncErrorRule((ctx) => {
|
||||
const res = ctx.state.metadata(RESOURCE)!;
|
||||
let errors;
|
||||
switch (res.status()) {
|
||||
case 'idle':
|
||||
return undefined;
|
||||
|
|
@ -174,11 +184,11 @@ export function validateAsync<TValue, TParams, TResult, TPathKind extends PathKi
|
|||
if (!res.hasValue()) {
|
||||
return undefined;
|
||||
}
|
||||
const errors = opts.errors(res.value()!, ctx as FieldContext<TValue, TPathKind>);
|
||||
errors = opts.onSuccess(res.value()!, ctx as FieldContext<TValue, TPathKind>);
|
||||
return addDefaultField(errors, ctx.field);
|
||||
case 'error':
|
||||
// TODO: Design error handling for async validation. For now, just throw the error.
|
||||
throw res.error();
|
||||
errors = opts.onError(res.error(), ctx as FieldContext<TValue, TPathKind>);
|
||||
return addDefaultField(errors, ctx.field);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -203,6 +213,7 @@ export function validateHttp<TValue, TResult = unknown, TPathKind extends PathKi
|
|||
validateAsync(path, {
|
||||
params: opts.request,
|
||||
factory: (request: Signal<any>) => httpResource(request, opts.options),
|
||||
errors: opts.errors,
|
||||
onSuccess: opts.onSuccess,
|
||||
onError: opts.onError,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,9 +87,10 @@ export function validateStandardSchema<TSchema, TValue extends IgnoreUnknownProp
|
|||
loader: async ({params}) => (await params)?.issues ?? [],
|
||||
});
|
||||
},
|
||||
errors: (issues, {fieldOf}) => {
|
||||
onSuccess: (issues, {fieldOf}) => {
|
||||
return issues.map((issue) => standardIssueToFormTreeError(fieldOf(path), issue));
|
||||
},
|
||||
onError: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ describe('resources', () => {
|
|||
return params as Cat[];
|
||||
},
|
||||
}),
|
||||
errors: (cats, {fieldOf}) => {
|
||||
onSuccess: (cats, {fieldOf}) => {
|
||||
return cats.map((cat, index) =>
|
||||
customError({
|
||||
kind: 'meows_too_much',
|
||||
|
|
@ -170,6 +170,7 @@ describe('resources', () => {
|
|||
}),
|
||||
);
|
||||
},
|
||||
onError: () => null,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -196,13 +197,14 @@ describe('resources', () => {
|
|||
return params as Cat[];
|
||||
},
|
||||
}),
|
||||
errors: (cats, {fieldOf}) => {
|
||||
onSuccess: (cats, {fieldOf}) => {
|
||||
return customError({
|
||||
kind: 'meows_too_much',
|
||||
name: cats[0].name,
|
||||
field: fieldOf(p)[0],
|
||||
});
|
||||
},
|
||||
onError: () => null,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -222,8 +224,9 @@ describe('resources', () => {
|
|||
(p) => {
|
||||
validateHttp(p, {
|
||||
request: ({value}) => `/api/check?username=${value()}`,
|
||||
errors: (available: boolean) =>
|
||||
onSuccess: (available: boolean) =>
|
||||
available ? undefined : customError({kind: 'username-taken'}),
|
||||
onError: () => null,
|
||||
});
|
||||
},
|
||||
{injector},
|
||||
|
|
@ -266,8 +269,9 @@ describe('resources', () => {
|
|||
required(address.street);
|
||||
validateHttp(address, {
|
||||
request: ({value}) => ({url: '/checkaddress', params: {...value()}}),
|
||||
errors: (message: string, {fieldOf}) =>
|
||||
onSuccess: (message: string, {fieldOf}) =>
|
||||
customError({message, field: fieldOf(address.street)}),
|
||||
onError: () => null,
|
||||
});
|
||||
});
|
||||
const addressForm = form(addressModel, addressSchema, {injector});
|
||||
|
|
@ -286,4 +290,36 @@ describe('resources', () => {
|
|||
customError({message: 'Invalid!', field: addressForm.street}),
|
||||
]);
|
||||
});
|
||||
it('should call onError handler when http validation fails', async () => {
|
||||
const addressModel = signal<Address>({street: '123 Main St', city: '', zip: ''});
|
||||
const addressSchema = schema<Address>((address) => {
|
||||
required(address.street);
|
||||
validateHttp(address, {
|
||||
request: ({value}) => ({url: '/checkaddress', params: {...value()}}),
|
||||
onSuccess: () => undefined,
|
||||
onError: () => [
|
||||
customError({kind: 'address-api-failed', message: 'API is down', field: addressForm}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const addressForm = form(addressModel, addressSchema, {injector});
|
||||
|
||||
TestBed.tick();
|
||||
|
||||
const req = backend.expectOne('/checkaddress?street=123%20Main%20St&city=&zip=');
|
||||
req.flush(null, {status: 500, statusText: 'Server Error'});
|
||||
|
||||
await appRef.whenStable();
|
||||
|
||||
expect(addressForm().pending()).toBe(false);
|
||||
expect(addressForm().invalid()).toBe(true);
|
||||
expect(addressForm().errors()).toEqual([
|
||||
customError({
|
||||
kind: 'address-api-failed',
|
||||
message: 'API is down',
|
||||
field: addressForm,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ describe('submit', () => {
|
|||
params,
|
||||
loader: () => resolvers.promise,
|
||||
}),
|
||||
errors: () => {},
|
||||
onSuccess: () => {},
|
||||
onError: () => {},
|
||||
});
|
||||
},
|
||||
{injector: TestBed.inject(Injector)},
|
||||
|
|
|
|||
|
|
@ -188,7 +188,8 @@ describe('validation status', () => {
|
|||
setTimeout(() => r(validateValueForChild(params, undefined))),
|
||||
),
|
||||
})),
|
||||
errors: (errs) => errs,
|
||||
onSuccess: (results) => results,
|
||||
onError: () => null,
|
||||
});
|
||||
},
|
||||
{injector},
|
||||
|
|
@ -233,11 +234,12 @@ describe('validation status', () => {
|
|||
setTimeout(() => r(validateValueForChild(params, undefined))),
|
||||
),
|
||||
})),
|
||||
errors: (errs, {fieldOf}) =>
|
||||
errs.map((e) => ({
|
||||
onSuccess: (results, {fieldOf}) =>
|
||||
results.map((e) => ({
|
||||
...e,
|
||||
field: fieldOf(p.child),
|
||||
})),
|
||||
onError: () => null,
|
||||
});
|
||||
},
|
||||
{injector},
|
||||
|
|
@ -282,11 +284,12 @@ describe('validation status', () => {
|
|||
setTimeout(() => r(validateValueForChild(params, undefined))),
|
||||
),
|
||||
})),
|
||||
errors: (errs, {fieldOf}) =>
|
||||
errs.map((e) => ({
|
||||
onSuccess: (results, {fieldOf}) =>
|
||||
results.map((e) => ({
|
||||
...e,
|
||||
field: fieldOf(p.child),
|
||||
})),
|
||||
onError: () => null,
|
||||
});
|
||||
},
|
||||
{injector},
|
||||
|
|
@ -334,11 +337,12 @@ describe('validation status', () => {
|
|||
setTimeout(() => r(validateValueForChild(params, undefined))),
|
||||
),
|
||||
})),
|
||||
errors: (errs, {fieldOf}) =>
|
||||
errs.map((e) => ({
|
||||
onSuccess: (results, {fieldOf}) =>
|
||||
results.map((e) => ({
|
||||
...e,
|
||||
field: fieldOf(p.child),
|
||||
})),
|
||||
onError: () => null,
|
||||
});
|
||||
},
|
||||
{injector},
|
||||
|
|
@ -383,7 +387,8 @@ describe('validation status', () => {
|
|||
setTimeout(() => r(validateValueForChild(params, undefined))),
|
||||
),
|
||||
})),
|
||||
errors: (errs) => errs,
|
||||
onSuccess: (results) => results,
|
||||
onError: () => null,
|
||||
});
|
||||
},
|
||||
{injector},
|
||||
|
|
@ -443,7 +448,8 @@ describe('validation status', () => {
|
|||
params,
|
||||
loader: () => new Promise<ValidationError[]>((r) => setTimeout(() => r([]))),
|
||||
})),
|
||||
errors: (errs) => errs,
|
||||
onSuccess: (results) => results,
|
||||
onError: () => null,
|
||||
});
|
||||
},
|
||||
{injector},
|
||||
|
|
@ -470,7 +476,8 @@ describe('validation status', () => {
|
|||
params,
|
||||
loader: () => promise,
|
||||
})),
|
||||
errors: (errs) => errs,
|
||||
onSuccess: (results) => results,
|
||||
onError: () => null,
|
||||
});
|
||||
validateAsync(p, {
|
||||
params: () => [],
|
||||
|
|
@ -479,7 +486,8 @@ describe('validation status', () => {
|
|||
params,
|
||||
loader: () => promise2,
|
||||
})),
|
||||
errors: (errs) => errs,
|
||||
onSuccess: (results) => results,
|
||||
onError: () => null,
|
||||
});
|
||||
},
|
||||
{injector},
|
||||
|
|
@ -514,7 +522,8 @@ describe('validation status', () => {
|
|||
params,
|
||||
loader: () => invalidPromise,
|
||||
})),
|
||||
errors: (errs) => errs,
|
||||
onSuccess: (results) => results,
|
||||
onError: () => null,
|
||||
});
|
||||
validateAsync(p, {
|
||||
params: () => [],
|
||||
|
|
@ -523,7 +532,8 @@ describe('validation status', () => {
|
|||
params,
|
||||
loader: () => validPromise,
|
||||
})),
|
||||
errors: (errs) => errs,
|
||||
onSuccess: (results) => results,
|
||||
onError: () => null,
|
||||
});
|
||||
validateAsync(p, {
|
||||
params: () => [],
|
||||
|
|
@ -532,7 +542,8 @@ describe('validation status', () => {
|
|||
params,
|
||||
loader: () => pendingPromise,
|
||||
})),
|
||||
errors: (errs) => errs,
|
||||
onSuccess: (results) => results,
|
||||
onError: () => null,
|
||||
});
|
||||
},
|
||||
{injector},
|
||||
|
|
|
|||
Loading…
Reference in a new issue