From b96507aed343087bd18b8349bc13e2634c3aef97 Mon Sep 17 00:00:00 2001 From: cipchk Date: Mon, 20 Jan 2025 01:19:36 +0800 Subject: [PATCH 1/5] refactor(abc:error-collect): migrate to signals --- .../error-collect/error-collect.component.ts | 66 ++++++++----------- .../abc/error-collect/error-collect.spec.ts | 4 +- packages/abc/error-collect/index.en-US.md | 2 +- packages/abc/error-collect/index.zh-CN.md | 2 +- 4 files changed, 32 insertions(+), 42 deletions(-) diff --git a/packages/abc/error-collect/error-collect.component.ts b/packages/abc/error-collect/error-collect.component.ts index 879091f62c..fc590eed8f 100644 --- a/packages/abc/error-collect/error-collect.component.ts +++ b/packages/abc/error-collect/error-collect.component.ts @@ -1,19 +1,20 @@ -import { Direction, Directionality } from '@angular/cdk/bidi'; +import { Directionality } from '@angular/cdk/bidi'; import { Platform } from '@angular/cdk/platform'; import { DOCUMENT } from '@angular/common'; import { ChangeDetectionStrategy, - ChangeDetectorRef, Component, DestroyRef, ElementRef, - Input, + InputSignalWithTransform, OnInit, ViewEncapsulation, inject, - numberAttribute + input, + numberAttribute, + signal } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { interval } from 'rxjs'; import { AlainConfigService } from '@delon/util/config'; @@ -24,12 +25,12 @@ import { NzIconDirective } from 'ng-zorro-antd/icon'; exportAs: 'errorCollect', template: ` - {{ count }} + {{ count() }} `, host: { '[class.error-collect]': 'true', - '[class.error-collect-rtl]': `dir === 'rtl'`, - '[class.d-none]': '_hiden', + '[class.error-collect-rtl]': `dir() === 'rtl'`, + '[class.d-none]': '_hiden()', '(click)': '_click()' }, preserveWhitespaces: false, @@ -38,24 +39,21 @@ import { NzIconDirective } from 'ng-zorro-antd/icon'; imports: [NzIconDirective] }) export class ErrorCollectComponent implements OnInit { - private readonly el: HTMLElement = inject(ElementRef).nativeElement; - private readonly cdr = inject(ChangeDetectorRef); + private readonly el = inject>(ElementRef).nativeElement; private readonly doc = inject(DOCUMENT); - private readonly directionality = inject(Directionality); private readonly platform = inject(Platform); private readonly destroy$ = inject(DestroyRef); - private formEl: HTMLFormElement | null = null; - _hiden = true; - count = 0; - dir?: Direction = 'ltr'; + _hiden = signal(true); + count = signal(0); + dir = toSignal(inject(Directionality).change); - @Input({ transform: numberAttribute }) freq!: number; - @Input({ transform: numberAttribute }) offsetTop!: number; + readonly freq: InputSignalWithTransform = input(0, { transform: numberAttribute }); + readonly offsetTop: InputSignalWithTransform = input(0, { transform: numberAttribute }); constructor(configSrv: AlainConfigService) { - configSrv.attach(this, 'errorCollect', { freq: 500, offsetTop: 65 + 64 + 8 * 2 }); + configSrv.attach(this, 'errorCollect', { freq: 250, offsetTop: 65 + 64 + 8 * 2 }); } private get errEls(): NodeListOf { @@ -64,36 +62,23 @@ export class ErrorCollectComponent implements OnInit { private update(): void { const count = this.errEls.length; - if (count === this.count) return; - this.count = count; - this._hiden = count === 0; - this.cdr.markForCheck(); + if (count === this.count()) return; + this.count.set(count); + this._hiden.set(count === 0); } _click(): boolean { - if (this.count === 0) return false; + if (this.count() === 0) return false; // nz-form-control const els = this.errEls; const formItemEl = this.findParent(els[0], '[nz-form-control]') || els[0]; formItemEl.scrollIntoView(true); // fix header height - this.doc.documentElement.scrollTop -= this.offsetTop; + this.doc.documentElement.scrollTop -= this.offsetTop(); return true; } - private install(): void { - this.dir = this.directionality.value; - this.directionality.change.pipe(takeUntilDestroyed(this.destroy$)).subscribe(direction => { - this.dir = direction; - this.cdr.detectChanges(); - }); - interval(this.freq) - .pipe(takeUntilDestroyed(this.destroy$)) - .subscribe(() => this.update()); - this.update(); - } - - private findParent(el: Element, selector: string): HTMLFormElement | null { + private findParent(el: HTMLElement, selector: string): HTMLFormElement | null { let retEl: HTMLFormElement | null = null; while (el) { if (el.querySelector(selector)) { @@ -110,6 +95,11 @@ export class ErrorCollectComponent implements OnInit { this.formEl = this.findParent(this.el, 'form'); if (this.formEl === null) throw new Error('No found form element'); - this.install(); + + interval(this.freq()) + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(() => this.update()); + + this.update(); } } diff --git a/packages/abc/error-collect/error-collect.spec.ts b/packages/abc/error-collect/error-collect.spec.ts index 600f9df40c..286be231d0 100644 --- a/packages/abc/error-collect/error-collect.spec.ts +++ b/packages/abc/error-collect/error-collect.spec.ts @@ -40,14 +40,14 @@ describe('abc: error-collect', () => { beforeEach(() => getPropertiesAndCreate()); it('should be collect error', (done: () => void) => { setTimeout(() => { - expect(context.comp.count).toBe(1); + expect(context.comp.count()).toBe(1); done(); }, 21); }); it('should be click go to first error element', (done: () => void) => { setTimeout(() => { - expect(context.comp.count).toBe(1); + expect(context.comp.count()).toBe(1); const el = dl.query(By.css('.ant-form-item-has-error')).nativeElement as HTMLElement; spyOn(el, 'scrollIntoView'); expect(el.scrollIntoView).not.toHaveBeenCalled(); diff --git a/packages/abc/error-collect/index.en-US.md b/packages/abc/error-collect/index.en-US.md index 3950bd0d76..80960b773d 100644 --- a/packages/abc/error-collect/index.en-US.md +++ b/packages/abc/error-collect/index.en-US.md @@ -14,5 +14,5 @@ A simple form exception messages collector that jump to element location via cli | Property | Description | Type | Default | Global Config | |----------|-------------|------|---------|---------------| -| `[freq]` | Monitor frequency, unit is milliseconds | `number` | `500` | ✅ | +| `[freq]` | Monitor frequency, unit is milliseconds | `number` | `250` | ✅ | | `[offsetTop]` | Top offset, unit is `px` | `number` | `145` | ✅ | diff --git a/packages/abc/error-collect/index.zh-CN.md b/packages/abc/error-collect/index.zh-CN.md index ebbc492311..8670edee07 100644 --- a/packages/abc/error-collect/index.zh-CN.md +++ b/packages/abc/error-collect/index.zh-CN.md @@ -14,5 +14,5 @@ module: import { ErrorCollectModule } from '@delon/abc/error-collect'; | 成员 | 说明 | 类型 | 默认值 | 全局配置 | |----|----|----|-----|------| -| `[freq]` | 监听频率,单位:毫秒 | `number` | `500` | ✅ | +| `[freq]` | 监听频率,单位:毫秒 | `number` | `250` | ✅ | | `[offsetTop]` | 顶部偏移值,单位:`px` | `number` | `145` | ✅ | From ed76b53377f0a65024a509c7cd8135b439c5254b Mon Sep 17 00:00:00 2001 From: cipchk Date: Mon, 20 Jan 2025 01:27:47 +0800 Subject: [PATCH 2/5] chore: update --- .../abc/auto-focus/auto-focus.directive.ts | 4 +-- .../error-collect/error-collect.component.ts | 5 ++-- .../abc/notice-icon/notice-icon.component.ts | 30 +++++-------------- packages/abc/st/st.component.ts | 2 +- 4 files changed, 13 insertions(+), 28 deletions(-) diff --git a/packages/abc/auto-focus/auto-focus.directive.ts b/packages/abc/auto-focus/auto-focus.directive.ts index f02b2b2ed1..3954334652 100644 --- a/packages/abc/auto-focus/auto-focus.directive.ts +++ b/packages/abc/auto-focus/auto-focus.directive.ts @@ -21,8 +21,8 @@ export class AutoFocusDirective implements AfterViewInit { private readonly platform = inject(Platform); private readonly destroy$ = inject(DestroyRef); - enabled = input(true, { transform: booleanAttribute }); - delay = input(300, { transform: numberAttribute }); + enabled = input(true, { transform: booleanAttribute }); + delay = input(300, { transform: numberAttribute }); ngAfterViewInit(): void { const el = this.el; diff --git a/packages/abc/error-collect/error-collect.component.ts b/packages/abc/error-collect/error-collect.component.ts index fc590eed8f..7fed5e4d99 100644 --- a/packages/abc/error-collect/error-collect.component.ts +++ b/packages/abc/error-collect/error-collect.component.ts @@ -6,7 +6,6 @@ import { Component, DestroyRef, ElementRef, - InputSignalWithTransform, OnInit, ViewEncapsulation, inject, @@ -49,8 +48,8 @@ export class ErrorCollectComponent implements OnInit { count = signal(0); dir = toSignal(inject(Directionality).change); - readonly freq: InputSignalWithTransform = input(0, { transform: numberAttribute }); - readonly offsetTop: InputSignalWithTransform = input(0, { transform: numberAttribute }); + readonly freq = input(0, { transform: numberAttribute }); + readonly offsetTop = input(0, { transform: numberAttribute }); constructor(configSrv: AlainConfigService) { configSrv.attach(this, 'errorCollect', { freq: 250, offsetTop: 65 + 64 + 8 * 2 }); diff --git a/packages/abc/notice-icon/notice-icon.component.ts b/packages/abc/notice-icon/notice-icon.component.ts index 833928bf50..e885e87efe 100644 --- a/packages/abc/notice-icon/notice-icon.component.ts +++ b/packages/abc/notice-icon/notice-icon.component.ts @@ -2,8 +2,6 @@ import { NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, Component, - OnDestroy, - OnInit, ViewEncapsulation, booleanAttribute, effect, @@ -13,7 +11,8 @@ import { output, signal } from '@angular/core'; -import { Subscription } from 'rxjs'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { map } from 'rxjs'; import { DelonLocaleService, LocaleData } from '@delon/theme'; import { NzBadgeComponent } from 'ng-zorro-antd/badge'; @@ -46,18 +45,15 @@ import { NoticeIconSelect, NoticeItem } from './notice-icon.types'; NoticeIconTabComponent ] }) -export class NoticeIconComponent implements OnInit, OnDestroy { - private readonly i18n = inject(DelonLocaleService); - private i18n$?: Subscription; - locale = signal({}); - +export class NoticeIconComponent { + locale = toSignal(inject(DelonLocaleService).change.pipe(map(data => data['noticeIcon']))); data = input([]); - count = input(undefined, { transform: numberAttribute }); - loading = input(false, { transform: booleanAttribute }); - popoverVisible = input(false, { transform: booleanAttribute }); + count = input(undefined, { transform: numberAttribute }); + loading = input(false, { transform: booleanAttribute }); + popoverVisible = input(false, { transform: booleanAttribute }); btnClass = input(); btnIconClass = input(); - centered = input(false, { transform: booleanAttribute }); + centered = input(false, { transform: booleanAttribute }); readonly select = output(); readonly clear = output(); readonly popoverVisibleChange = output(); @@ -84,14 +80,4 @@ export class NoticeIconComponent implements OnInit, OnDestroy { onClear(title: string): void { this.clear.emit(title); } - - ngOnInit(): void { - this.i18n$ = this.i18n.change.subscribe(() => { - this.locale.set(this.i18n.getData('noticeIcon')); - }); - } - - ngOnDestroy(): void { - this.i18n$?.unsubscribe(); - } } diff --git a/packages/abc/st/st.component.ts b/packages/abc/st/st.component.ts index 40fda15b09..c0baadee6f 100644 --- a/packages/abc/st/st.component.ts +++ b/packages/abc/st/st.component.ts @@ -334,7 +334,7 @@ export class STComponent implements AfterViewInit, OnChanges { @Input({ transform: booleanAttribute }) bordered = false; @Input() size!: 'small' | 'middle' | 'default'; @Input() scroll: { x?: string | null; y?: string | null } = { x: null, y: null }; - drag = input(null, { + drag = input(null, { transform: v => { const obj: STDragOptions | null = typeof v === 'object' ? v : booleanAttribute(v) ? {} : null; if (obj == null) return null; From be24d344ea8dcf0bf393ef6eb3f4da340474a3a2 Mon Sep 17 00:00:00 2001 From: cipchk Date: Mon, 20 Jan 2025 01:40:18 +0800 Subject: [PATCH 3/5] chore: fix build --- packages/abc/notice-icon/notice-icon.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/abc/notice-icon/notice-icon.component.ts b/packages/abc/notice-icon/notice-icon.component.ts index e885e87efe..4aab058922 100644 --- a/packages/abc/notice-icon/notice-icon.component.ts +++ b/packages/abc/notice-icon/notice-icon.component.ts @@ -46,7 +46,9 @@ import { NoticeIconSelect, NoticeItem } from './notice-icon.types'; ] }) export class NoticeIconComponent { - locale = toSignal(inject(DelonLocaleService).change.pipe(map(data => data['noticeIcon']))); + locale = toSignal(inject(DelonLocaleService).change.pipe(map(data => data['noticeIcon'])), { + requireSync: true + }); data = input([]); count = input(undefined, { transform: numberAttribute }); loading = input(false, { transform: booleanAttribute }); From 7b874496ce9199ed0774bb65880950a5cf3cc3c3 Mon Sep 17 00:00:00 2001 From: cipchk Date: Mon, 20 Jan 2025 02:07:25 +0800 Subject: [PATCH 4/5] chore: update auto focus --- .../auto-focus/auto-focus.directive.spec.ts | 14 ++----- .../abc/auto-focus/auto-focus.directive.ts | 37 +++++++++---------- packages/abc/auto-focus/index.en-US.md | 3 +- packages/abc/auto-focus/index.zh-CN.md | 3 +- 4 files changed, 26 insertions(+), 31 deletions(-) diff --git a/packages/abc/auto-focus/auto-focus.directive.spec.ts b/packages/abc/auto-focus/auto-focus.directive.spec.ts index ce6b9bc714..64b9959ddb 100644 --- a/packages/abc/auto-focus/auto-focus.directive.spec.ts +++ b/packages/abc/auto-focus/auto-focus.directive.spec.ts @@ -1,4 +1,4 @@ -import { Component, ViewChild } from '@angular/core'; +import { Component } from '@angular/core'; import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { AutoFocusDirective } from './auto-focus.directive'; @@ -16,11 +16,8 @@ describe('abc: auto-focus', () => { it('should be working', fakeAsync(() => { context.showInput = true; fixture.detectChanges(); - tick(301); - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(context.focus).toHaveBeenCalled(); - }); + tick(2); + expect(context.focus).toHaveBeenCalled(); })); it('should be not when enabled is false', fakeAsync(() => { @@ -28,9 +25,7 @@ describe('abc: auto-focus', () => { context.showInput = true; fixture.detectChanges(); tick(2); - fixture.whenStable().then(() => { - expect(context.focus).not.toHaveBeenCalled(); - }); + expect(context.focus).not.toHaveBeenCalled(); })); }); @@ -45,7 +40,6 @@ describe('abc: auto-focus', () => { imports: [AutoFocusDirective] }) class TestComponent { - @ViewChild(AutoFocusDirective) comp!: AutoFocusDirective; showInput = false; enabled = true; focus(): void {} diff --git a/packages/abc/auto-focus/auto-focus.directive.ts b/packages/abc/auto-focus/auto-focus.directive.ts index 3954334652..6a5e12afff 100644 --- a/packages/abc/auto-focus/auto-focus.directive.ts +++ b/packages/abc/auto-focus/auto-focus.directive.ts @@ -1,36 +1,35 @@ -import { Platform } from '@angular/cdk/platform'; import { - AfterViewInit, - DestroyRef, Directive, ElementRef, + afterNextRender, booleanAttribute, inject, input, - numberAttribute + numberAttribute, + output } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { take, timer } from 'rxjs'; @Directive({ selector: '[auto-focus], input[autofocus="autofocus"], textarea[autofocus="autofocus"]', exportAs: 'autoFocus' }) -export class AutoFocusDirective implements AfterViewInit { - private readonly el: HTMLElement = inject(ElementRef).nativeElement; - private readonly platform = inject(Platform); - private readonly destroy$ = inject(DestroyRef); - +export class AutoFocusDirective { + private readonly el = inject>(ElementRef).nativeElement; enabled = input(true, { transform: booleanAttribute }); - delay = input(300, { transform: numberAttribute }); + delay = input(25, { transform: numberAttribute }); + readonly focus = output(); - ngAfterViewInit(): void { - const el = this.el; - if (!this.platform.isBrowser || !(el instanceof HTMLElement) || !this.enabled()) { - return; - } - timer(this.delay()) - .pipe(takeUntilDestroyed(this.destroy$), take(1)) - .subscribe(() => el.focus({ preventScroll: false })); + constructor() { + afterNextRender(() => { + if (this.enabled()) { + timer(this.delay()) + .pipe(take(1)) + .subscribe(() => { + this.el.focus({ preventScroll: false }); + this.focus.emit(); + }); + } + }); } } diff --git a/packages/abc/auto-focus/index.en-US.md b/packages/abc/auto-focus/index.en-US.md index 69e1f6b5da..46e4ea68d3 100644 --- a/packages/abc/auto-focus/index.en-US.md +++ b/packages/abc/auto-focus/index.en-US.md @@ -15,4 +15,5 @@ Allows to focus HTML-element right after its appearance, By default, it will tak | Property | Description | Type | Default | |----------|-------------|------|---------| | `[enabled]` | Whether enabled of auto focus | `boolean` | `true` | -| `[delay]` | Delay of the focus (unit: ms) | `number` | `300` | +| `[delay]` | Delay of the focus (unit: ms) | `number` | `25` | +| `(focus)` | Get focus callback | `void` | `-` | diff --git a/packages/abc/auto-focus/index.zh-CN.md b/packages/abc/auto-focus/index.zh-CN.md index cf54a290bb..f5f3ddfade 100644 --- a/packages/abc/auto-focus/index.zh-CN.md +++ b/packages/abc/auto-focus/index.zh-CN.md @@ -15,4 +15,5 @@ module: import { AutoFocusModule } from '@delon/abc/auto-focus'; | 成员 | 说明 | 类型 | 默认值 | |----|----|----|-----| | `[enabled]` | 是否启用 | `boolean` | `true` | -| `[delay]` | 延迟时长(单位:毫秒) | `number` | `300` | +| `[delay]` | 延迟时长(单位:毫秒) | `number` | `25` | +| `(focus)` | 获得焦点回调 | `void` | `-` | From 3eb7c0f3072392a7c24ea4934726bf26274924f3 Mon Sep 17 00:00:00 2001 From: cipchk Date: Mon, 20 Jan 2025 16:05:50 +0800 Subject: [PATCH 5/5] chore: update --- .../abc/auto-focus/auto-focus.directive.ts | 4 +- packages/abc/cell/cell-host.directive.ts | 31 +++-- packages/abc/cell/cell.component.ts | 124 ++++++++---------- packages/abc/cell/cell.spec.ts | 7 +- packages/abc/cell/demo/simple.md | 1 + .../error-collect/error-collect.component.ts | 4 +- .../abc/notice-icon/notice-icon.component.ts | 8 +- src/app/app.config.ts | 23 +++- 8 files changed, 102 insertions(+), 100 deletions(-) diff --git a/packages/abc/auto-focus/auto-focus.directive.ts b/packages/abc/auto-focus/auto-focus.directive.ts index 6a5e12afff..454a244046 100644 --- a/packages/abc/auto-focus/auto-focus.directive.ts +++ b/packages/abc/auto-focus/auto-focus.directive.ts @@ -16,8 +16,8 @@ import { take, timer } from 'rxjs'; }) export class AutoFocusDirective { private readonly el = inject>(ElementRef).nativeElement; - enabled = input(true, { transform: booleanAttribute }); - delay = input(25, { transform: numberAttribute }); + enabled = input(true, { transform: booleanAttribute }); + delay = input(25, { transform: numberAttribute }); readonly focus = output(); constructor() { diff --git a/packages/abc/cell/cell-host.directive.ts b/packages/abc/cell/cell-host.directive.ts index b92645027d..7e2ff10c50 100644 --- a/packages/abc/cell/cell-host.directive.ts +++ b/packages/abc/cell/cell-host.directive.ts @@ -1,4 +1,4 @@ -import { Directive, Input, OnChanges, Type, ViewContainerRef, inject } from '@angular/core'; +import { Directive, Type, ViewContainerRef, effect, inject, input } from '@angular/core'; import { warn } from '@delon/util/other'; @@ -8,24 +8,27 @@ import { CellTextResult } from './cell.types'; @Directive({ selector: '[cell-widget-host]' }) -export class CellHostDirective implements OnChanges { +export class CellHostDirective { private readonly srv = inject(CellService); private readonly vcr = inject(ViewContainerRef); - @Input() data!: CellTextResult; + data = input.required(); - ngOnChanges(): void { - const widget = this.data.options.widget!; - const componentType = this.srv.getWidget(widget.key!)?.ref as Type; - if (componentType == null) { - if (typeof ngDevMode === 'undefined' || ngDevMode) { - warn(`cell: No widget for type "${widget.key}"`); + constructor() { + effect(() => { + const data = this.data(); + const widget = data.options.widget!; + const componentType = this.srv.getWidget(widget.key!)?.ref as Type; + if (componentType == null) { + if (typeof ngDevMode === 'undefined' || ngDevMode) { + warn(`cell: No widget for type "${widget.key}"`); + } + return; } - return; - } - this.vcr.clear(); - const componentRef = this.vcr.createComponent(componentType); - (componentRef.instance as { data: CellTextResult }).data = this.data; + this.vcr.clear(); + const componentRef = this.vcr.createComponent(componentType); + (componentRef.instance as { data: CellTextResult }).data = data; + }); } } diff --git a/packages/abc/cell/cell.component.ts b/packages/abc/cell/cell.component.ts index c8ffb6f682..8c28257372 100644 --- a/packages/abc/cell/cell.component.ts +++ b/packages/abc/cell/cell.component.ts @@ -1,30 +1,29 @@ import { NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, - ChangeDetectorRef, Component, ElementRef, - EventEmitter, - Input, - OnChanges, OnDestroy, - Output, Renderer2, - SimpleChange, ViewEncapsulation, booleanAttribute, - inject + computed, + effect, + inject, + input, + model, + signal } from '@angular/core'; +import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import type { SafeValue } from '@angular/platform-browser'; import { Router } from '@angular/router'; -import { Subscription } from 'rxjs'; +import { combineLatest, Subscription, take } from 'rxjs'; import { updateHostClass } from '@delon/util/browser'; import { WINDOW } from '@delon/util/token'; import { NzBadgeComponent } from 'ng-zorro-antd/badge'; import { NzCheckboxComponent } from 'ng-zorro-antd/checkbox'; -import type { NzSafeAny } from 'ng-zorro-antd/core/types'; import { NzIconDirective } from 'ng-zorro-antd/icon'; import { NzImage, NzImageModule, NzImageService } from 'ng-zorro-antd/image'; import { NzRadioComponent } from 'ng-zorro-antd/radio'; @@ -39,27 +38,29 @@ import type { CellDefaultText, CellOptions, CellTextResult, CellValue } from './ selector: 'cell, [cell]', template: ` + @let res = _res(); + @let text = _text(); @switch (safeOpt.type) { @case ('checkbox') { - - @if (showDefault) { + @if (showDefault()) { {{ safeOpt.default?.text }} } @else { @if (safeOpt.tooltip) { @@ -104,7 +105,7 @@ import type { CellDefaultText, CellOptions, CellTextResult, CellValue } from './ } } - @if (loading) { + @if (loading()) { } @else { @@ -127,10 +128,9 @@ import type { CellDefaultText, CellOptions, CellTextResult, CellValue } from './ CellHostDirective ] }) -export class CellComponent implements OnChanges, OnDestroy { +export class CellComponent implements OnDestroy { private readonly srv = inject(CellService); private readonly router = inject(Router); - private readonly cdr = inject(ChangeDetectorRef); private readonly renderer = inject(Renderer2); private readonly imgSrv = inject(NzImageService); private readonly win = inject(WINDOW); @@ -138,34 +138,40 @@ export class CellComponent implements OnChanges, OnDestroy { private destroy$?: Subscription; - _text!: string | SafeValue | string[] | number; - _unit?: string; - res?: CellTextResult; - showDefault = false; + _text = signal(''); + _unit = signal(undefined); + _res = signal(undefined); + showDefault = computed(() => this.value() == (this.safeOpt.default as CellDefaultText)?.condition); - @Input() value?: CellValue; - @Output() readonly valueChange = new EventEmitter(); - @Input() options?: CellOptions; - @Input({ transform: booleanAttribute }) loading = false; - @Input({ transform: booleanAttribute }) disabled = false; + value = model(); + options = input(); + loading = input(false, { transform: booleanAttribute }); + disabled = input(false, { transform: booleanAttribute }); get safeOpt(): CellOptions { - return this.res?.options ?? {}; + return this._res()?.options ?? {}; } - get isText(): boolean { - return this.res?.safeHtml === 'text'; - } - - private updateValue(): void { - this.destroy$?.unsubscribe(); - this.destroy$ = this.srv.get(this.value, this.options).subscribe(res => { - this.res = res; - this.showDefault = this.value == (this.safeOpt.default as CellDefaultText).condition; - this._text = res.result?.text ?? ''; - this._unit = res.result?.unit ?? this.safeOpt?.unit; - this.cdr.detectChanges(); - this.setClass(); + isText = computed(() => this._res()?.safeHtml === 'text'); + + constructor() { + combineLatest([toObservable(this.loading), toObservable(this.disabled)]) + .pipe(takeUntilDestroyed()) + .subscribe(() => this.setClass()); + + effect(() => { + const v = this.value(); + const o = this.options(); + this.destroy$?.unsubscribe(); + this.destroy$ = this.srv + .get(v, o) + .pipe(take(1)) + .subscribe(res => { + this._res.set(res); + this._text.set(res.result?.text ?? ''); + this._unit.set(res.result?.unit ?? this.safeOpt?.unit); + this.setClass(); + }); }); } @@ -176,32 +182,18 @@ export class CellComponent implements OnChanges, OnDestroy { [`cell`]: true, [`cell__${renderType}`]: renderType != null, [`cell__${size}`]: size != null, - [`cell__has-unit`]: this._unit, - [`cell__has-default`]: this.showDefault, - [`cell__disabled`]: this.disabled + [`cell__has-unit`]: this._unit(), + [`cell__has-default`]: this.showDefault(), + [`cell__disabled`]: this.disabled() }); el.setAttribute('data-type', `${type}`); } - ngOnChanges(changes: { [p in keyof CellComponent]?: SimpleChange }): void { - // Do not call updateValue when only updating loading, disabled - if (Object.keys(changes).every(k => ['loading', 'disabled'].includes(k))) { - this.setClass(); - } else { - this.updateValue(); - } - } - - change(value: NzSafeAny): void { - this.value = value; - this.valueChange.emit(value); - } - _link(e: Event): void { e.preventDefault(); e.stopPropagation(); - if (this.disabled) return; + if (this.disabled()) return; const link = this.safeOpt.link; const url = link?.url; @@ -219,7 +211,7 @@ export class CellComponent implements OnChanges, OnDestroy { if (config == null || config.big == null) return; let idx = -1; - const list = (this._text as string[]).map((p, index) => { + const list = (this._text() as string[]).map((p, index) => { if (idx === -1 && p === img) idx = index; return typeof config.big === 'function' ? config.big(p) : p; }); diff --git a/packages/abc/cell/cell.spec.ts b/packages/abc/cell/cell.spec.ts index d729afd0df..3a1da1062b 100644 --- a/packages/abc/cell/cell.spec.ts +++ b/packages/abc/cell/cell.spec.ts @@ -1,4 +1,4 @@ -import { Component, DebugElement, ViewChild } from '@angular/core'; +import { Component, DebugElement } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BrowserModule, By, DomSanitizer } from '@angular/platform-browser'; import { provideNoopAnimations } from '@angular/platform-browser/animations'; @@ -223,7 +223,7 @@ describe('abc: cell', () => { it('#valueChange', () => { spyOn(context, 'valueChange'); - context.value = true; + context.value = false; context.options = { type: 'checkbox' }; fixture.detectChanges(); expect(context.valueChange).not.toHaveBeenCalled(); @@ -368,9 +368,6 @@ class TestWidget { imports: [CellComponent] }) class TestComponent { - @ViewChild('comp', { static: true }) - comp!: CellComponent; - value?: unknown; valueChange(_?: NzSafeAny): void {} options?: CellOptions; diff --git a/packages/abc/cell/demo/simple.md b/packages/abc/cell/demo/simple.md index e150dc0a11..7c4504206c 100644 --- a/packages/abc/cell/demo/simple.md +++ b/packages/abc/cell/demo/simple.md @@ -91,6 +91,7 @@ import { NzGridModule } from 'ng-zorro-antd/grid'; [options]="{ type: 'checkbox', tooltip: 'Tooltip', checkbox: { label: 'Label' } }" [disabled]="disabled" /> + {{ checkbox }} Change Disabled
diff --git a/packages/abc/error-collect/error-collect.component.ts b/packages/abc/error-collect/error-collect.component.ts index 7fed5e4d99..20d50ada6b 100644 --- a/packages/abc/error-collect/error-collect.component.ts +++ b/packages/abc/error-collect/error-collect.component.ts @@ -48,8 +48,8 @@ export class ErrorCollectComponent implements OnInit { count = signal(0); dir = toSignal(inject(Directionality).change); - readonly freq = input(0, { transform: numberAttribute }); - readonly offsetTop = input(0, { transform: numberAttribute }); + readonly freq = input(0, { transform: numberAttribute }); + readonly offsetTop = input(0, { transform: numberAttribute }); constructor(configSrv: AlainConfigService) { configSrv.attach(this, 'errorCollect', { freq: 250, offsetTop: 65 + 64 + 8 * 2 }); diff --git a/packages/abc/notice-icon/notice-icon.component.ts b/packages/abc/notice-icon/notice-icon.component.ts index 4aab058922..2d55771fe8 100644 --- a/packages/abc/notice-icon/notice-icon.component.ts +++ b/packages/abc/notice-icon/notice-icon.component.ts @@ -50,12 +50,12 @@ export class NoticeIconComponent { requireSync: true }); data = input([]); - count = input(undefined, { transform: numberAttribute }); - loading = input(false, { transform: booleanAttribute }); - popoverVisible = input(false, { transform: booleanAttribute }); + count = input(undefined, { transform: numberAttribute }); + loading = input(false, { transform: booleanAttribute }); + popoverVisible = input(false, { transform: booleanAttribute }); btnClass = input(); btnIconClass = input(); - centered = input(false, { transform: booleanAttribute }); + centered = input(false, { transform: booleanAttribute }); readonly select = output(); readonly clear = output(); readonly popoverVisibleChange = output(); diff --git a/src/app/app.config.ts b/src/app/app.config.ts index c04d9aca21..5f41267282 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -2,7 +2,14 @@ import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/ import ngLang from '@angular/common/locales/zh'; import { APP_ID, ApplicationConfig, ErrorHandler, importProvidersFrom } from '@angular/core'; import { provideAnimations } from '@angular/platform-browser/animations'; -import { provideRouter, withComponentInputBinding, withInMemoryScrolling, withViewTransitions } from '@angular/router'; +import { + provideRouter, + RouterFeatures, + withComponentInputBinding, + withHashLocation, + withInMemoryScrolling, + withViewTransitions +} from '@angular/router'; import { ServiceWorkerModule } from '@angular/service-worker'; import { provideNuMonacoEditorConfig } from '@ng-util/monaco-editor'; @@ -85,17 +92,19 @@ const alainConfig: AlainConfig = { const ngZorroConfig: NzConfig = {}; +const routerFeatures: RouterFeatures[] = [ + withComponentInputBinding(), + withViewTransitions(), + withInMemoryScrolling({ scrollPositionRestoration: 'top' }) +]; +if (!environment.production) routerFeatures.push(withHashLocation()); + export const appConfig: ApplicationConfig = { providers: [ { provide: APP_ID, useValue: 'ngAlainDoc' }, provideHttpClient(withFetch(), withInterceptors([mockInterceptor])), provideAnimations(), - provideRouter( - routes, - withComponentInputBinding(), - withViewTransitions(), - withInMemoryScrolling({ scrollPositionRestoration: 'top' }) - ), + provideRouter(routes, ...routerFeatures), // provideClientHydration(), // 暂时不开启水合,除了编译时间长,还有就是对DOM要求比较高 provideAlain({ config: alainConfig, defaultLang, i18nClass: I18NService }), provideNzConfig(ngZorroConfig),