From f11cf78948be3d99e6d5d852bd169f178b3c0d1d Mon Sep 17 00:00:00 2001 From: Joe Skeen Date: Thu, 1 Feb 2024 22:36:12 -0700 Subject: [PATCH 1/4] generate input/output bindings to the GridJS package --- apps/demo/project.json | 2 +- apps/demo/src/app/app.component.css | 0 apps/demo/src/app/app.component.html | 2 - apps/demo/src/app/app.component.ts | 124 ++++- apps/demo/src/app/app.config.ts | 7 - apps/demo/src/app/app.routes.ts | 3 - apps/demo/src/main.ts | 3 +- apps/demo/src/styles.css | 1 - package.json | 2 + packages/gridjs-angular/README.md | 35 +- packages/gridjs-angular/project.json | 7 + packages/gridjs-angular/src/index.ts | 2 +- packages/gridjs-angular/src/lib/constants.ts | 35 -- .../src/lib/gridjs-angular.component.ts | 118 ++--- .../src/lib/gridjs-binding-base.ts | 486 ++++++++++++++++++ pnpm-lock.yaml | 15 + scripts/gridjs-binding-base.mustache | 80 +++ scripts/update-bindings.mjs | 121 +++++ 18 files changed, 883 insertions(+), 160 deletions(-) delete mode 100644 apps/demo/src/app/app.component.css delete mode 100644 apps/demo/src/app/app.component.html delete mode 100644 apps/demo/src/app/app.config.ts delete mode 100644 apps/demo/src/app/app.routes.ts delete mode 100644 apps/demo/src/styles.css delete mode 100644 packages/gridjs-angular/src/lib/constants.ts create mode 100644 packages/gridjs-angular/src/lib/gridjs-binding-base.ts create mode 100644 scripts/gridjs-binding-base.mustache create mode 100644 scripts/update-bindings.mjs diff --git a/apps/demo/project.json b/apps/demo/project.json index 7683b09..7ee2240 100644 --- a/apps/demo/project.json +++ b/apps/demo/project.json @@ -16,7 +16,7 @@ "polyfills": ["zone.js"], "tsConfig": "apps/demo/tsconfig.app.json", "assets": ["apps/demo/src/favicon.ico", "apps/demo/src/assets"], - "styles": ["apps/demo/src/styles.css"], + "styles": ["node_modules/gridjs/dist/theme/mermaid.min.css"], "scripts": [] }, "configurations": { diff --git a/apps/demo/src/app/app.component.css b/apps/demo/src/app/app.component.css deleted file mode 100644 index e69de29..0000000 diff --git a/apps/demo/src/app/app.component.html b/apps/demo/src/app/app.component.html deleted file mode 100644 index a57ab4e..0000000 --- a/apps/demo/src/app/app.component.html +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/apps/demo/src/app/app.component.ts b/apps/demo/src/app/app.component.ts index 394c181..979ec24 100644 --- a/apps/demo/src/app/app.component.ts +++ b/apps/demo/src/app/app.component.ts @@ -1,19 +1,123 @@ import { Component } from '@angular/core'; -import { RouterModule } from '@angular/router'; import { GridJsAngularComponent } from 'gridjs-angular'; -import 'gridjs/dist/theme/mermaid.css'; +import { TData } from 'gridjs/dist/src/types'; @Component({ - standalone: true, - imports: [GridJsAngularComponent, RouterModule], selector: 'gridjs-angular-root', - templateUrl: './app.component.html', - styleUrl: './app.component.css', + standalone: true, + imports: [GridJsAngularComponent], + template: ``, }) export class AppComponent { + onLoad = (event: any) => console.log('Grid loaded', event); + onBeforeLoad = (event: any) => console.log('Before grid loaded', event); + onReady = (event: any) => console.log('Grid ready', event); + onCellClick = (event: any) => console.log('Grid cell clicked', event); + onRowClick = (event: any) => console.log('Grid row clicked', event); + columns = ['Name', 'Email', 'Phone Number']; - data = [ - ['John', 'john@example.com', '(353) 01 222 3333'], - ['Mark', 'mark@gmail.com', '(01) 22 888 4444'], - ]; + data: TData = [ + { + name: 'John Doe', + email: 'john.doe@example.com', + phone_number: '555-123-4567', + }, + { + name: 'Jane Smith', + email: 'jane.smith@example.com', + phone_number: '555-543-2109', + }, + { + name: 'Mike Johnson', + email: 'm.johnson@example.com', + phone_number: '555-987-6543', + }, + { + name: 'Sara Lee', + email: 's.lee@example.com', + phone_number: '555-345-6789', + }, + { + name: 'William Brown', + email: 'w.brown@example.com', + phone_number: '555-234-5678', + }, + { + name: 'Mary White', + email: 'mary.white@example.com', + phone_number: '555-765-4321', + }, + { + name: 'Daniel Green', + email: 'd.green@example.com', + phone_number: '555-456-7890', + }, + { + name: 'Emma Black', + email: 'emma.black@example.com', + phone_number: '555-876-5432', + }, + { + name: 'James Young', + email: 'j.young@example.com', + phone_number: '555-678-9012', + }, + { + name: 'Grace Kim', + email: 'grace.kim@example.com', + phone_number: '555-321-0987', + }, + { + name: 'Thomas Lee', + email: 'thomas.lee@example.com', + phone_number: '555-901-2345', + }, + { + name: 'Elizabeth Davis', + email: 'elizabeth.davis@example.com', + phone_number: '555-432-1098', + }, + { + name: 'Michael Harris', + email: 'm.harris@example.com', + phone_number: '555-789-0123', + }, + { + name: 'Laura Nguyen', + email: 'laura.nguyen@example.com', + phone_number: '555-234-5678', + }, + { + name: 'Kenneth Wilson', + email: 'k.wilson@example.com', + phone_number: '555-678-9012', + }, + { + name: 'Nancy Moore', + email: 'nancy.moore@example.com', + phone_number: '555-321-0987', + }, + { + name: 'Andrew Taylor', + email: 'andrew.taylor@example.com', + phone_number: '555-901-2345', + }, + { + name: 'Steven Thompson', + email: 's.thompson@example.com', + phone_number: '555-432-1098', + }, + ].map((x) => [x.name, x.email, x.phone_number]); + } diff --git a/apps/demo/src/app/app.config.ts b/apps/demo/src/app/app.config.ts deleted file mode 100644 index ed40494..0000000 --- a/apps/demo/src/app/app.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ApplicationConfig } from '@angular/core'; -import { provideRouter } from '@angular/router'; -import { appRoutes } from './app.routes'; - -export const appConfig: ApplicationConfig = { - providers: [provideRouter(appRoutes)], -}; diff --git a/apps/demo/src/app/app.routes.ts b/apps/demo/src/app/app.routes.ts deleted file mode 100644 index 8762dfe..0000000 --- a/apps/demo/src/app/app.routes.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Route } from '@angular/router'; - -export const appRoutes: Route[] = []; diff --git a/apps/demo/src/main.ts b/apps/demo/src/main.ts index 514c89a..57c05db 100644 --- a/apps/demo/src/main.ts +++ b/apps/demo/src/main.ts @@ -1,7 +1,6 @@ import { bootstrapApplication } from '@angular/platform-browser'; -import { appConfig } from './app/app.config'; import { AppComponent } from './app/app.component'; -bootstrapApplication(AppComponent, appConfig).catch((err) => +bootstrapApplication(AppComponent).catch((err) => console.error(err) ); diff --git a/apps/demo/src/styles.css b/apps/demo/src/styles.css deleted file mode 100644 index 90d4ee0..0000000 --- a/apps/demo/src/styles.css +++ /dev/null @@ -1 +0,0 @@ -/* You can add global styles to this file, and also import other style files */ diff --git a/package.json b/package.json index 5e1a79b..e242ce7 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/parser": "^6.20.0", "autoprefixer": "^10.4.17", + "change-case": "^5.4.2", "eslint": "~8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-playwright": "^0.22.1", @@ -56,6 +57,7 @@ "jest-environment-jsdom": "^29.7.0", "jest-preset-angular": "~14.0.0", "jsonc-eslint-parser": "^2.4.0", + "mustache": "^4.2.0", "ng-packagr": "~17.1.2", "nx": "17.3.1", "postcss": "^8.4.33", diff --git a/packages/gridjs-angular/README.md b/packages/gridjs-angular/README.md index e0fcd27..e642626 100644 --- a/packages/gridjs-angular/README.md +++ b/packages/gridjs-angular/README.md @@ -2,6 +2,9 @@ Angular wrapper for [Grid.js](https://github.com/grid-js/gridjs) +[![gridjs-angular repository on GitHub](https://img.shields.io/badge/github-gridjs--angular-green?logo=github&link=https%3A%2F%2Fgithub.com%2Fgrid-js%2Fgridjs-angular)](https://github.com/grid-js/gridjs-angular) +![GridJS peer Dependency Version](https://img.shields.io/npm/dependency-version/gridjs-angular/peer/gridjs) + ## Install ```bash @@ -27,7 +30,7 @@ In your component template ```ts import { Component } from '@angular/core'; -import { UserConfig } from 'gridjs'; +import { Config } from 'gridjs'; @Component({ template: ` @@ -41,7 +44,7 @@ import { UserConfig } from 'gridjs'; ` }) class ExampleComponent { - public gridConfig: UserConfig = { + public gridConfig: Config = { columns: ['Name', 'Email', 'Phone Number'], data: [ ['John', 'john@example.com', '(353) 01 222 3333'], @@ -70,13 +73,10 @@ class ExampleComponent { } ``` -Finally don't forget to add gridjs theme in your index.html +Finally don't forget to add gridjs theme to your `angular.json` file, or import it some other way. -```html - +```json +styles: ["node_modules/gridjs/dist/theme/mermaid.min.css"] ``` ## Inputs @@ -89,7 +89,7 @@ Finally don't forget to add gridjs theme in your index.html ## Outputs -- You can pass all Grid.js events as outputs with a little difference `load` event renamed to `beforeLoad`. See [Grid.js Events](https://gridjs.io/docs/examples/event-handler) +- You can bind to all Grid.js events as outputs. Additionally, the `load` event can also be accessed via `gridLoad` (to avoid conflict with the native DOM `load` event). See [Grid.js Events](https://gridjs.io/docs/examples/event-handler) ### Can I Grid.js rendering helpers? Yes @@ -114,4 +114,19 @@ Finally don't forget to add gridjs theme in your index.html } ``` -### Can I use Angular components in plugins, formatters, etc? Not yet +### Can I use Angular template syntax in plugins, formatters, etc? + +Not currently. + +You can't use Angular template syntax in Grid.js plugins, formatters, etc. because they cannot be connected to Angular's change detection system. You can use `h` function or `html` function to create custom HTML for your grid. + +## Development + +The `gridjs-angular` repository is a monorepo that uses [Nx](https://nx.dev) and [pnpm](https://pnpm.io/). + +### Useful commands + +- `pnpm install` - Install all dependencies +- `nx serve demo` - Run demo app +- `nx migrate latest` - Update Nx to the latest version, and upgrade all packages from package.json to their latest version +- `nx update-bindings gridjs-angular` - Update the input and output bindings from GridJS to the Angular component. This command should be run after updating the GridJS version. diff --git a/packages/gridjs-angular/project.json b/packages/gridjs-angular/project.json index cb65ece..f791540 100644 --- a/packages/gridjs-angular/project.json +++ b/packages/gridjs-angular/project.json @@ -31,6 +31,13 @@ }, "lint": { "executor": "@nx/eslint:lint" + }, + "update-bindings": { + "executor": "nx:run-commands", + "outputs": ["{workspaceRoot}/packages/gridjs-angular/src/lib/gridjs-binding-base.ts"], + "options": { + "command": "node scripts/update-bindings.mjs" + } } } } diff --git a/packages/gridjs-angular/src/index.ts b/packages/gridjs-angular/src/index.ts index 2a1c485..96e1656 100644 --- a/packages/gridjs-angular/src/index.ts +++ b/packages/gridjs-angular/src/index.ts @@ -1,2 +1,2 @@ -export * from './lib/constants'; export * from './lib/gridjs-angular.component'; +export { GRID_EVENTS as GRID_JS_EVENTS } from './lib/gridjs-binding-base'; diff --git a/packages/gridjs-angular/src/lib/constants.ts b/packages/gridjs-angular/src/lib/constants.ts deleted file mode 100644 index 7402548..0000000 --- a/packages/gridjs-angular/src/lib/constants.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Config } from 'gridjs'; -import { GridEvents } from 'gridjs/dist/src/events'; - -export const GRID_JS_EVENTS: (keyof GridEvents)[] = [ - 'beforeLoad', - 'cellClick', - 'load', - 'rowClick', - 'ready', -]; - -export const GRID_JS_PROPS: (keyof Config)[] = [ - 'eventEmitter', - 'plugin', - 'data', - 'server', - 'header', - 'from', - 'storage', - 'pipeline', - 'autoWidth', - 'width', - 'height', - 'translator', - 'style', - 'className', - 'fixedHeader', - 'columns', - 'search', - 'pagination', - 'sort', - 'language', - 'plugins', - 'processingThrottleMs', -]; diff --git a/packages/gridjs-angular/src/lib/gridjs-angular.component.ts b/packages/gridjs-angular/src/lib/gridjs-angular.component.ts index 4cce13c..357e5c2 100644 --- a/packages/gridjs-angular/src/lib/gridjs-angular.component.ts +++ b/packages/gridjs-angular/src/lib/gridjs-angular.component.ts @@ -3,131 +3,73 @@ import { Component, ElementRef, EventEmitter, - Input, - OnChanges, OnDestroy, Output, - ViewEncapsulation, } from '@angular/core'; -import { CommonModule } from '@angular/common'; import { Config, Grid } from 'gridjs'; -import { GRID_JS_EVENTS, GRID_JS_PROPS } from './constants'; +import { GRID_EVENTS, GridJsAngularBindingBase } from './gridjs-binding-base'; +import { GridEvents } from 'gridjs/dist/src/events'; + +/** only properties that exist on the Config interface (not the Config class) */ +type EventName = keyof GridEvents; +type EventHandler = (...args: any[]) => void; -type GridJsAngularComponentProps = Omit< - Partial, - 'instance' | 'store' | 'assign' | 'update' ->; @Component({ selector: 'gridjs-angular', - template: '', standalone: true, - imports: [CommonModule], - encapsulation: ViewEncapsulation.None, + template: '', }) export class GridJsAngularComponent - implements AfterViewInit, OnChanges, OnDestroy, GridJsAngularComponentProps + extends GridJsAngularBindingBase + implements AfterViewInit, OnDestroy { - private nativeElement: HTMLElement; - private instance?: Grid; - private initialized = false; - private listeners: Map void> = new Map(); - @Input() config?: Partial; - // TODO: auto generate Inputs/Output to easily sync with grid-js main package - // props - @Input() plugins: Config['plugins'] = []; - @Input() eventEmitter?: Config['eventEmitter']; - @Input() plugin?: Config['plugin']; - @Input() data: Config['data']; - @Input() server: Config['server']; - @Input() header: Config['header']; - @Input() from?: Config['from']; - @Input() storage?: Config['storage']; - @Input() pipeline?: Config['pipeline']; - @Input() autoWidth?: Config['autoWidth']; - @Input() width?: Config['width']; - @Input() height?: Config['height']; - @Input() translator?: Config['translator']; - @Input() style: Config['style']; - @Input() className: Config['className']; - @Input() fixedHeader?: Config['fixedHeader']; - @Input() columns?: Config['columns']; - @Input() search?: Config['search']; - @Input() pagination?: Config['pagination']; - @Input() sort?: Config['sort']; - @Input() language?: Config['language']; - @Input() resizable?: Config['resizable']; - @Input() processingThrottleMs?: Config['processingThrottleMs']; + private readonly listeners = new Map(); - // events - @Output() beforeLoad: EventEmitter = new EventEmitter(true); - // renamed load event to avoid conflict with native load event - @Output() gridLoad: EventEmitter = new EventEmitter(true); - @Output() cellClick: EventEmitter = new EventEmitter(true); - @Output() rowClick: EventEmitter = new EventEmitter(true); - @Output() ready: EventEmitter = new EventEmitter(true); + /** alias of `load` event due to possible conflict with native load event */ + @Output() readonly gridLoad = this.load; - constructor(private elementDef: ElementRef) { - this.nativeElement = this.elementDef.nativeElement; + constructor(private readonly host: ElementRef) { + super(); } ngAfterViewInit(): void { - this.instance = new Grid(this.getConfig(this.config ?? {})); + const instance = new Grid(this.config()); + this.instance.set(instance); this.registerEvents(); - this.instance.render(this.nativeElement); - this.initialized = true; - } - - ngOnChanges(): void { - if (this.initialized) { - this.updateConfig(this.config); - } + instance.render(this.host.nativeElement); } ngOnDestroy(): void { - if (this.initialized) { - if (this.instance) { - this.unregisterEvents(); - this.instance = undefined; - } + if (this.instance()) { + this.unregisterEvents(); + this.instance.set(undefined); } } + // public api to interact with grid instance getGridInstance() { - return this.instance; + return this.instance(); } updateConfig(config: Partial = {}) { - this.instance?.updateConfig(this.getConfig(config)).forceRender(); + this.gridConfig.set(config); } private registerEvents() { - for (const event of GRID_JS_EVENTS) { - const emitter = - event === 'load' - ? this.gridLoad - : >(this)[event]; + for (const event of GRID_EVENTS) { + const emitter = (this)[event] as EventEmitter; + if (!emitter) { + continue; + } const listener = (...args: any[]) => emitter.emit(args); this.listeners.set(event, listener); - if (emitter) { - this.instance?.on(event as any, listener); - } + this.instance()?.on(event, listener); } } private unregisterEvents() { for (const [event, listener] of this.listeners.entries()) { - this.instance?.off(event as any, listener); - } - } - - private getConfig(config: Partial = {}) { - const newConfig = structuredClone(config); - for (const [key, value] of Object.entries(this)) { - if (GRID_JS_PROPS.includes(key as any)) { - (newConfig as any)[key] = value; - } + this.instance()?.off(event, listener); } - this.config = newConfig; - return newConfig; } } diff --git a/packages/gridjs-angular/src/lib/gridjs-binding-base.ts b/packages/gridjs-angular/src/lib/gridjs-binding-base.ts new file mode 100644 index 0000000..307294d --- /dev/null +++ b/packages/gridjs-angular/src/lib/gridjs-binding-base.ts @@ -0,0 +1,486 @@ +// This file is generated automatically using "nx update-bindings gridjs-angular" +// Do not edit this file manually +import { Config } from 'gridjs'; +import { GridEvents } from 'gridjs/dist/src/events'; +import { Component, Input, Output, EventEmitter, signal, computed, effect } from '@angular/core'; +import 'preact'; + +type GridEventsEmitter = Record>; + +export const GRID_EVENTS: Array = [ + 'beforeLoad', + 'load', + 'ready', + 'cellClick', + 'rowClick', +]; + +@Component({ template: '' }) +export abstract class GridJsAngularBindingBase implements GridEventsEmitter { + constructor() { + effect(() => { + const instanceVal = this.instance(); + const instance = this.instance(); + if (instanceVal === undefined || !instance) { + return; + } + instance.updateConfig({ instance: instanceVal }); + instance.forceRender(); + }); + effect(() => { + const storeVal = this.store(); + const instance = this.instance(); + if (storeVal === undefined || !instance) { + return; + } + instance.updateConfig({ store: storeVal }); + instance.forceRender(); + }); + effect(() => { + const eventEmitterVal = this.eventEmitter(); + const instance = this.instance(); + if (eventEmitterVal === undefined || !instance) { + return; + } + instance.updateConfig({ eventEmitter: eventEmitterVal }); + instance.forceRender(); + }); + effect(() => { + const pluginVal = this.plugin(); + const instance = this.instance(); + if (pluginVal === undefined || !instance) { + return; + } + instance.updateConfig({ plugin: pluginVal }); + instance.forceRender(); + }); + effect(() => { + const containerVal = this.container(); + const instance = this.instance(); + if (containerVal === undefined || !instance) { + return; + } + instance.updateConfig({ container: containerVal }); + instance.forceRender(); + }); + effect(() => { + const tableRefVal = this.tableRef(); + const instance = this.instance(); + if (tableRefVal === undefined || !instance) { + return; + } + instance.updateConfig({ tableRef: tableRefVal }); + instance.forceRender(); + }); + effect(() => { + const dataVal = this.data(); + const instance = this.instance(); + if (dataVal === undefined || !instance) { + return; + } + instance.updateConfig({ data: dataVal }); + instance.forceRender(); + }); + effect(() => { + const serverVal = this.server(); + const instance = this.instance(); + if (serverVal === undefined || !instance) { + return; + } + instance.updateConfig({ server: serverVal }); + instance.forceRender(); + }); + effect(() => { + const headerVal = this.header(); + const instance = this.instance(); + if (headerVal === undefined || !instance) { + return; + } + instance.updateConfig({ header: headerVal }); + instance.forceRender(); + }); + effect(() => { + const fromVal = this.from(); + const instance = this.instance(); + if (fromVal === undefined || !instance) { + return; + } + instance.updateConfig({ from: fromVal }); + instance.forceRender(); + }); + effect(() => { + const storageVal = this.storage(); + const instance = this.instance(); + if (storageVal === undefined || !instance) { + return; + } + instance.updateConfig({ storage: storageVal }); + instance.forceRender(); + }); + effect(() => { + const processingThrottleMsVal = this.processingThrottleMs(); + const instance = this.instance(); + if (processingThrottleMsVal === undefined || !instance) { + return; + } + instance.updateConfig({ processingThrottleMs: processingThrottleMsVal }); + instance.forceRender(); + }); + effect(() => { + const pipelineVal = this.pipeline(); + const instance = this.instance(); + if (pipelineVal === undefined || !instance) { + return; + } + instance.updateConfig({ pipeline: pipelineVal }); + instance.forceRender(); + }); + effect(() => { + const autoWidthVal = this.autoWidth(); + const instance = this.instance(); + if (autoWidthVal === undefined || !instance) { + return; + } + instance.updateConfig({ autoWidth: autoWidthVal }); + instance.forceRender(); + }); + effect(() => { + const widthVal = this.width(); + const instance = this.instance(); + if (widthVal === undefined || !instance) { + return; + } + instance.updateConfig({ width: widthVal }); + instance.forceRender(); + }); + effect(() => { + const heightVal = this.height(); + const instance = this.instance(); + if (heightVal === undefined || !instance) { + return; + } + instance.updateConfig({ height: heightVal }); + instance.forceRender(); + }); + effect(() => { + const paginationVal = this.pagination(); + const instance = this.instance(); + if (paginationVal === undefined || !instance) { + return; + } + instance.updateConfig({ pagination: paginationVal }); + instance.forceRender(); + }); + effect(() => { + const sortVal = this.sort(); + const instance = this.instance(); + if (sortVal === undefined || !instance) { + return; + } + instance.updateConfig({ sort: sortVal }); + instance.forceRender(); + }); + effect(() => { + const translatorVal = this.translator(); + const instance = this.instance(); + if (translatorVal === undefined || !instance) { + return; + } + instance.updateConfig({ translator: translatorVal }); + instance.forceRender(); + }); + effect(() => { + const fixedHeaderVal = this.fixedHeader(); + const instance = this.instance(); + if (fixedHeaderVal === undefined || !instance) { + return; + } + instance.updateConfig({ fixedHeader: fixedHeaderVal }); + instance.forceRender(); + }); + effect(() => { + const resizableVal = this.resizable(); + const instance = this.instance(); + if (resizableVal === undefined || !instance) { + return; + } + instance.updateConfig({ resizable: resizableVal }); + instance.forceRender(); + }); + effect(() => { + const columnsVal = this.columns(); + const instance = this.instance(); + if (columnsVal === undefined || !instance) { + return; + } + instance.updateConfig({ columns: columnsVal }); + instance.forceRender(); + }); + effect(() => { + const searchVal = this.search(); + const instance = this.instance(); + if (searchVal === undefined || !instance) { + return; + } + instance.updateConfig({ search: searchVal }); + instance.forceRender(); + }); + effect(() => { + const languageVal = this.language(); + const instance = this.instance(); + if (languageVal === undefined || !instance) { + return; + } + instance.updateConfig({ language: languageVal }); + instance.forceRender(); + }); + effect(() => { + const pluginsVal = this.plugins(); + const instance = this.instance(); + if (pluginsVal === undefined || !instance) { + return; + } + instance.updateConfig({ plugins: pluginsVal }); + instance.forceRender(); + }); + effect(() => { + const styleVal = this.style(); + const instance = this.instance(); + if (styleVal === undefined || !instance) { + return; + } + instance.updateConfig({ style: styleVal }); + instance.forceRender(); + }); + effect(() => { + const classNameVal = this.className(); + const instance = this.instance(); + if (classNameVal === undefined || !instance) { + return; + } + instance.updateConfig({ className: classNameVal }); + instance.forceRender(); + }); + } + + readonly instance = signal(undefined); + @Input({alias: 'instance'}) + set _instance(value: Config['instance'] | undefined) { + this.instance.set(value); + } + + readonly store = signal(undefined); + @Input({alias: 'store'}) + set _store(value: Config['store'] | undefined) { + this.store.set(value); + } + + readonly eventEmitter = signal(undefined); + @Input({alias: 'eventEmitter'}) + set _eventEmitter(value: Config['eventEmitter'] | undefined) { + this.eventEmitter.set(value); + } + + readonly plugin = signal(undefined); + @Input({alias: 'plugin'}) + set _plugin(value: Config['plugin'] | undefined) { + this.plugin.set(value); + } + + readonly container = signal(undefined); + @Input({alias: 'container'}) + set _container(value: Config['container'] | undefined) { + this.container.set(value); + } + + readonly tableRef = signal(undefined); + @Input({alias: 'tableRef'}) + set _tableRef(value: Config['tableRef'] | undefined) { + this.tableRef.set(value); + } + + readonly data = signal(undefined); + @Input({alias: 'data'}) + set _data(value: Config['data'] | undefined) { + this.data.set(value); + } + + readonly server = signal(undefined); + @Input({alias: 'server'}) + set _server(value: Config['server'] | undefined) { + this.server.set(value); + } + + readonly header = signal(undefined); + @Input({alias: 'header'}) + set _header(value: Config['header'] | undefined) { + this.header.set(value); + } + + readonly from = signal(undefined); + @Input({alias: 'from'}) + set _from(value: Config['from'] | undefined) { + this.from.set(value); + } + + readonly storage = signal(undefined); + @Input({alias: 'storage'}) + set _storage(value: Config['storage'] | undefined) { + this.storage.set(value); + } + + readonly processingThrottleMs = signal(undefined); + @Input({alias: 'processingThrottleMs'}) + set _processingThrottleMs(value: Config['processingThrottleMs'] | undefined) { + this.processingThrottleMs.set(value); + } + + readonly pipeline = signal(undefined); + @Input({alias: 'pipeline'}) + set _pipeline(value: Config['pipeline'] | undefined) { + this.pipeline.set(value); + } + + readonly autoWidth = signal(undefined); + @Input({alias: 'autoWidth'}) + set _autoWidth(value: Config['autoWidth'] | undefined) { + this.autoWidth.set(value); + } + + readonly width = signal(undefined); + @Input({alias: 'width'}) + set _width(value: Config['width'] | undefined) { + this.width.set(value); + } + + readonly height = signal(undefined); + @Input({alias: 'height'}) + set _height(value: Config['height'] | undefined) { + this.height.set(value); + } + + readonly pagination = signal(undefined); + @Input({alias: 'pagination'}) + set _pagination(value: Config['pagination'] | undefined) { + this.pagination.set(value); + } + + readonly sort = signal(undefined); + @Input({alias: 'sort'}) + set _sort(value: Config['sort'] | undefined) { + this.sort.set(value); + } + + readonly translator = signal(undefined); + @Input({alias: 'translator'}) + set _translator(value: Config['translator'] | undefined) { + this.translator.set(value); + } + + readonly fixedHeader = signal(undefined); + @Input({alias: 'fixedHeader'}) + set _fixedHeader(value: Config['fixedHeader'] | undefined) { + this.fixedHeader.set(value); + } + + readonly resizable = signal(undefined); + @Input({alias: 'resizable'}) + set _resizable(value: Config['resizable'] | undefined) { + this.resizable.set(value); + } + + readonly columns = signal(undefined); + @Input({alias: 'columns'}) + set _columns(value: Config['columns'] | undefined) { + this.columns.set(value); + } + + readonly search = signal(undefined); + @Input({alias: 'search'}) + set _search(value: Config['search'] | undefined) { + this.search.set(value); + } + + readonly language = signal(undefined); + @Input({alias: 'language'}) + set _language(value: Config['language'] | undefined) { + this.language.set(value); + } + + readonly plugins = signal(undefined); + @Input({alias: 'plugins'}) + set _plugins(value: Config['plugins'] | undefined) { + this.plugins.set(value); + } + + readonly style = signal(undefined); + @Input({alias: 'style'}) + set _style(value: Config['style'] | undefined) { + this.style.set(value); + } + + readonly className = signal(undefined); + @Input({alias: 'className'}) + set _className(value: Config['className'] | undefined) { + this.className.set(value); + } + + readonly gridConfig = signal | undefined>(undefined); + @Input({alias: 'gridConfig'}) + set _gridConfig(value: Partial | undefined) { + this.gridConfig.set(value); + } + + readonly config = computed>(() => { + const configValue: Partial = { + instance: this.instance(), + store: this.store(), + eventEmitter: this.eventEmitter(), + plugin: this.plugin(), + container: this.container(), + tableRef: this.tableRef(), + data: this.data(), + server: this.server(), + header: this.header(), + from: this.from(), + storage: this.storage(), + processingThrottleMs: this.processingThrottleMs(), + pipeline: this.pipeline(), + autoWidth: this.autoWidth(), + width: this.width(), + height: this.height(), + pagination: this.pagination(), + sort: this.sort(), + translator: this.translator(), + fixedHeader: this.fixedHeader(), + resizable: this.resizable(), + columns: this.columns(), + search: this.search(), + language: this.language(), + plugins: this.plugins(), + style: this.style(), + className: this.className(), + }; + for(let key in configValue) { + const keyName = key as keyof Config; + if (configValue[keyName] === undefined) { + delete configValue[keyName]; + } + } + return { + ...this.gridConfig(), + ...configValue + }; + }); + + @Output() + readonly beforeLoad = new EventEmitter(); + @Output() + readonly load = new EventEmitter(); + @Output() + readonly ready = new EventEmitter(); + @Output() + readonly cellClick = new EventEmitter(); + @Output() + readonly rowClick = new EventEmitter(); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90a8d7c..89c566a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,6 +124,9 @@ devDependencies: autoprefixer: specifier: ^10.4.17 version: 10.4.17(postcss@8.4.33) + change-case: + specifier: ^5.4.2 + version: 5.4.2 eslint: specifier: ~8.56.0 version: 8.56.0 @@ -145,6 +148,9 @@ devDependencies: jsonc-eslint-parser: specifier: ^2.4.0 version: 2.4.0 + mustache: + specifier: ^4.2.0 + version: 4.2.0 ng-packagr: specifier: ~17.1.2 version: 17.1.2(@angular/compiler-cli@17.1.2)(tslib@2.6.2)(typescript@5.3.3) @@ -5996,6 +6002,10 @@ packages: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + /change-case@5.4.2: + resolution: {integrity: sha512-WB3UiTDpT+vrTilAWaJS4gaIH/jc1He4H9f6erQvraUYas90uWT0JOYFkG1imdNv710XJ6gJvqynrgOHc4ihDA==} + dev: true + /char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -9091,6 +9101,11 @@ packages: dns-packet: 5.6.1 thunky: 1.1.0 + /mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + dev: true + /mute-stream@1.0.0: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} diff --git a/scripts/gridjs-binding-base.mustache b/scripts/gridjs-binding-base.mustache new file mode 100644 index 0000000..fd3d7a4 --- /dev/null +++ b/scripts/gridjs-binding-base.mustache @@ -0,0 +1,80 @@ +// This file is generated automatically using "nx update-bindings gridjs-angular" +// Do not edit this file manually +{{#inputTypes}} +import { {{typeName}} } from '{{&importPath}}'; +{{/inputTypes}} +{{#outputTypes}} +import { {{typeName}} } from '{{&importPath}}'; +{{/outputTypes}} +import { Component, Input, Output, EventEmitter, signal, computed, effect } from '@angular/core'; +import 'preact'; + +{{#outputTypes}} +type {{typeName}}Emitter = Record>; +{{/outputTypes}} + +{{#outputTypes}} +export const {{#constantCase}}{{typeName}}{{/constantCase}}: Array = [ + {{#members}} + '{{.}}', + {{/members}} +]; +{{/outputTypes}} + +@Component({ template: '' }) +export abstract class GridJsAngularBindingBase implements GridEventsEmitter { + constructor() { + {{#inputTypes}}{{#members}} + effect(() => { + const {{.}}Val = this.{{.}}(); + const instance = this.instance(); + if ({{.}}Val === undefined || !instance) { + return; + } + instance.updateConfig({ {{.}}: {{.}}Val }); + instance.forceRender(); + }); + {{/members}}{{/inputTypes}} + } + +{{#inputTypes}} + {{#members}} + readonly {{.}} = signal<{{typeName}}['{{.}}'] | undefined>(undefined); + @Input({alias: '{{.}}'}) + set _{{.}}(value: {{typeName}}['{{.}}'] | undefined) { + this.{{.}}.set(value); + } + + {{/members}} + readonly gridConfig = signal | undefined>(undefined); + @Input({alias: 'gridConfig'}) + set _gridConfig(value: Partial<{{typeName}}> | undefined) { + this.gridConfig.set(value); + } + + readonly {{#camelCase}}{{typeName}}{{/camelCase}} = computed>(() => { + const {{#camelCase}}{{typeName}}Value{{/camelCase}}: Partial<{{typeName}}> = { + {{#members}} + {{.}}: this.{{.}}(), + {{/members}} + }; + for(let key in {{#camelCase}}{{typeName}}Value{{/camelCase}}) { + const keyName = key as keyof {{typeName}}; + if ({{#camelCase}}{{typeName}}Value{{/camelCase}}[keyName] === undefined) { + delete {{#camelCase}}{{typeName}}Value{{/camelCase}}[keyName]; + } + } + return { + ...this.gridConfig(), + ...{{#camelCase}}{{typeName}}Value{{/camelCase}} + }; + }); +{{/inputTypes}} + +{{#outputTypes}} + {{#members}} + @Output() + readonly {{.}} = new EventEmitter(); + {{/members}} +{{/outputTypes}} +} diff --git a/scripts/update-bindings.mjs b/scripts/update-bindings.mjs new file mode 100644 index 0000000..8f54260 --- /dev/null +++ b/scripts/update-bindings.mjs @@ -0,0 +1,121 @@ +import ts from 'typescript'; +import { basename } from 'path'; +import { readFileSync, writeFileSync } from 'fs'; +import Mustache from 'mustache'; +import { camelCase, constantCase } from 'change-case'; + +const config = { + sourceTypings: [ + { + path: 'node_modules/gridjs/dist/src/config.d.ts', + importPath: 'gridjs', + bindingTypes: 'input', + }, + { + path: 'node_modules/gridjs/dist/src/events.d.ts', + importPath: 'gridjs/dist/src/events', + bindingTypes: 'output', + }, + ], + bindingClassTemplate: 'scripts/gridjs-binding-base.mustache', + outputPath: 'packages/gridjs-angular/src/lib/gridjs-binding-base.ts', +}; + +const mustacheHelpers = { + camelCase: () => (text, render) => camelCase(render(text)), + constantCase: () => (text, render) => constantCase(render(text)), + noTrailingComma: () => (text, render) => { + const result = render(text); + return result.endsWith(',') ? result.slice(0, -1) : result; + }, +}; +const template = readFileSync(config.bindingClassTemplate, 'utf-8'); + +const types = extractTypeInformation(config.sourceTypings); + +const contents = Mustache.render(template, { + inputTypes: types.filter((t) => t.bindingTypes === 'input'), + outputTypes: types.filter((t) => t.bindingTypes === 'output'), + ...mustacheHelpers, +}); +writeFileSync(config.outputPath, contents); + +function extractTypeInformation(sourceTypings) { + return sourceTypings.map(({ path, bindingTypes, importPath }) => { + const program = ts.createProgram({ + rootNames: [path], + options: {}, + }); + const checker = program.getTypeChecker(); + const sourceFile = program.getSourceFile(path); + if (!sourceFile) { + return; + } + + return ts.forEachChild(sourceFile, (node) => { + if (ts.isInterfaceDeclaration(node)) { + const symbol = checker.getSymbolAtLocation(node.name); + if (!symbol) { + return; + } + + const name = symbol.getName(); + const members = [...symbol.members.entries()] + .map((m) => ({ + name: m[0], + valueDeclaration: m[1].valueDeclaration, + })) + .filter( + (m) => m.valueDeclaration?.kind === ts.SyntaxKind.PropertySignature, + ) + .map((m) => m.name); + return { + typeName: name, + bindingTypes, + importPath, + members, + }; + } else if (ts.isTypeAliasDeclaration(node)) { // export type GridEvents = ContainerEvents & TableEvents; + const srcType = checker.getTypeAtLocation(node); + if (srcType.isIntersection()) { + const members = []; + srcType.types.forEach((t) => { + const symbol = t.getSymbol(); + if (!symbol) { + console.warn('No symbol found for type', t); + return; + } + const declaration = symbol.getDeclarations()?.[0]; + if (ts.isInterfaceDeclaration(declaration)) { // ContainerEvents & TableEvents are both interfaces + declaration.members.forEach(m => members.push(m.name?.getText())); + } + }); + + return { + typeName: node.name.getText(), + bindingTypes, + importPath, + members, + }; + } + + // const symbol = checker.getSymbolAtLocation(node.name); + // if (!symbol) { + // return; + // } + + // const name = symbol.getName(); + // const members = [...symbol.members.entries()] + // .map((m) => ({ name: m[0], valueDeclaration: m[1].valueDeclaration })) + // .filter( + // (m) => m.valueDeclaration?.kind === ts.SyntaxKind.PropertySignature, + // ) + // .map((m) => m.name); + // interfaces.push({ + // interfaceName: name, + // members, + // }); + } + }); + }); +} From cdaf6f48827f065733250ad9adce560314c3dde4 Mon Sep 17 00:00:00 2001 From: Joe Skeen Date: Thu, 1 Feb 2024 22:40:12 -0700 Subject: [PATCH 2/4] clean up script --- scripts/update-bindings.mjs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/scripts/update-bindings.mjs b/scripts/update-bindings.mjs index 8f54260..d97f330 100644 --- a/scripts/update-bindings.mjs +++ b/scripts/update-bindings.mjs @@ -1,5 +1,4 @@ import ts from 'typescript'; -import { basename } from 'path'; import { readFileSync, writeFileSync } from 'fs'; import Mustache from 'mustache'; import { camelCase, constantCase } from 'change-case'; @@ -98,23 +97,6 @@ function extractTypeInformation(sourceTypings) { members, }; } - - // const symbol = checker.getSymbolAtLocation(node.name); - // if (!symbol) { - // return; - // } - - // const name = symbol.getName(); - // const members = [...symbol.members.entries()] - // .map((m) => ({ name: m[0], valueDeclaration: m[1].valueDeclaration })) - // .filter( - // (m) => m.valueDeclaration?.kind === ts.SyntaxKind.PropertySignature, - // ) - // .map((m) => m.name); - // interfaces.push({ - // interfaceName: name, - // members, - // }); } }); }); From ec8170919c3aefd3f06e0a0d2890c4a7dcfb2767 Mon Sep 17 00:00:00 2001 From: Joe Skeen Date: Fri, 2 Feb 2024 08:25:04 -0700 Subject: [PATCH 3/4] improve fake data shown in demo app --- apps/demo/src/app/app.component.ts | 101 +++-------------------------- package.json | 1 + pnpm-lock.yaml | 8 +++ 3 files changed, 17 insertions(+), 93 deletions(-) diff --git a/apps/demo/src/app/app.component.ts b/apps/demo/src/app/app.component.ts index 979ec24..8a4f449 100644 --- a/apps/demo/src/app/app.component.ts +++ b/apps/demo/src/app/app.component.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; import { GridJsAngularComponent } from 'gridjs-angular'; +import { faker } from '@faker-js/faker'; import { TData } from 'gridjs/dist/src/types'; @Component({ @@ -27,97 +28,11 @@ export class AppComponent { onRowClick = (event: any) => console.log('Grid row clicked', event); columns = ['Name', 'Email', 'Phone Number']; - data: TData = [ - { - name: 'John Doe', - email: 'john.doe@example.com', - phone_number: '555-123-4567', - }, - { - name: 'Jane Smith', - email: 'jane.smith@example.com', - phone_number: '555-543-2109', - }, - { - name: 'Mike Johnson', - email: 'm.johnson@example.com', - phone_number: '555-987-6543', - }, - { - name: 'Sara Lee', - email: 's.lee@example.com', - phone_number: '555-345-6789', - }, - { - name: 'William Brown', - email: 'w.brown@example.com', - phone_number: '555-234-5678', - }, - { - name: 'Mary White', - email: 'mary.white@example.com', - phone_number: '555-765-4321', - }, - { - name: 'Daniel Green', - email: 'd.green@example.com', - phone_number: '555-456-7890', - }, - { - name: 'Emma Black', - email: 'emma.black@example.com', - phone_number: '555-876-5432', - }, - { - name: 'James Young', - email: 'j.young@example.com', - phone_number: '555-678-9012', - }, - { - name: 'Grace Kim', - email: 'grace.kim@example.com', - phone_number: '555-321-0987', - }, - { - name: 'Thomas Lee', - email: 'thomas.lee@example.com', - phone_number: '555-901-2345', - }, - { - name: 'Elizabeth Davis', - email: 'elizabeth.davis@example.com', - phone_number: '555-432-1098', - }, - { - name: 'Michael Harris', - email: 'm.harris@example.com', - phone_number: '555-789-0123', - }, - { - name: 'Laura Nguyen', - email: 'laura.nguyen@example.com', - phone_number: '555-234-5678', - }, - { - name: 'Kenneth Wilson', - email: 'k.wilson@example.com', - phone_number: '555-678-9012', - }, - { - name: 'Nancy Moore', - email: 'nancy.moore@example.com', - phone_number: '555-321-0987', - }, - { - name: 'Andrew Taylor', - email: 'andrew.taylor@example.com', - phone_number: '555-901-2345', - }, - { - name: 'Steven Thompson', - email: 's.thompson@example.com', - phone_number: '555-432-1098', - }, - ].map((x) => [x.name, x.email, x.phone_number]); - + data: TData = new Array(20) + .fill(undefined) + .map(() => [ + faker.person.fullName(), + faker.internet.email(), + faker.phone.number(), + ]); } diff --git a/package.json b/package.json index e242ce7..b9244c0 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@angular/cli": "~17.1.2", "@angular/compiler-cli": "~17.1.2", "@angular/language-service": "~17.1.2", + "@faker-js/faker": "^8.4.0", "@nx/devkit": "17.3.1", "@nx/eslint": "17.3.1", "@nx/eslint-plugin": "17.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89c566a..cf618b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,6 +73,9 @@ devDependencies: '@angular/language-service': specifier: ~17.1.2 version: 17.1.2 + '@faker-js/faker': + specifier: ^8.4.0 + version: 8.4.0 '@nx/devkit': specifier: 17.3.1 version: 17.3.1(nx@17.3.1) @@ -3321,6 +3324,11 @@ packages: resolution: {integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@faker-js/faker@8.4.0: + resolution: {integrity: sha512-htW87352wzUCdX1jyUQocUcmAaFqcR/w082EC8iP/gtkF0K+aKcBp0hR5Arb7dzR8tQ1TrhE9DNa5EbJELm84w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'} + dev: true + /@fastify/busboy@2.1.0: resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==} engines: {node: '>=14'} From e7a3d6e3b9941a793750fe4cbb601b588e822088 Mon Sep 17 00:00:00 2001 From: Joe Skeen Date: Sat, 3 Feb 2024 03:06:02 -0700 Subject: [PATCH 4/4] Update package.json loosen peer dependency restrictions --- packages/gridjs-angular/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/gridjs-angular/package.json b/packages/gridjs-angular/package.json index add10c2..ee495b9 100644 --- a/packages/gridjs-angular/package.json +++ b/packages/gridjs-angular/package.json @@ -14,8 +14,8 @@ "repository": "https://github.com/grid-js/gridjs-angular", "license": "MIT", "peerDependencies": { - "@angular/common": "^17.1.2", - "@angular/core": "^17.1.2", + "@angular/common": ">=17", + "@angular/core": ">=17", "gridjs": "^6.1.1" }, "dependencies": {