我try 创建一个监听DOM property changes的指令,以自动将属性值保存到LocalStorage,并在页面加载时自动恢复(以保存任何HTML元素的用户首选项).我需要它来监听输入的变化,但是因为我想要一个通用指令,所以我不想监听change事件.

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

我不得不将其转换为typescript并使其适应使用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,则观察器会在某个地方中断,原因我不明白,就好像在视图初始化后替换了nativeElementchecked属性.

你知道实现这一指令的正确方式是什么吗?

推荐答案

正如我所 comments 的,我觉得当您的view模型(或组件)实例上的属性发生变化时,这样做会更好,这是相当简单的.

gist

//
// 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">

希望这有帮助!

Angular相关问答推荐

Angular独立组件(使用LoadComponents)-懒惰加载服务不工作

Angular 和PrimeNg文件Uploader:渲染元件时如何显示选定文件的列表?

使选项卡与父零部件成一定Angular 激活

如果过滤器在rxjs流中触发,则返回空数组

使用Angular 13的NgxPrinterService打印时,第二页上的空白区域

为什么在回调完成之前,可观察对象会返回?

如何从URL中角下载图片?

如何防止打印后的空格后的HTML元素的Angular ?

为什么动画只触发一次?

Angular将文件作为字符串读取给dxf解析器

如何在 Angular RxJS 中替换订阅中的 subscrib?

为什么Angular的ViewEncapsulation枚举有没有值为1的键?

Angular MSAL - 对 Bearer 令牌和 AUD 属性的怀疑

Angular p-triStateCheckbox for each Select 更改不同的 colored颜色

PrimeNG 避免在同一位置使用不同键的 cogo toast 重叠

错误:在工作区外运行 Angular CLI 时,此命令不可用

将 html ngModel 值传递给服务 --Angular

如何在不刷新整页的情况下更新组件

为什么 Angular2 routerLinkActive 将活动类设置为多个链接?

在 Angular 2 中使用 store (ngrx) 有什么好处