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..8a4f449 100644
--- a/apps/demo/src/app/app.component.ts
+++ b/apps/demo/src/app/app.component.ts
@@ -1,19 +1,38 @@
import { Component } from '@angular/core';
-import { RouterModule } from '@angular/router';
import { GridJsAngularComponent } from 'gridjs-angular';
-import 'gridjs/dist/theme/mermaid.css';
+import { faker } from '@faker-js/faker';
+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 = new Array(20)
+ .fill(undefined)
+ .map(() => [
+ faker.person.fullName(),
+ faker.internet.email(),
+ faker.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..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",
@@ -49,6 +50,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 +58,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)
+[](https://github.com/grid-js/gridjs-angular)
+
+
## 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/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": {
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..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)
@@ -124,6 +127,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 +151,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)
@@ -3315,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'}
@@ -5996,6 +6010,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 +9109,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..d97f330
--- /dev/null
+++ b/scripts/update-bindings.mjs
@@ -0,0 +1,103 @@
+import ts from 'typescript';
+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,
+ };
+ }
+ }
+ });
+ });
+}