71

I have a simple form that looks like this

<form (ngSubmit)="save()" #documentEditForm="ngForm">
...
</form>

and need to submit the the form and check its validity from outside

eg. Either submit it programatically, or with a <button type="submit"> that is outside the <form> tags.

Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
Philipp
  • 4,180
  • 7
  • 27
  • 41

9 Answers9

113

You can link the button to the form using the form attribute on the button:

<form (ngSubmit)="save()" id="ngForm" #documentEditForm="ngForm"> 
  ... 
</form>

<button form="ngForm">
  SAVE
</button>

You can still check its validity like this:

<button form="ngForm" [disabled]="!documentEditForm.form.valid">
  SAVE
</button>

The form needs to have an ID id="example-form" and the submit button a matching ID in the form="example-form"

See here for more details: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-form

Yodacheese
  • 4,787
  • 5
  • 34
  • 42
  • 4
    I just want to add one more thing. It seems like angular doesn't care about the button being disabled once its outside the form - i.e. if you press enter on the an input element it will still try to submit it. I'm not sure if this is really a bug in Angular itself or there is another solution. A quick workaround for it would be just to go (ngSubmit)="form.valid && save()" – Yodacheese Feb 22 '17 at 00:31
  • 3
    fyi IE & Edge don't support the 'form' attribute on 'button', so this won't work there (afaik). – bob Jun 13 '17 at 09:49
  • Who's to say that the User Experience demands a GUI button? Think about a _"Text-Verification" Form_. Many of these submit the form as soon as the text-length reaches, say, `6` digits. You're still in the same boat if you, want to hide the button and trigger a `click` on that button. _You need some way to directly invoke the `.submit()` or `.ngSubmit.emit()` method(s)_. **But I like that you provided a solid, straightforward solution, so, thx :)** – Cody Sep 07 '17 at 18:21
  • 3
    `documentEditForm` is not defined yet! – Amirhossein Mehrvarzi Mar 31 '18 at 21:27
  • 2
    @bob Edge supports this from Edge 16 ([see MDN on compatibility](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Browser_compatibility) details) – Wilt Nov 17 '20 at 11:31
  • Did not work for me even after trying multiple variations. The text seems to indicate `id` of form and `form` value of button can be set arbitrarily, but uses `ngForm` for these, which is a key word and confusing. – Jason May 11 '21 at 17:15
96

Found out how to do it:

  • trigger submit with <formname>.ngSubmit.emit()
  • get form status with <formname>.form.valid

Example:

<form (ngSubmit)="save()" #documentEditForm="ngForm">
...
</form>

<button class="btn-save button primary"
(click)="documentEditForm.ngSubmit.emit()"
[disabled]="!documentEditForm.form.valid">SAVE</button>

Edit: As @yuriy-yakovenko has pointed out, you should add in your component code the following:

@ViewChild('documentEditForm') documentEditForm: FormGroupDirective; 

And don't forget to import the FormGroupDirective if you haven't done yet

pakobill
  • 416
  • 4
  • 11
Philipp
  • 4,180
  • 7
  • 27
  • 41
  • 6
    ngSubmit is undefined – Yuriy Yakovenko Jul 12 '17 at 10:50
  • 3
    @VievChild('documentEditForm') documentEditForm: FormGroupDirective; also needed – Yuriy Yakovenko Jul 12 '17 at 11:34
  • 7
    If I call emit then also my ngForm's submited (Boolean property) is not changed to true. Can you help please. – Partha Sarathi Ghosh Aug 03 '17 at 15:08
  • 1
    Yes, I have the same issue as @ParthaSarathiGhosh – AntonK Oct 05 '17 at 04:04
  • I found this question here on StackOverflow, it has a solution, but ... not sure if it is really Angular style.. https://stackoverflow.com/questions/46571277/angular-2-forms-set-ngform-submitted-to-true-via-ngform-object – axel Apr 30 '18 at 16:06
  • Works for me but, as @YuriyYakovenko said you need the view child reference in your component class: @ViewChild('documentEditForm') documentEditForm: ElementRef; – Gerardo Tarragona May 31 '18 at 15:28
  • Yes, I have the same issue as @ParthaSarathiGhosh, Did you solve it ? – MELWIN VINCENT Aug 13 '18 at 11:56
  • 1
    this.documentEditForm.onSubmit(event); //From TS files – Partha Sarathi Ghosh Aug 20 '18 at 05:40
  • Please try this solution if it can be helpful: https://stackoverflow.com/a/52659388/2049788 – Chintan Thummar Oct 05 '18 at 06:20
  • Don't use this. Call `onSubmit()` or you won't set `submitted=true` – Simon_Weaver Jan 05 '19 at 08:38
  • 2
    IMPORTANT: calling `.ngSubmit.emit` will **NOT** set `submitted` to true. If you care about handling whether the form was submitted or not, use `.onSubmit(null)` instead, otherwise `.submitted` will be false. – briosheje Dec 04 '19 at 10:01
  • 1
    A few things ---> This answer works and I didn't even need @ViewChild. I just used ```(ngSubmit)="save(documentEditForm)``` and passed the form to the my save function. Important: I had major bugs because the form doesn't really submit itself!! Use ```.onSubmit(null)``` instead of ```.ngSubmit.emit()``` like @briosheje proposed. – Itay Dec 14 '20 at 14:31
  • @YuriyYakovenko if I add the ViewChild, I am getting ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'null'. Current value: 'true' – bokkie Feb 05 '21 at 08:37
  • @bokkie This is old solution, please don’t do that, use ReactiveForms – Yuriy Yakovenko Feb 05 '21 at 09:25
  • @YuriyYakovenko Thanks for the reply. Template driven forms are not old or outdated, our use case doesn't need reactive forms, plus, across the app we use template driven forms. Is this not possible with template driven forms? – bokkie Feb 05 '21 at 11:16
27

Important: if using Angular material form controls + reactive forms

Call onSubmit(undefined) to properly set submitted = true on the [formGroup] directive

Note: The directive is not the same object as the angular form itself (more below on that)

Here's part of the sourcecode for the [formGroup] directive. (for reactive forms)

@Directive({
  selector: '[formGroup]',
  providers: [formDirectiveProvider],
  host: {'(submit)': 'onSubmit($event)', '(reset)': 'onReset()'},
  exportAs: 'ngForm'
})
export class FormGroupDirective extends ControlContainer implements Form,
    OnChanges {
  /**
   * @description
   * Reports whether the form submission has been triggered.
   */
  public readonly submitted: boolean = false;

 .....

 onSubmit($event: Event): boolean {
    (this as{submitted: boolean}).submitted = true;
    syncPendingControls(this.form, this.directives);
    this.ngSubmit.emit($event);
    return false;
 }

You use it like this:

<form [formGroup]="form" #formRef="ngForm">

And you can get a reference to the FormGroupDirective in your ts file with:

@ViewChild('formRef') 
formRef: FormGroupDirective;
  • Note: NgForm is another directive automatically applied when you create a <form> tag.
  • Confusing note: Both NgForm and FormGroupDirective have exportAs: 'ngForm' in their source code, (and they also both declare submitted and ngSubmit properties). But when I put #ngForm='ngForm' I get an object of type FormGroupDirective and not NgForm (verified in debugger). I'm not sure exactly why - but that's why i declared it as FormGroupDirective and not NgForm - I think maybe the first wins.

You use it like this:

this.formRef.onSubmit(undefined)

Example:

// html
<form [formGroup]="form" #formRef="ngForm">
    // ...Form Controls
</form>

// component.ts
export class MyComponent {

    @ViewChild('formRef')
    formRef: FormGroupDirective;

    form: FormGroup = new FormGroup({
        myInput: new FormControl(''),
        //etc...
    });

    submitFormProgrammatically() {
        this.formRef.onSubmit(undefined);
    }
}

If you were to just call this.formRef.ngSubmit.emit() as some other answers have suggested you won't get the all important submitted = true set.

Why does this matter?

If you're using any Angular CDK or Angular Material controls the error condition isn't displayed unless the form field has been touched (clicked or gained focus) OR the form as a whole has been submitted.

So if you have a missing required field that the mouse/cursor has never entered then that field won't be shown in red even if you do ngSubmit.emit() (because submitted = false for the form and the control has touched = false).

So how does it work normally with a regular submit button?

Normally if you have <button type='submit'>Submit</button> (inside the <form> tag) it will trigger the standard HTML <form> to submit (nothing to do with Angular) - and that raises a standard submit event on the form tag.

IF that <form> tag also has a [formGroup] directive on it (as shown above) then the HTML form submit event is 'caught' by the directive and that's what causes the onSubmit() function above to be called.

This in turn raises the ngSubmit event - which you can catch yourself if you need to do further processing - like showing an alert.

So it's very important to call onSubmit and not ngSubmit.emitto get the validation handling to work when using material (reactive form) controls. The $event parameter can just be null or undefined.

Further reading: Look at ErrorStateMatcher (for Angular CDK/Material only) to see the exact rules. You can create your own if working around the limitations of the default becomes too complex.

Even more confusing: The [formGroup] directive is NOT the same object as FormGroup which just holds data. Only the directive has submitted on it - whereas FormGroup has things like touched, pristine, dirty.

Community
  • 1
  • 1
Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
7

A trick that worked for me using

  • Reactive Forms
  • Angular2
  • incl. IE

was this:

<!-- real button will simulate click on invisible button (cf. form) -->
<button onclick="document.getElementById('hiddenSaveButtonForMicrosoftWithLove').click()">
  The Real Button outside forms
</button>

<form>
  <!-- will be called in the background and is never visible -->
  <button id="hiddenSaveButtonForMicrosoftWithLove" type="submit" style="display: none;">hiddenSaveButtonForMicrosoftWithLove</button>
</form>
Marcel
  • 568
  • 7
  • 12
  • I would suggest the correct one provided by @Yodacheese since it is the most accurate answer based on the question, but I voted on this one because it is battle-tested. I have used this approach since so many years and I still use it in some scenarios. I would suggest an addition, the `[disabled] ` attribute to both buttons if you want to keep the form from submitting by hitting enter if isn't valid. – Sparker73 Sep 21 '19 at 01:55
  • Thanks @Sparker73 - happy to edit if you provide the complete solution – Marcel Sep 24 '19 at 09:37
7

This example will work in Angular 6 and up

<form (ngSubmit)="save()" id="ngForm" [formGroup]="form"> 
    ... 
</form>

<button type="submit" class="btn-save button primary" form="ngForm">Save</button>
Vince
  • 81
  • 1
  • 1
  • This is a nice solution but it doesn't work in IE :( You can check it here: https://www.w3schools.com/tags/att_button_form.asp – mryuso92 Aug 09 '19 at 09:35
5

If you are using Reactive Forms use the formGroup invalid property to disable the submit button:

<button  
  form="ngForm"
  [disabled]=" editor.invalid>Enviar</button> 

...

<form [formGroup]="editor"  id="ngForm"   (ngSubmit)="save()" novalidate >
...
</form>
Javier Rojano
  • 814
  • 9
  • 17
3

This works for me.

<form #editForm="ngForm">
  <button type="button" (click)="editForm.submitted = true; editForm.ngSubmit.emit(); anotherMethod();">
    Submit programatically
  </button>
</form>

The key is to set both submitted = true and emitting ngSubmit event.

michal.jakubeczy
  • 8,221
  • 1
  • 59
  • 63
  • "Cannot assign to 'submitted' because it is a read-only property" - that's the error I get – Itay Dec 14 '20 at 15:32
0

Below solution is working in my case, please try this simple solution. I am not sure, if it will be working into all the conditions:

<form #documentEditForm="ngForm" id="ngForm" (ngSubmit)="save(documentEditForm.valid)"> 
    ...Your Input Elements... 
</form>

The button should be declared outside the form like this:

<button form="ngForm">Submit</button>

The validation of the form should check into save() by following conditions

save(isValid:boolean){
    if(isValid) {
        ...Your code to save details...
    }
}

Hope this simple solution will help you.

Chintan Thummar
  • 1,462
  • 19
  • 32
0

I'm writing this answer in hopes that it helps someone. I was spending a whole day trying to solve this issue, following every answer and comments therein... but I was still getting the error message that #documentEditForm (my equivalent) was not found (cannot read property x of undefined...), no matter what I tried.

The reason why I thought being 'outside' the form tag was problematic was due to the fact that this problem went away as soon as I placed the submit/validation button inside the </form> tag. I went down this rabbit hole, but later realized there are legit reasons why I need to be outside of the form.

But later I realized that I had to do TWO things to solve this problem by (1) adding ? to the ngForm's name when validating and (2) implementing ViewChild(NgForm) documentEditForm!: NgForm.

<form #documentEditForm="ngForm">
...
</form>

<button class="btn-save button primary" (click)="save()" [disabled]="documentEditForm?.invalid">
   SAVE
</button>

Inside component.ts:

import { Component, OnInit, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';

export class XYZComponent implements OnInit {

  @ViewChild(NgForm) documentEditForm!: NgForm;

...

When using template-driven forms in Angular, and you use the #form_name="ngForm" syntax, you expose it to just inside the form. However, when you use ViewChild, then you expose the form to the entire component, not just inside the form. Please remember that if the app fails to compile or gives unexpected output/error, give the good old ? a try.

Jason
  • 657
  • 7
  • 13