Angular 如何观察 DOM 元素属性变化?

问题描述 投票:0回答:1

我正在尝试创建一个指令来监听

DOM property changes
自动将属性值保存到 LocalStorage,并在页面加载时自动恢复它(以保存用户对任何 HTML 元素的首选项)。我需要它来监听输入更改,但由于我想要一个通用指令,所以我不想监听
change
事件。

我使用这个答案找到了一个解决方案:https://stackoverflow.com/a/55737231/7920723

我必须将其转换为打字稿并调整它以使用

Observables
,这是一个mvce:

import { AfterViewInit, Component, Directive, ElementRef, EventEmitter, Input, Output } from '@angular/core';
import { Observable } from 'rxjs';

@Directive({
  selector: '[cdltStoreProperty]',
  standalone: true
})
export class StoreAttributeDirective implements AfterViewInit {
  id!: string;

  @Input('cdltStoreProperty') attributeName: string | null = null;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  @Output() readonly storedValue = new EventEmitter<any>();

  constructor(
    private ref: ElementRef
  ) {
    console.log('test');
  }

  ngAfterViewInit(): void {
    const element = this.ref.nativeElement;
    
    if (this.attributeName) {
      this.id = `${StoreAttributeDirective.name}_${element.getAttribute('id')}`;
      console.log(`id = ${this.id}`);
      
      const valueStored = this.restoreValue();
      if (!valueStored) {
        const value = element[this.attributeName];
        this.saveValue(value);
        this.storedValue.emit(value);
      }
      
      observePropertyChange(this.ref, this.attributeName).subscribe(newValue => {
        console.log("property changed");
        this.saveValue(newValue)
      });
    }
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  restoreValue() : any {
    const valueStored = window.localStorage.getItem(this.id);
    console.log(`valueStored = ${valueStored} of type = ${typeof valueStored}`);

    if (valueStored && this.attributeName) {
      const value = JSON.parse(valueStored);
      console.log(`Restoring value ${value} of type ${typeof value}`);
      this.ref.nativeElement[this.attributeName] = value;
      return value;
    }

    return undefined;
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  saveValue(value: any) {
    console.log(`Saving ${value} of type ${typeof value}`);
    const valueToStore = JSON.stringify(value);
    console.log(`Saving value to store ${valueToStore} of type ${typeof valueToStore}`);
    window.localStorage.setItem(this.id, valueToStore);
  }
}

@Component({
  selector: 'cdlt-mvce',
  standalone: true,
  imports: [StoreAttributeDirective],
  template: `
    <input
      id="remember-me-checkbox"
      type="checkbox"
      cdltStoreProperty="checked"
      [checked]="checkedValue"
      (storedValue)="loadStoredValue($event)"
      (change)="checkedValue = !checkedValue"
>`,
  styleUrl: './mvce.component.scss'
})
export class MvceComponent {
  checkedValue = true;

  loadStoredValue(value: any) {
    this.checkedValue = value;
  }
}

function observePropertyChange(elementRef: ElementRef, propertyName: string) : Observable<any> {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const propertyObserver$ = new Observable<any>(observer => {
    const superProps = Object.getPrototypeOf(elementRef.nativeElement);
    const propertyDescriptor = Object.getOwnPropertyDescriptor(superProps, propertyName);

    if (!propertyDescriptor) {
      console.error(`No property descriptor for ${propertyName}`);
      return;
    }
    const superGet = propertyDescriptor.get;
    const superSet = propertyDescriptor.set;
    if (!superGet) {
      console.error(`No getter for ${propertyName}`);
      return;
    } else if (!superSet) {
      console.error(`No setter for ${propertyName}`);
      return;
    }

    const newProps = {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      get: function() : any {
        return superGet.apply(this, []);
      },
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      set: function (t : any) : any {
        observer.next(t);
        return superSet.apply(this, [t]);
      }
    };

    Object.defineProperty(elementRef.nativeElement, propertyName, newProps);
  });

  return propertyObserver$;
}

由于我正在访问属性,所以我在

AfterViewInit
阶段订阅了该属性,您认为这是正确的吗?

这段代码有两个问题:

  • 如果在未选中
    input
    时刷新,恢复功能会引发
    ExpressionChangedAfterItHasBeenCheckedError
    ,所以我想这不是执行此操作的正确方法
  • 如果您使用未绑定属性
    checked
    的输入,例如使用
    checked=false
    ,则观察者会在某处中断,原因我不明白,就好像
    nativeElement
    checked
    属性在之后被替换一样视图初始化。

您知道实现此类指令的正确方法是什么吗?

angular dom properties observable
1个回答
0
投票

正如评论所述,我觉得更好的主意是当视图模型(或组件)实例上的属性发生更改时执行此操作,这相当简单。

要点

//
// Inspired by https://github.com/zhaosiyang/property-watch-decorator/blob/master/src/index.ts
//
// Annotate a field in a typescript class to store its' content in localstorage transparently.
//
export function Preference<T = any>(preferenceKey: string, defaultValueObj: T) {
  return (target: any, key: PropertyKey) => {
    Object.defineProperty(target, key, {
      set: function(value) {
        localStorage.setItem(preferenceKey, JSON.stringify(value));
      },
      get: function() {
        const rawValue = window.localStorage.getItem(preferenceKey);
        const value = rawValue !== null && rawValue !== undefined ? <T>JSON.parse(rawValue) : defaultValueObj;
        return value;
      },
    });
  };
}

然后您可以在您的组件中使用它:

   class PreferencesComponent {
       @Preference('TableView.itemsPerPage, 10)
       itemsPerPage: number;
   }

并正常绑定:

   <input type="number" [(ngModel)]="itemsPerPage" min="10" max="100">

希望这有帮助!

© www.soinside.com 2019 - 2024. All rights reserved.