Skip to content
Closed
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
4 changes: 2 additions & 2 deletions packages/forms/signals/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ export * from './src/api/rules/validation/validation_errors';
export * from './src/api/structure';
export * from './src/api/transformed_value';
export * from './src/api/types';
export * from './src/directive/form_field_directive';
export * from './src/directive/ng_signal_form';
export * from './src/directive/form_field';
export * from './src/directive/form_root';
2 changes: 1 addition & 1 deletion packages/forms/signals/src/api/control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {InputSignal, InputSignalWithTransform, ModelSignal, OutputRef} from '@angular/core';
import type {FormFieldBindingOptions} from '../directive/form_field_directive';
import type {FormFieldBindingOptions} from '../directive/form_field';
import type {ValidationError, WithOptionalFieldTree} from './rules/validation/validation_errors';
import type {DisabledReason} from './types';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/

import type {FormField} from '../../../directive/form_field_directive';
import type {FormField} from '../../../directive/form_field';
import type {ReadonlyFieldTree} from '../../types';
import type {StandardSchemaValidationError} from './standard_schema';

Expand Down
2 changes: 1 addition & 1 deletion packages/forms/signals/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import {Injector, Signal, WritableSignal} from '@angular/core';
import {AbstractControl} from '@angular/forms';
import type {FormField} from '../directive/form_field_directive';
import type {FormField} from '../directive/form_field';
import type {MetadataKey, ValidationError} from './rules';

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/forms/signals/src/directive/control_custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import type {ɵControlDirectiveHost as ControlDirectiveHost} from '@angular/core';
import type {FormField} from './form_field_directive';
import type {FormField} from './form_field';
import {
bindingUpdated,
CONTROL_BINDING_NAMES,
Expand Down
2 changes: 1 addition & 1 deletion packages/forms/signals/src/directive/control_cva.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
readFieldStateBindingValue,
} from './bindings';
import {setNativeDomProperty} from './native';
import type {FormField} from './form_field_directive';
import type {FormField} from './form_field';

export function cvaControlCreate(
host: ControlDirectiveHost,
Expand Down
2 changes: 1 addition & 1 deletion packages/forms/signals/src/directive/control_native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
readFieldStateBindingValue,
type ControlBindingKey,
} from './bindings';
import type {FormField} from './form_field_directive';
import type {FormField} from './form_field';
import {InputValidityMonitor} from './input_validity_monitor';
import {
getNativeControlValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@
* found in the LICENSE file at https://angular.dev/license
*/

import {Directive, input} from '@angular/core';
import {Directive, input, untracked} from '@angular/core';

import {submit} from '../api/structure';
import {FieldTree} from '../api/types';
import {FieldState, FieldTree} from '../api/types';
import {FieldNode} from '../field/node';

/**
* A directive that binds a `FieldTree` to a `<form>` element.
*
* It automatically:
* 1. Sets `novalidate` on the form element to disable browser validation.
* 2. Listens for the `submit` event, prevents the default behavior, and calls `submit()` on the
* `FieldTree`.
* `FieldTree` if it defines its own submission options.
*
* @usageNotes
*
Expand All @@ -42,6 +43,14 @@ export class FormRoot<T> {

protected onSubmit(event: Event): void {
event.preventDefault();
submit(this.fieldTree());

untracked(() => {
const fieldTree = this.fieldTree();
const node = fieldTree() as FieldState<unknown> as FieldNode;

if (node.structure.fieldManager.submitOptions) {
submit(fieldTree);
}
});
}
}
2 changes: 1 addition & 1 deletion packages/forms/signals/src/field/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import type {
FieldTree,
MarkAsTouchedOptions,
} from '../api/types';
import type {FormField} from '../directive/form_field_directive';
import type {FormField} from '../directive/form_field';
import {DYNAMIC} from '../schema/logic';
import {LogicNode} from '../schema/logic_node';
import {FieldPathNode} from '../schema/path_node';
Expand Down
2 changes: 1 addition & 1 deletion packages/forms/signals/src/field/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {computed, signal, Signal} from '@angular/core';
import type {FormField} from '../directive/form_field_directive';
import type {FormField} from '../directive/form_field';
import type {Debouncer, DisabledReason} from '../api/types';
import {DEBOUNCER} from './debounce';
import type {FieldNode} from './node';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('FormRoot', () => {
expect(formElement.hasAttribute('novalidate')).toBeTrue();
});

it('should call submit on the field tree when form is submitted', async () => {
it('should call submit if the field tree defines submit options', async () => {
const fixture = act(() => TestBed.createComponent(TestCmp));
const component = fixture.componentInstance;
const formElement = fixture.nativeElement.querySelector('form') as HTMLFormElement;
Expand All @@ -53,9 +53,34 @@ describe('FormRoot', () => {
act(() => formElement.dispatchEvent(event));

expect(event.defaultPrevented).toBe(true);
expect(component.f().touched()).toBeTrue();
expect(component.submitted).toBeTrue();
});

it('should not call submit if the field tree does not define submit options', async () => {
@Component({
template: `
<form [formRoot]="f">
<button type="submit">Submit</button>
</form>
`,
imports: [FormRoot],
})
class TestCmpNoSubmit {
readonly f = form(signal({}));
}

const fixture = act(() => TestBed.createComponent(TestCmpNoSubmit));
const component = fixture.componentInstance;
const formElement = fixture.nativeElement.querySelector('form') as HTMLFormElement;

const event = new Event('submit', {cancelable: true});
act(() => formElement.dispatchEvent(event));

expect(event.defaultPrevented).toBe(true);
expect(component.f().touched()).withContext('submit would mark this as touched').toBeFalse();
});

it('works when FormsModule is imported', () => {
@Component({
template: `
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {disabled} from '@angular/forms/signals';

import {SignalFormControl} from '../../compat';
import {FormField} from '../../src/directive/form_field_directive';
import {FormField} from '../../src/directive/form_field';

describe('SignalFormControl (web)', () => {
beforeEach(() => {
Expand Down
Loading