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

feat(abc:cell): add provideCellWidgets #1700

Merged
merged 3 commits into from Nov 12, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .prettierignore
Expand Up @@ -21,3 +21,6 @@ packages/theme/system/mixins/_functions.less
theme-default.less
theme-dark.less
theme-compact.less

# TODO: https://github.com/prettier/prettier/pull/15606
# packages/**/*
3 changes: 2 additions & 1 deletion packages/abc/cell/cell-host.directive.ts
Expand Up @@ -6,7 +6,8 @@ import { CellService } from './cell.service';
import { CellWidgetData } from './cell.types';

@Directive({
selector: '[cell-widget-host]'
selector: '[cell-widget-host]',
standalone: true
})
export class CellHostDirective implements OnInit {
@Input() data!: CellWidgetData;
Expand Down
130 changes: 72 additions & 58 deletions packages/abc/cell/cell.component.ts
@@ -1,3 +1,4 @@
import { NgTemplateOutlet } from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Expand All @@ -13,86 +14,99 @@ import {
SimpleChange,
ViewEncapsulation
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import type { SafeValue } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { Subscription } from 'rxjs';

import { updateHostClass } from '@delon/util/browser';
import { BooleanInput, InputBoolean } from '@delon/util/decorator';
import { WINDOW } from '@delon/util/token';
import { NzBadgeModule } from 'ng-zorro-antd/badge';
import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
import type { NzSafeAny } from 'ng-zorro-antd/core/types';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzImage, NzImageService } from 'ng-zorro-antd/image';
import { NzRadioModule } from 'ng-zorro-antd/radio';
import { NzTagModule } from 'ng-zorro-antd/tag';
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';

import { CellHostDirective } from './cell-host.directive';
import { CellService } from './cell.service';
import type { CellDefaultText, CellOptions, CellTextResult, CellValue, CellWidgetData } from './cell.types';

@Component({
selector: 'cell, [cell]',
template: `
<ng-template #text>
<ng-container [ngSwitch]="safeOpt.type">
<label
*ngSwitchCase="'checkbox'"
nz-checkbox
[nzDisabled]="disabled"
[ngModel]="value"
(ngModelChange)="change($event)"
>
{{ safeOpt.checkbox?.label }}
</label>
<label
*ngSwitchCase="'radio'"
nz-radio
[nzDisabled]="disabled"
[ngModel]="value"
(ngModelChange)="change($event)"
>
{{ safeOpt.radio?.label }}
</label>
<a
*ngSwitchCase="'link'"
(click)="_link($event)"
[attr.target]="safeOpt.link?.target"
[attr.title]="value"
[innerHTML]="_text"
></a>
<nz-tag *ngSwitchCase="'tag'" [nzColor]="res?.result?.color">
<span [innerHTML]="_text"></span>
</nz-tag>
<nz-badge *ngSwitchCase="'badge'" [nzStatus]="res?.result?.color" nzText="{{ _text }}" />
<ng-template *ngSwitchCase="'widget'" cell-widget-host [data]="hostData" />
<ng-container *ngSwitchCase="'img'">
<img
*ngFor="let i of $any(_text)"
[attr.src]="i"
[attr.height]="safeOpt.img?.size"
[attr.width]="safeOpt.img?.size"
(click)="_showImg(i)"
class="img"
[class.point]="safeOpt.img?.big"
/>
</ng-container>
<ng-container *ngSwitchDefault>
<span *ngIf="!isText" [innerHTML]="_text" [attr.title]="value"></span>
<span *ngIf="isText" [innerText]="_text" [attr.title]="value"></span>
<span *ngIf="_unit" class="unit">{{ _unit }}</span>
</ng-container>
</ng-container>
@switch(safeOpt.type) { @case('checkbox') {
<label nz-checkbox [nzDisabled]="disabled" [ngModel]="value" (ngModelChange)="change($event)">
{{ safeOpt.checkbox?.label }}
</label>
} @case('radio') {
<label nz-radio [nzDisabled]="disabled" [ngModel]="value" (ngModelChange)="change($event)">
{{ safeOpt.radio?.label }}
</label>
} @case('link') {
<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>
</nz-tag>
} @case('badge') {
<nz-badge [nzStatus]="res?.result?.color" nzText="{{ _text }}" />
} @case('widget') {
<ng-template cell-widget-host [data]="hostData" />
} @case('img') { @for (i of $any(_text); track $index) {
<img
[attr.src]="i"
[attr.height]="safeOpt.img?.size"
[attr.width]="safeOpt.img?.size"
(click)="_showImg(i)"
class="img"
[class.point]="safeOpt.img?.big"
/>
} } @default { @if(isText) {
<span [innerText]="_text" [attr.title]="value"></span>
} @else {
<span [innerHTML]="_text" [attr.title]="value"></span>
} @if(_unit) {
<span class="unit">{{ _unit }}</span>
} } }
</ng-template>
<ng-template #textWrap>
<ng-container *ngIf="showDefault">{{ safeOpt.default?.text }}</ng-container>
<ng-container *ngIf="!showDefault">
<span *ngIf="safeOpt.tooltip; else text" [nz-tooltip]="safeOpt.tooltip">
<ng-template [ngTemplateOutlet]="text" />
</span>
</ng-container>
@if (showDefault) {
{{ safeOpt.default?.text }}
} @else { @if (safeOpt.tooltip) {
<span [nz-tooltip]="safeOpt.tooltip">
<ng-template [ngTemplateOutlet]="text" />
</span>
} @else {
<ng-template [ngTemplateOutlet]="text" />
} }
</ng-template>
<span *ngIf="loading; else textWrap" nz-icon nzType="loading"></span>
@if (loading) {
<span nz-icon nzType="loading"></span>
} @else {
<ng-template [ngTemplateOutlet]="textWrap" />
}
`,
exportAs: 'cell',
preserveWhitespaces: false,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
encapsulation: ViewEncapsulation.None,
standalone: true,
imports: [
FormsModule,
NgTemplateOutlet,
NzCheckboxModule,
NzRadioModule,
NzIconModule,
NzTagModule,
NzBadgeModule,
NzToolTipModule,
CellHostDirective
]
})
export class CellComponent implements OnChanges, OnDestroy {
static ngAcceptInputType_loading: BooleanInput;
Expand Down Expand Up @@ -151,7 +165,7 @@ export class CellComponent implements OnChanges, OnDestroy {

private setClass(): void {
const { el, renderer } = this;
const { renderType, size } = this.safeOpt;
const { renderType, size, type } = this.safeOpt;
updateHostClass(el.nativeElement, renderer, {
[`cell`]: true,
[`cell__${renderType}`]: renderType != null,
Expand All @@ -160,7 +174,7 @@ export class CellComponent implements OnChanges, OnDestroy {
[`cell__has-default`]: this.showDefault,
[`cell__disabled`]: this.disabled
});
el.nativeElement.dataset.type = this.safeOpt.type;
el.nativeElement.setAttribute('data-type', `${type}`);
}

ngOnChanges(changes: { [p in keyof CellComponent]?: SimpleChange }): void {
Expand Down
5 changes: 3 additions & 2 deletions packages/abc/cell/cell.module.ts
Expand Up @@ -25,9 +25,10 @@ const COMPS = [CellComponent];
NzTagModule,
NzToolTipModule,
NzIconModule,
NzImageModule
NzImageModule,
...COMPS,
CellHostDirective
],
declarations: [...COMPS, CellHostDirective],
exports: COMPS
})
export class CellModule {}
14 changes: 14 additions & 0 deletions packages/abc/cell/cell.spec.ts
Expand Up @@ -16,6 +16,7 @@ import { CellComponent } from './cell.component';
import { CellModule } from './cell.module';
import { CellService } from './cell.service';
import { CellFuValue, CellOptions, CellWidgetData } from './cell.types';
import { provideCellWidgets } from './provide';

const DATE = new Date(2022, 0, 1, 1, 2, 3);

Expand Down Expand Up @@ -292,6 +293,19 @@ describe('abc: cell', () => {
});
});

