Skip to content
Open
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
6 changes: 5 additions & 1 deletion packages/core/src/resource/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@
export * from './api';
export {debounced} from './debounce';
export {resourceFromSnapshots} from './from_snapshots';
export {resource} from './resource';
export {
isInParamsFunction as ɵisInParamsFunction,
resource,
setInParamsFunction as ɵsetInParamsFunction,
} from './resource';
36 changes: 22 additions & 14 deletions packages/forms/signals/src/field/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import {
computed,
runInInjectionContext,
untracked,
ɵRuntimeError as RuntimeError,
untracked,
ɵisInParamsFunction,
ɵsetInParamsFunction,
} from '@angular/core';
import {MetadataKey} from '../api/rules/metadata';
import {RuntimeErrorCode} from '../errors';
Expand All @@ -34,20 +36,26 @@ export class FieldMetadataState {
return;
}

untracked(() =>
runInInjectionContext(this.node.structure.injector, () => {
for (const key of this.node.logicNode.logic.getMetadataKeys()) {
if (key.create) {
const logic = this.node.logicNode.logic.getMetadata(key);
const result = key.create!(
this.node,
computed(() => logic.compute(this.node.context)),
);
this.metadata.set(key, result);
const wasInParams = ɵisInParamsFunction();
if (wasInParams) ɵsetInParamsFunction(false);
Comment on lines +39 to +40

@JeanMeche JeanMeche Jul 2, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alternative I'm thiking of, is that would be done as part of untracked but it would make that function more load bearing than it should.

try {
untracked(() =>
runInInjectionContext(this.node.structure.injector, () => {
for (const key of this.node.logicNode.logic.getMetadataKeys()) {
if (key.create) {
const logic = this.node.logicNode.logic.getMetadata(key);
const result = key.create!(
this.node,
computed(() => logic.compute(this.node.context)),
);
this.metadata.set(key, result);
}
}
}
}),
);
}),
);
} finally {
if (wasInParams) ɵsetInParamsFunction(true);
}
}

/** Gets the value of an `MetadataKey` for the field. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import {ApplicationRef, computed, Injector, linkedSignal, signal} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import * as z from 'zod';
import {form, schema, validateStandardSchema} from '../../../../public_api';
import {form, schema, validateHttp, validateStandardSchema} from '../../../../public_api';

interface Flight {
id: number;
Expand Down Expand Up @@ -460,4 +460,28 @@ describe('standard schema integration', () => {
}),
]);
});

it('should work', () => {
const Schema = z.object({code: z.string().min(1)});

const injector = TestBed.inject(Injector);
const model = signal({code: ''});

function createForm() {
form(
model,
(p) => {
validateStandardSchema(p, Schema); // (A) schema validation
validateHttp(p.code, {
request: ({value}) => `/api/check?code=${value()}`,
onSuccess: () => null,
onError: () => null,
});
},
{injector},
);
}

expect(createForm).not.toThrow();
});
});
28 changes: 28 additions & 0 deletions packages/forms/signals/test/node/resource.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,4 +507,32 @@ describe('resources', () => {

expect(success).toBeTrue();
});

it('should not throw NG0992 when using validateAsync on parent and lazily-created child', () => {
const model = signal({child: ''});
const injector = TestBed.inject(Injector);
function createForm() {
form(
model,
(p) => {
validateAsync(p, {
params: () => 1,
factory: () => resource({loader: async () => []}),
onSuccess: () => null,
onError: () => null,
});

validateAsync(p.child, {
params: () => 1,
factory: () => resource({loader: async () => []}),
onSuccess: () => null,
onError: () => null,
});
},
{injector},
);
}

expect(() => createForm()).not.toThrow();
});
});
Loading