Skip to content

feat(forms): resource and loader validation - #71613

Open
benjavicente wants to merge 2 commits into
angular:mainfrom
benjavicente:feat/forms-resource-and-loader-validation
Open

benjavicente wants to merge 2 commits into
angular:mainfrom
benjavicente:feat/forms-resource-and-loader-validation

Conversation

@benjavicente

Copy link
Copy Markdown

PR Checklist

  • The commit message follows our guidelines
  • Tests have been added
  • Docs have been added or updated

PR Type

  • Feature
  • Documentation content changes

What is the current behavior?

Issue: #70884
Related discussion: #71414 (comment)

When debounce is enabled, validateAsync calls ɵchain internally and supplies the factory with a params signal that can throw resource status markers.

This implicitly relies on the factory reading that signal inside a core resource's params callback, where those markers are handled. Implementing the Resource interface alone does not provide that behavior.

The public chain API makes this composition explicit inside resource params, but validateAsync hides it from consumers.

validateAsync(schemaPath.username, {
  params: ({value}) => value(),
  debounce: 300,
  factory: (params) =>
    resource({
      // This read must happen inside resource params because the signal
      // implicitly chains the debounced resource.
      params: () => params(),
      loader: ({params}) => checkUsername(params),
    }),
  onSuccess: (available) => (available ? undefined : {kind: 'usernameTaken'}),
  onError: () => ({kind: 'serverError'}),
});

This does not work but types pass, no documentation says it should not work:

validateAsync(schemaPath.username, {
  params: ({value}) => value(),
  debounce: 300,
  factory: (params) => {
    const username$ = toObservable(params);

    return rxResource({
      stream: () =>
        username$.pipe(
          switchMap((username) =>
            username === undefined ? of(undefined) : checkUsername(username),
          ),
        ),
    });
  },
  onSuccess: (available) => (available ? undefined : {kind: 'usernameTaken'}),
  onError: () => ({kind: 'serverError'}),
});

Usually, any API that read the params directly assuming it is a simple signal that will not throw, will fail for validateAsyncs that are debounced.

What is the new behavior?

validateResource

The factory receives a params resource instead of an implicitly chained signal. Consumers can explicitly compose it using the resource API:

validateResource(schemaPath.username, {
  params: ({value}) => value(),
  debounce: 300,
  factory: (paramsResource) =>
    resource({
      // Chaining is explicit and happens inside resource params.
      params: ({chain}) => chain(paramsResource),
      loader: ({params}) => checkUsername(params),
    }),
  onSuccess: (available) => (available ? undefined : {kind: 'usernameTaken'}),
  onError: () => ({kind: 'serverError'}),
});

The validator composes the params resource with the returned resource, preserving validation's pending, idle, and error states.

validateAsyncLoader

For async functions, consumers can provide a loader directly without creating a resource or repeating the params callback:

validateAsyncLoader(schemaPath.username, {
  params: ({value}) => value(),
  debounce: 300,
  loader: ({params}) => checkUsername(params),
  onSuccess: (available) => (available ? undefined : {kind: 'usernameTaken'}),
  onError: () => ({kind: 'serverError'}),
});

settledResourceValue

This helper exposes a resource's settled value as a signal. It returns the value for resolved and local states, and undefined for other states.

This makes resource values easier to compose with utilities that accept ordinary reactive values. For example:

validateResource(schemaPath.username, {
  params: ({value}) => value(),
  debounce: 300,
  factory: (paramsResource) => {
    const value = settledResourceValue(paramsResource);
    const result = injectFetch(() => {
      const username = value();
      return {
        value: username,
        disabled: username === undefined,
      };
    });

    // The adapter returns an implementation of the Resource interface.
    return toResource(result);
  },
  onSuccess: (available) => (available ? undefined : {kind: 'usernameTaken'}),
  onError: () => ({kind: 'serverError'}),
});

validateResource preserves the params resource's pending and error states even when the adapted resource is disabled while params are unavailable.

validateAsync

validateAsync is deprecated without changing its existing runtime behavior. Standard Schema validation and the guide examples are updated to use the new APIs.

It is marked as deprecated since it is using chain internally in a way it can end up unexpected to consumers.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Feel free to take over the PR/branch. AI assisted code. This is the "ideal" API I see for validation with promises and resources that keep the shape close to the current API. The APIs changes are sorted by priority, the important change needed to solve the issue is validateResource, the other API changes could be reverted.

Expose settledResourceValue to read completed resource values as a signal
when integrating resource state with value-based reactive APIs.

Expose the existing fatal resource error policy internally so Forms can
preserve programming errors when adapting validation params to snapshots.
Separate resource factories from async loaders so validation can compose
resource state without exposing implicit chain markers through a signal.

Add validateResource and validateAsyncLoader, preserve pending and error
state across debounce transitions, and delegate reloads to the resource.
Migrate Standard Schema validation and update the guides and public APIs.
Cover validation gating, errors, reloads, request replacement, cleanup,
and compatibility with the existing validator.

DEPRECATED: deprecate validateAsync in Signal Forms

Use validateAsyncLoader for async functions or validateResource for resource
factories. The validateResource factory receives params as a Resource
instead of a Signal. Preserve the existing validateAsync runtime behavior.
@pullapprove
pullapprove Bot requested a review from kirjs October 25, 2026 21:00
@angular-robot angular-robot Bot added detected: deprecation PR contains a commit with a deprecation detected: feature PR contains a feature commit area: core Issues related to the framework runtime area: forms labels Oct 25, 2026
@ngbot ngbot Bot added this to the Backlog milestone Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: core Issues related to the framework runtime area: forms detected: deprecation PR contains a commit with a deprecation detected: feature PR contains a feature commit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant