Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: migrate to signals #1883

Merged
merged 6 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading