我正在尝试创建一个指令来监听
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
属性在之后被替换一样视图初始化。您知道实现此类指令的正确方法是什么吗?
正如评论所述,我觉得更好的主意是当视图模型(或组件)实例上的属性发生更改时执行此操作,这相当简单。
//
// 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">
希望这有帮助!