Stavm
Stavm

Reputation: 8131

Initialize template driven Form with query parameters

I would like to initialize a Template-Driven Form with query parameter values.

Intuitively you would create the form and populate it on ngAfterViewInit:

HTML

<form #f="ngForm">
    <input type="text" id="firstName" name="fname" #fname ngModel>

    <input *ngIf="fname.value" type="text" id="lastName" name="lname" ngModel>

    <button type="submit">Submit</button>
</form>

Component:

@ViewChild('f') f: NgForm;

constructor(private route: ActivatedRoute) {}
  
ngAfterViewInit() {
    const queryParams = this.route.snapshot.queryParams;

    this.f.form.setValue(queryParams)
}

then access it with query parameters: ?fname=aaa&lname=bbb

now, there are two issues with this approach:

  1. as it turns out, this does not work because Angular requires another tick to register the form
  2. setValue won't work because the second ctrl, lname doesnt exist at the time of applying the values.

this will require me to

  1. add an extra cycle (Angular team suggests setTimeout @ console error)
  2. use patchValue that only applies valid values, twice.

something like:

 ngAfterViewInit() {
    const queryParams = { fname: 'aaa', lname: 'bbb'};

    // if we wish to access template driven form, we need to wait an extra tick for form registration.
    // angular suggests using setTimeout or such - switched it to timer operator instead.

    timer(1)
      // since last name ctrl is only shown when first name has value (*ngIf="fname.value"),
      // patchValue won't patch it on the first 'run' because it doesnt exist yet.
      // so we need to do it twice.

      .pipe(repeat(2))
      // we use patchValue and not setValue because of the above reason.
      // setValue applies the whole value, while patch only applies controls that exists on the form.
      // and since, last name doesnt exist at first, it requires us to use patch. twice.

      .subscribe(() => this.f.form.patchValue(queryParams))
  }

Is there a less hacky way to accomplish this Without creating a variable for each control on the component side as doing that, would, in my opinion, make template driven redundant.

attached: stackblitz Demo of the "hacky" soultion

Upvotes: 5

Views: 1189

Answers (4)

Bertramp
Bertramp

Reputation: 385

You can use the QueryParamMap observable from the ActivatedRoute instead of the snapshot, then map the params to an object and subscribe to it in the template with the async pipe

Html

<h1 class="header">Hello There</h1>
<div class="form-container"*ngIf="(formModel$ | async) as formModel">
  <form class="form" #ngForm="ngForm" (ngSubmit)="onFormSubmit()" >
    <input [(ngModel)]="formModel.fname" name="fname">
    <input [(ngModel)]="formModel.lname" name="lname">
    <button type="submit">Execute Order 66</button>
  </form>
</div>
<div class="img-container">
  <img *ngIf="(executeOrder$ | async) === true" src="https://vignette.wikia.nocookie.net/starwars/images/4/44/End_Days.jpg/revision/latest?cb=20111028234105">
</div>

Component

interface FormModel {
  fname: string;
  lname: string;
}

@Component({
  selector: 'hello',
  templateUrl: './hello.component.html',
  styleUrls: ['./hello.component.css']
})
export class HelloComponent implements OnInit  {
  @ViewChild('ngForm') ngForm: NgForm;
  formModel$: Observable<FormModel>;
  executeOrder$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);

  constructor(private activatedRoute: ActivatedRoute){}

  ngOnInit(): void {
    this.formModel$ = this.activatedRoute.queryParamMap.pipe(
      map(paramsMap => {
        const entries = paramsMap.keys.map(k => [k, paramsMap.get(k)]);
        const obj = {}
        for(const entry of entries){
          obj[entry[0]] = entry[1]
        }

        // Should be working with es2020: return Object.fromEntries(entries)

        return obj as FormModel
      })
    )
  }

  onFormSubmit() {
    console.log(this.ngForm.value)
    this.executeOrder$.next(true);
  }
}

I have created a working example on StackBlitz that uses this method

https://stackblitz.com/edit/angular-ivy-6vqpdz?file=src/app/hello.component.ts

Upvotes: 0

Iam Coder
Iam Coder

Reputation: 1003

with [(ngModel)] can try the below

<form #heroForm="ngForm">
<div class="form-group">
    <label for="fname">First Name</label>
    <input type="text" class="form-control" name="fname" [(ngModel)]="queryParams.fname" required>
</div>
    <div class="form-group" *ngIf="queryParams?.fname">
        <label for="lname">Last Name</label>
        <input type="text" class="form-control" name="lname" [(ngModel)]="queryParams.lname">
</div>
        <button type="submit" class="btn btn-success">Submit</button>

Then in form component

export class HeroFormComponent implements OnInit {
  @ViewChild("heroForm", null) heroForm: NgForm;
queryParams={};
  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    
    this.queryParams = { fname: "aaa", lname: "bbb" };
  }
}

You no need to declare for each form control. just assign queryParams & ngModel will handle the rest.

Upvotes: 3

Robin Dijkhof
Robin Dijkhof

Reputation: 19288

You can use [hidden] instead of ngIf. That way the element remains in the dom. Also I'm using a timeout of 0 ms.

https://stackblitz.com/edit/angular-gts2wl-298w8l?file=src%2Fapp%2Fhero-form%2Fhero-form.component.html

Upvotes: 0

abhay tripathi
abhay tripathi

Reputation: 4022

We can use [(ngmodel)] along with local variables to directly bind over here.

<form #f="ngForm">
    <input type="text" id="firstName" name="fname" #fname [(ngModel)]="myfname">
    <input *ngIf="fname.value" type="text" id="lastName" name="lname" [(ngModel)]="mylname">
    <button type="submit">Submit</button>
</form>

some component.ts

myfname:string;
mylname:string;

ngAfterViewInit() {
    const queryParams = this.route.snapshot.queryParams;
    myfname = queryParams.fname;
    mylname = queryParams.lname;
}

we can also use constructor() instead of ngAfterViewInit().

Upvotes: 0

Related Questions