Skip to content

Commit

Permalink
refactor: migrate to signals (#1883)
Browse files Browse the repository at this point in the history
  • Loading branch information
cipchk authored Jan 29, 2025
1 parent 7397da3 commit f503c9f
Show file tree
Hide file tree
Showing 15 changed files with 163 additions and 189 deletions.
14 changes: 4 additions & 10 deletions packages/abc/auto-focus/auto-focus.directive.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -16,21 +16,16 @@ 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(() => {
context.enabled = false;
context.showInput = true;
fixture.detectChanges();
tick(2);
fixture.whenStable().then(() => {
expect(context.focus).not.toHaveBeenCalled();
});
expect(context.focus).not.toHaveBeenCalled();
}));
});

Expand All @@ -45,7 +40,6 @@ describe('abc: auto-focus', () => {
imports: [AutoFocusDirective]
})
class TestComponent {
@ViewChild(AutoFocusDirective) comp!: AutoFocusDirective;
showInput = false;
enabled = true;
focus(): void {}
Expand Down
39 changes: 19 additions & 20 deletions packages/abc/auto-focus/auto-focus.directive.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLElement>>(ElementRef).nativeElement;
enabled = input(true, { transform: booleanAttribute });
delay = input(25, { transform: numberAttribute });
readonly focus = output();

enabled = input<boolean, boolean | string | null | undefined>(true, { transform: booleanAttribute });
delay = input<number, number | string | null | undefined>(300, { transform: numberAttribute });

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();
});
}
});
}
}
3 changes: 2 additions & 1 deletion packages/abc/auto-focus/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | `-` |
3 changes: 2 additions & 1 deletion packages/abc/auto-focus/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ module: import { AutoFocusModule } from '@delon/abc/auto-focus';
| 成员 | 说明 | 类型 | 默认值 |
|----|----|----|-----|
| `[enabled]` | 是否启用 | `boolean` | `true` |
| `[delay]` | 延迟时长(单位:毫秒) | `number` | `300` |
| `[delay]` | 延迟时长(单位:毫秒) | `number` | `25` |
| `(focus)` | 获得焦点回调 | `void` | `-` |
31 changes: 17 additions & 14 deletions packages/abc/cell/cell-host.directive.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<CellTextResult>();

ngOnChanges(): void {
const widget = this.data.options.widget!;
const componentType = this.srv.getWidget(widget.key!)?.ref as Type<unknown>;
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<unknown>;
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;
});
}
}
124 changes: 58 additions & 66 deletions packages/abc/cell/cell.component.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -39,35 +38,37 @@ import type { CellDefaultText, CellOptions, CellTextResult, CellValue } from './
selector: 'cell, [cell]',
template: `
<ng-template #text>
@let res = _res();
@let text = _text();
@switch (safeOpt.type) {
@case ('checkbox') {
<label nz-checkbox [nzDisabled]="disabled" [ngModel]="value" (ngModelChange)="change($event)">
<label nz-checkbox [nzDisabled]="disabled()" [ngModel]="value()" (ngModelChange)="value.set($event)">
{{ safeOpt.checkbox?.label }}
</label>
}
@case ('radio') {
<label nz-radio [nzDisabled]="disabled" [ngModel]="value" (ngModelChange)="change($event)">
<label nz-radio [nzDisabled]="disabled()" [ngModel]="value()" (ngModelChange)="value.set($event)">
{{ safeOpt.radio?.label }}
</label>
}
@case ('link') {
<a (click)="_link($event)" [attr.target]="safeOpt.link?.target" [attr.title]="value" [innerHTML]="_text"></a>
<a (click)="_link($event)" [attr.target]="safeOpt.link?.target" [attr.title]="value()" [innerHTML]="text"></a>
}
@case ('tag') {
<nz-tag [nzColor]="res?.result?.color">
<span [innerHTML]="_text"></span>
<span [innerHTML]="text"></span>
</nz-tag>
}
@case ('badge') {
<nz-badge [nzStatus]="res?.result?.color" nzText="{{ _text }}" />
<nz-badge [nzStatus]="res?.result?.color" nzText="{{ text }}" />
}
@case ('widget') {
@if (res) {
<ng-template cell-widget-host [data]="res" />
}
}
@case ('img') {
@for (i of $any(_text); track $index) {
@for (i of $any(text); track $index) {
@let img = safeOpt.img;
<img
[attr.src]="i"
Expand All @@ -80,19 +81,19 @@ import type { CellDefaultText, CellOptions, CellTextResult, CellValue } from './
}
}
@default {
@if (isText) {
<span [innerText]="_text" [attr.title]="value"></span>
@if (isText()) {
<span [innerText]="text" [attr.title]="value()"></span>
} @else {
<span [innerHTML]="_text" [attr.title]="value"></span>
<span [innerHTML]="text" [attr.title]="value()"></span>
}
@if (_unit) {
<span class="unit">{{ _unit }}</span>
@if (_unit()) {
<span class="unit">{{ _unit() }}</span>
}
}
}
</ng-template>
<ng-template #textWrap>
@if (showDefault) {
@if (showDefault()) {
{{ safeOpt.default?.text }}
} @else {
@if (safeOpt.tooltip) {
Expand All @@ -104,7 +105,7 @@ import type { CellDefaultText, CellOptions, CellTextResult, CellValue } from './
}
}
</ng-template>
@if (loading) {
@if (loading()) {
<nz-icon nzType="loading" />
} @else {
<ng-template [ngTemplateOutlet]="textWrap" />
Expand All @@ -127,45 +128,50 @@ 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);
private readonly el: HTMLElement = inject(ElementRef).nativeElement;

private destroy$?: Subscription;

_text!: string | SafeValue | string[] | number;
_unit?: string;
res?: CellTextResult;
showDefault = false;
_text = signal<string | SafeValue | string[] | number>('');
_unit = signal<string | undefined>(undefined);
_res = signal<CellTextResult | undefined>(undefined);
showDefault = computed(() => this.value() == (this.safeOpt.default as CellDefaultText)?.condition);

@Input() value?: CellValue;
@Output() readonly valueChange = new EventEmitter<NzSafeAny>();
@Input() options?: CellOptions;
@Input({ transform: booleanAttribute }) loading = false;
@Input({ transform: booleanAttribute }) disabled = false;
value = model<CellValue>();
options = input<CellOptions>();
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();
});
});
}

Expand All @@ -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;
Expand All @@ -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;
});
Expand Down
Loading

0 comments on commit f503c9f

Please sign in to comment.