5

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

2

4 Answers 4

3
+50

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.

Sign up to request clarification or add additional context in comments.

Comments

0

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().

3 Comments

Thanks tripathi, I would rather not define variables for each form control (I think i stated that at the bottom of the question) because then it would not make sense to use template driven at all
you already defining one variable for selecting form? you can also define single variable model: { fname: string, lname: string } what are you are trying to achieve by not defining variables.? @Stavm
@Stavm - all above answers given later were also using the same [(ngModel)] technique.
0

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

2 Comments

Thanks for answering Robin, the approach you are suggesting isn't practical, its basically saying "render all possible forms and then apply the values". the whole idea of having dynamic form parts, is not having one giant form when you don't need it PLUS have the submit only send form values that are needed for that specific form.
@stavm not ideal, but it is easy though. You can fix the submit problem by also disabling control that are not visible. Disabled values will not be send
0

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

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.