Angular中如何将ReactiveForms数据存储到localStorage并同步更新
Angular汽车经销商应用需求与代码改造
我开发了一个模拟汽车经销商的Angular应用,现有功能包括:
- 展示车辆列表,点击车辆跳转至详情页查看完整属性
- 详情页点击编辑按钮弹出对话框,通过ReactiveForms修改车辆数据
- 应用启动时从.json文件加载车辆列表到localStorage
需要实现三个核心需求:
- 订阅ReactiveForms,输入时实时修改车辆信息
- 提交表单时将新数据更新到localStorage
- 优先从localStorage拉取数据展示列表,若无数据则从.json加载初始数据

现有代码展示
表单组件(ChangeInfoFormComponent)TS代码
export class ChangeInfoFormComponent implements OnInit { constructor(private dialogRef: MatDialogRef<EditCarComponent>) {} ngOnInit(): void { throw new Error('Method not implemented.'); } carInfoForm = new FormGroup({ brandName: new FormControl(''), model: new FormControl(''), year: new FormControl(''), keywords: new FormControl(''), }) onSubmit() { // TODO: Use EventEmitter with form value console.warn(this.carInfoForm.value); } close() { this.dialogRef.close(); } }
表单组件HTML代码
<form [formGroup]="carInfoForm"> <label for="brandName">Brand: </label> <input id="brandName" type="text" formControlName="brandName" (ngModel)="onSubmit();"> <!-- <p> Value: {{brandName.value}}</p> --> <hr /> <label for="model">Model: </label> <input id="model" type="text" formControlName="model"> <!-- <p> Value: {{model.value}}</p> --> <hr /> <label for="year">Year: </label> <input id="year" type="text" formControlName="year"> <!-- <p> Value: {{year}}</p> --> <hr /> <label for="keywords">Keywords: </label> <input id="keywords" type="text" formControlName="keywords"> <!-- <p> Value: {{keywords.value}}</p> --> <br /> </form> <button class="btn btn-outline-secondary" style="margin-right: 2%;" (click)="close();"> Back </button> <button class="btn btn-outline-secondary" style="margin-right: 2%;" type="submit" [disabled]="!carInfoForm.valid"> Submit </button>
车辆详情组件(CarDetailComponent)TS代码
export class CarDetailComponent implements OnInit { form: FormGroup; pageTitle: string = 'Car Detail'; car: ICar | undefined; brand: string = ''; model: string = ''; sub!: Subscription; errorMessage: string = ''; imageWidth: number = 400; imageMargin: number = 2; constructor( private route: ActivatedRoute, private router: Router, private carService: CarService, public dialog: MatDialog, // private dialogRef: MatDialogRef<EditCarComponent>, ) {} ngOnInit(): void { const id = Number(this.route.snapshot.paramMap.get('id')); this.pageTitle += `: `; this.sub = this.carService.getCarById(id).subscribe({ next: (cars) => { this.car = cars; console.log(this.car); }, error: (err) => (this.errorMessage = err), }); } openDialog() { const dialogConfig = new MatDialogConfig(); dialogConfig.disableClose = true; dialogConfig.autoFocus = true; dialogConfig.data = { id: 1, title: "Test title dialogConfig", } this.dialog.open(EditCarComponent, dialogConfig); } onBack(): void { this.router.navigate(['/cars']); } }
车辆详情组件HTML代码
<div class="card"> <div class="card-header"> {{pageTitle + car?.brandName + ' ' + car?.model + ', ' + car?.year}} </div> <div class="card-body"> <div class="row"> <div class="col-md-7 justify-content-between pt-4"> <ul> <td><b>Brand:</b> {{car?.brandName}}</td><br/> <td><b>Model:</b> {{car?.model}}</td><br/> <td><b>Year:</b> {{car?.year}}</td><br/> <td><b>Keywords:</b> {{car?.keywords}}</td> </ul> </div> <div class="col-md-3 col justify-content-between"> <img class="img-responsive" src={{car?.imageUrl}} alt={{car?.brandName}} [style.width.px]='imageWidth' [style.margin.px]='imageMargin'> </div> </div> </div> <div class="card-footer"> <button class="btn btn-outline-secondary" style="width: 10%; margin-right: 2%;" (click)="onBack()"> Back </button> <button class="btn btn-outline-secondary" style="width: 10%" (click)="openDialog()"> Edit </button> </div> </div>
车辆列表组件(CarListComponent)TS代码
export class CarListComponent implements OnInit { pageTitle: string = 'Car List'; imageWidth: number = 275; imageMargin: number = 2; showImage: boolean = false; errorMessage: string = ''; sub!: Subscription; brandName: string = ''; private _listFilter: string = ''; get listFilter(): string { return this._listFilter; } set listFilter(value: string) { this._listFilter = value; this.filteredCars = this.performFilter(value); } filteredCars: ICar[] = []; cars: ICar[] = []; constructor(private carService: CarService) {} performFilter(filterBy: string): ICar[] { filterBy = filterBy.toLocaleLowerCase(); return this.cars.filter((car: ICar) => car.brandName.toLocaleLowerCase().includes(filterBy) ); } toggleImage(): void { this.showImage = !this.showImage; } showInfo(id: number): void { let selectedCar = this.cars.find((car) => car.carId === id); this.brandName = JSON.stringify(selectedCar?.brandName); } ngOnInit(): void { this.sub = this.carService.getCars().subscribe({ next: (cars) => { (this.cars = cars), (this.filteredCars = this.cars); localStorage.setItem('cars', JSON.stringify(cars)); }, error: (err) => (this.errorMessage = err), }); } ngOnDestroy(): void { this.sub.unsubscribe(); } }
需求实现方案
1. 改造CarService,优先从localStorage加载数据
修改服务逻辑,先读取localStorage,无数据时再从json文件加载并缓存:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, of, tap } from 'rxjs'; import { ICar } from './car.interface'; @Injectable({ providedIn: 'root' }) export class CarService { private carsUrl = 'assets/cars.json'; constructor(private http: HttpClient) {} getCars(): Observable<ICar[]> { // 优先读取localStorage const storedCars = localStorage.getItem('cars'); if (storedCars) { return of(JSON.parse(storedCars)); } // 无数据则从json加载并缓存 return this.http.get<ICar[]>(this.carsUrl).pipe( tap(cars => localStorage.setItem('cars', JSON.stringify(cars))) ); } getCarById(id: number): Observable<ICar | undefined> { return this.getCars().pipe( map(cars => cars.find(car => car.carId === id)) ); } updateCar(updatedCar: ICar): void { const storedCars = JSON.parse(localStorage.getItem('cars') || '[]'); const carIndex = storedCars.findIndex((car: ICar) => car.carId === updatedCar.carId); if (carIndex !== -1) { storedCars[carIndex] = updatedCar; localStorage.setItem('cars', JSON.stringify(storedCars)); } } }
2. 改造对话框表单组件,实现实时更新与提交
修改CarDetailComponent的对话框打开逻辑
传入当前车辆数据,订阅对话框关闭事件刷新数据:
openDialog() { const dialogConfig = new MatDialogConfig(); dialogConfig.disableClose = true; dialogConfig.autoFocus = true; // 传入当前车辆数据 dialogConfig.data = this.car; const dialogRef = this.dialog.open(ChangeInfoFormComponent, dialogConfig); // 对话框关闭后更新详情页数据 dialogRef.afterClosed().subscribe(result => { if (result) { this.car = result; } }); }
重构ChangeInfoFormComponent
初始化表单值,订阅表单变化实现实时更新,提交时更新localStorage:
import { Component, Inject, OnInit } from '@angular/core'; import { FormGroup, FormControl } from '@angular/forms'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { ICar } from '../car.interface'; import { CarService } from '../car.service'; @Component({ selector: 'app-change-info-form', templateUrl: './change-info-form.component.html', styleUrls: ['./change-info-form.component.css'] }) export class ChangeInfoFormComponent implements OnInit { carInfoForm: FormGroup; currentCar: ICar; constructor( private dialogRef: MatDialogRef<ChangeInfoFormComponent>, @Inject(MAT_DIALOG_DATA) public data: ICar, private carService: CarService ) { this.currentCar = { ...data }; // 用当前车辆数据初始化表单 this.carInfoForm = new FormGroup({ brandName: new FormControl(this.currentCar.brandName), model: new FormControl(this.currentCar.model), year: new FormControl(this.currentCar.year), keywords: new FormControl(this.currentCar.keywords) }); } ngOnInit(): void { // 订阅表单值变化,实时更新车辆数据 this.carInfoForm.valueChanges.subscribe(value => { this.currentCar = { ...this.currentCar, ...value }; }); } onSubmit() { if (this.carInfoForm.valid) { // 更新localStorage this.carService.updateCar(this.currentCar); // 关闭对话框并返回更新后的数据 this.dialogRef.close(this.currentCar); } } close() { this.dialogRef.close(); } }
修复表单HTML错误
移除无效的ngModel绑定,绑定表单提交事件:
<form [formGroup]="carInfoForm" (ngSubmit)="onSubmit()"> <label for="brandName">Brand: </label> <input id="brandName" type="text" formControlName="brandName"> <hr /> <label for="model">Model: </label> <input id="model" type="text" formControlName="model"> <hr /> <label for="year">Year: </label> <input id="year" type="text" formControlName="year"> <hr /> <label for="keywords">Keywords: </label> <input id="keywords" type="text" formControlName="keywords"> <br /> </form> <button class="btn btn-outline-secondary" style="margin-right: 2%;" (click)="close();"> Back </button> <button class="btn btn-outline-secondary" style="margin-right: 2%;" type="submit" [disabled]="!carInfoForm.valid"> Submit </button>
3. 优化车辆列表组件加载逻辑
移除手动保存localStorage的代码,由CarService统一处理:
ngOnInit(): void { this.sub = this.carService.getCars().subscribe({ next: (cars) => { this.cars = cars; this.filteredCars = this.cars; }, error: (err) => (this.errorMessage = err) }); }
内容的提问来源于stack exchange,提问作者k0uva
相关产品推荐
相关产品推荐