describe('[widget]', () => {
it('via provideCellWidgets', () => {
TestBed.configureTestingModule({
imports: [CellModule, NoopAnimationsModule],
declarations: [TestComponent, TestWidget],
providers: [provideCellWidgets({ KEY: TestWidget.KEY, type: TestWidget })]
});
({ fixture, dl, context } = createTestContext(TestComponent));
page = new PageObject();
page.update('1', { widget: { key: TestWidget.KEY, data: 'new data' } }).check('1-new data');
});
});

class PageObject {
update(value: unknown, options?: CellOptions): this {
context.value = value;
Expand Down
30 changes: 8 additions & 22 deletions packages/abc/cell/index.en-US.md
Expand Up @@ -62,7 +62,7 @@ Cell formatting is supported for multiple data types, and supports widget mode.
- `enum` Enum
- `widget` Custom widget

**Custom widget**
## Custom widget

Just implement the `CellWidgetInstance` interface, for example:

Expand Down Expand Up @@ -95,28 +95,14 @@ export class CellTestWidget implements CellWidgetInstance {

`data` is a fixed parameter, including `value`, `options` configuration items.

Secondly, you also need to call `CellService.registerWidget` to register the widget; usually a new module will be built separately, for example:
Finally, register the widget through `provideCellWidgets` under `app.config.ts`, for example:

```ts
import { NgModule } from '@angular/core';

import { CellService } from '@delon/abc/cell';

import { CellTestWidget } from './test';
import { SharedModule } from '../shared.module';

export const CELL_WIDGET_COMPONENTS = [CellTestWidget];

@NgModule({
declarations: CELL_WIDGET_COMPONENTS,
imports: [SharedModule],
exports: CELL_WIDGET_COMPONENTS
})
export class CellWidgetModule {
constructor(srv: CellService) {
srv.registerWidget(CellTestWidget.KEY, CellTestWidget);
}
export const appConfig: ApplicationConfig = {
providers: [
provideCellWidgets(
{ KEY: CellTestWidget.KEY, type: CellTestWidget }
),
]
}
```

Finally, just register `CellWidgetModule` under the root module.
1 change: 1 addition & 0 deletions packages/abc/cell/index.ts
Expand Up @@ -3,3 +3,4 @@ export * from './cell-host.directive';
export * from './cell.module';
export * from './cell.service';
export * from './cell.types';
export * from './provide';
30 changes: 8 additions & 22 deletions packages/abc/cell/index.zh-CN.md
Expand Up @@ -62,7 +62,7 @@ module: import { CellModule } from '@delon/abc/cell';
- `enum` 枚举转换
- `widget` 自定义小部件

**自定义小部件**
## 自定义小部件

实现 `CellWidgetInstance` 接口即可,例如:

Expand Down Expand Up @@ -95,28 +95,14 @@ export class CellTestWidget implements CellWidgetInstance {

其中 `data` 为固定参数,包含 `value`、`options` 配置项。

其次,还需要调用 `CellService.registerWidget` 注册小部件;通常会单独构建一个新的模块,例如:
最后在 `app.config.ts` 下通过 `provideCellWidgets` 注册小部件,例如:

```ts
import { NgModule } from '@angular/core';

import { CellService } from '@delon/abc/cell';

import { CellTestWidget } from './test';
import { SharedModule } from '../shared.module';

export const CELL_WIDGET_COMPONENTS = [CellTestWidget];

@NgModule({
declarations: CELL_WIDGET_COMPONENTS,
imports: [SharedModule],
exports: CELL_WIDGET_COMPONENTS
})
export class CellWidgetModule {
constructor(srv: CellService) {
srv.registerWidget(CellTestWidget.KEY, CellTestWidget);
}
export const appConfig: ApplicationConfig = {
providers: [
provideCellWidgets(
{ KEY: CellTestWidget.KEY, type: CellTestWidget }
),
]
}
```

最后,将 `CellWidgetModule` 注册到根模块下即可。
18 changes: 18 additions & 0 deletions packages/abc/cell/provide.ts
@@ -0,0 +1,18 @@
import { ENVIRONMENT_INITIALIZER, EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core';

import type { NzSafeAny } from 'ng-zorro-antd/core/types';

import { CellService } from './cell.service';

export function provideCellWidgets(...widgets: Array<{ KEY: string; type: NzSafeAny }>): EnvironmentProviders {
return makeEnvironmentProviders([
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useValue: () => {
const srv = inject(CellService);
widgets.forEach(widget => srv.registerWidget(widget.KEY, widget.type));
}
}
]);
}
7 changes: 4 additions & 3 deletions src/app/app.config.ts
Expand Up @@ -21,6 +21,7 @@ import { provideNuMonacoEditorConfig } from '@ng-util/monaco-editor';
import { zhCN as dateLang } from 'date-fns/locale';
import { provideTinymce } from 'ngx-tinymce';

import { provideCellWidgets } from '@delon/abc/cell';
import { mockInterceptor, provideDelonMockConfig } from '@delon/mock';
import { ALAIN_I18N_TOKEN, provideAlain } from '@delon/theme';
import { AlainConfig } from '@delon/util/config';
Expand All @@ -32,7 +33,7 @@ import { I18NService, StartupService } from '@core';
import { CustomErrorHandler } from './core/error-handler';
import { EXAMPLE_COMPONENTS } from './routes/gen/examples';
import { routes } from './routes/routes';
import { CellWidgetModule } from './shared/cell-widget/module';
import { CELL_WIDGETS } from './shared/cell-widget';
import { IconComponent } from './shared/components/icon/icon.component';
import { JsonSchemaModule } from './shared/json-schema/json-schema.module';
import { STWidgetModule } from './shared/st-widget/st-widget.module';
Expand Down Expand Up @@ -100,16 +101,16 @@ export const appConfig: ApplicationConfig = {
provideHttpClient(withInterceptors([mockInterceptor]), withFetch()),
provideAnimations(),
provideRouter(routes, withComponentInputBinding()),
// provideClientHydration(),
// provideClientHydration(), // 暂时不开启水合,除了编译时间长,还有就是对DOM要求比较高
provideAlain(alainConfig),
provideNzConfig(ngZorroConfig),
provideDelonMockConfig({ data: MOCKDATA }),
provideNuMonacoEditorConfig({ defaultOptions: { scrollBeyondLastLine: false } }),
provideTinymce({
baseURL: 'https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.2/'
}),
provideCellWidgets(...CELL_WIDGETS),
importProvidersFrom(
CellWidgetModule,
JsonSchemaModule,
STWidgetModule,
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })
Expand Down
3 changes: 3 additions & 0 deletions src/app/shared/cell-widget/index.ts
@@ -0,0 +1,3 @@
import { CellTestWidget } from './test';

export const CELL_WIDGETS = [{ KEY: CellTestWidget.KEY, type: CellTestWidget }];