Reputation: 8131
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:
setValue
won't work because the second ctrl, lname
doesnt exist at the time of applying the values.this will require me to
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
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
<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>
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
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
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.
Upvotes: 0
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