You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular中如何将ReactiveForms数据存储到localStorage并同步更新

Angular汽车经销商应用需求与代码改造

我开发了一个模拟汽车经销商的Angular应用,现有功能包括:

  • 展示车辆列表,点击车辆跳转至详情页查看完整属性
  • 详情页点击编辑按钮弹出对话框,通过ReactiveForms修改车辆数据
  • 应用启动时从.json文件加载车辆列表到localStorage

需要实现三个核心需求:

  1. 订阅ReactiveForms,输入时实时修改车辆信息
  2. 提交表单时将新数据更新到localStorage
  3. 优先从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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 17:06:32