Selaa lähdekoodia

feat(admin-ui): Allow custom React components in data table columns

Relates to #2347, relates to #2353
Michael Bromley 2 vuotta sitten
vanhempi
sitoutus
5cde775401

+ 6 - 5
packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table2.component.html

@@ -111,13 +111,14 @@
                 <td *ngFor="let column of visibleSortedColumns" [class.active]="activeIndex === i">
                     <div class="cell-content" [ngClass]="column.align">
                         <ng-container
-                            *ngIf="
-                                customComponents.get(column.id) as customComponetConfig;
-                                else defaultComponent
-                            "
+                            *ngIf="customComponents.get(column.id) as componentConfig; else defaultComponent"
                         >
                             <ng-container
-                                *ngComponentOutlet="customComponetConfig.component; inputs: { rowItem: item }"
+                                *ngComponentOutlet="
+                                    componentConfig.config.component;
+                                    inputs: { rowItem: item };
+                                    injector: componentConfig.injector
+                                "
                             ></ng-container>
                         </ng-container>
                         <ng-template #defaultComponent>

+ 10 - 7
packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table2.component.ts

@@ -7,6 +7,7 @@ import {
     ContentChildren,
     EventEmitter,
     inject,
+    Injector,
     Input,
     OnChanges,
     OnDestroy,
@@ -124,10 +125,11 @@ export class DataTable2Component<T> implements AfterContentInit, OnChanges, OnDe
     @ContentChild('vdrDt2CustomSearch') customSearchTemplate: TemplateRef<any>;
     @ContentChildren(TemplateRef) templateRefs: QueryList<TemplateRef<any>>;
 
+    injector = inject(Injector);
     route = inject(ActivatedRoute);
     filterPresetService = inject(FilterPresetService);
     dataTableCustomComponentService = inject(DataTableCustomComponentService);
-    protected customComponents = new Map<string, DataTableComponentConfig>();
+    protected customComponents = new Map<string, { config: DataTableComponentConfig; injector: Injector }>();
 
     rowTemplate: TemplateRef<any>;
     currentStart: number;
@@ -226,12 +228,13 @@ export class DataTable2Component<T> implements AfterContentInit, OnChanges, OnDe
                 column.setVisibility(column.hiddenByDefault);
             }
             column.onColumnChange(updateColumnVisibility);
-            const customComponent = this.dataTableCustomComponentService.getCustomComponentsFor(
-                this.id,
-                column.id,
-            );
-            if (customComponent) {
-                this.customComponents.set(column.id, customComponent);
+            const config = this.dataTableCustomComponentService.getCustomComponentsFor(this.id, column.id);
+            if (config) {
+                const injector = Injector.create({
+                    parent: this.injector,
+                    providers: config.providers ?? [],
+                });
+                this.customComponents.set(column.id, { config, injector });
             }
         });
 

+ 32 - 0
packages/admin-ui/src/lib/react/src/components/react-custom-column.component.ts

@@ -0,0 +1,32 @@
+import { Component, inject, InjectionToken, Input, OnInit, ViewEncapsulation } from '@angular/core';
+import { CustomColumnComponent } from '@vendure/admin-ui/core';
+import { ElementType } from 'react';
+import { ReactComponentHostDirective } from '../directives/react-component-host.directive';
+
+export const REACT_CUSTOM_COLUMN_COMPONENT_OPTIONS = new InjectionToken<{
+    component: ElementType;
+    props?: Record<string, any>;
+}>('REACT_CUSTOM_COLUMN_COMPONENT_OPTIONS');
+
+@Component({
+    selector: 'vdr-react-custom-column-component',
+    template: ` <div [vdrReactComponentHost]="reactComponent" [props]="props"></div> `,
+    styleUrls: ['./react-global-styles.scss'],
+    encapsulation: ViewEncapsulation.None,
+    standalone: true,
+    imports: [ReactComponentHostDirective],
+})
+export class ReactCustomColumnComponent implements CustomColumnComponent, OnInit {
+    @Input() rowItem: any;
+
+    protected reactComponent = inject(REACT_CUSTOM_COLUMN_COMPONENT_OPTIONS).component;
+    private options = inject(REACT_CUSTOM_COLUMN_COMPONENT_OPTIONS);
+    protected props: Record<string, any>;
+
+    ngOnInit() {
+        this.props = {
+            rowItem: this.rowItem,
+            ...(this.options.props ?? {}),
+        };
+    }
+}

+ 2 - 0
packages/admin-ui/src/lib/react/src/public_api.ts

@@ -1,4 +1,5 @@
 // This file was generated by the build-public-api.ts script
+export * from './components/react-custom-column.component';
 export * from './components/react-custom-detail.component';
 export * from './components/react-form-input.component';
 export * from './components/react-route.component';
@@ -11,6 +12,7 @@ export * from './react-hooks/use-injector';
 export * from './react-hooks/use-page-metadata';
 export * from './react-hooks/use-query';
 export * from './register-react-custom-detail-component';
+export * from './register-react-data-table-component';
 export * from './register-react-form-input-component';
 export * from './register-react-route-component';
 export * from './types';

+ 109 - 0
packages/admin-ui/src/lib/react/src/register-react-data-table-component.ts

@@ -0,0 +1,109 @@
+import { APP_INITIALIZER } from '@angular/core';
+import {
+    DataTableColumnId,
+    DataTableCustomComponentService,
+    DataTableLocationId,
+} from '@vendure/admin-ui/core';
+import { ElementType } from 'react';
+import {
+    REACT_CUSTOM_COLUMN_COMPONENT_OPTIONS,
+    ReactCustomColumnComponent,
+} from './components/react-custom-column.component';
+
+/**
+ * @description
+ * Configures a {@link CustomDetailComponent} to be placed in the given location.
+ *
+ * @docsCategory react-extensions
+ */
+export interface ReactDataTableComponentConfig {
+    /**
+     * @description
+     * The location in the UI where the custom component should be placed.
+     */
+    tableId: DataTableLocationId;
+    /**
+     * @description
+     * The column in the table where the custom component should be placed.
+     */
+    columnId: DataTableColumnId;
+    /**
+     * @description
+     * The component to render in the table cell. This component will receive the `rowItem` prop
+     * which is the data object for the row, e.g. the `Product` object if used in the `product-list` table.
+     */
+    component: ElementType;
+    /**
+     * @description
+     * Optional props to pass to the React component.
+     */
+    props?: Record<string, any>;
+}
+
+/**
+ * @description
+ * The props that will be passed to the React component registered via {@link registerReactDataTableComponent}.
+ */
+export interface ReactDataTableComponentProps {
+    rowItem: any;
+    [prop: string]: any;
+}
+
+/**
+ * @description
+ * Registers a React component to be rendered in a data table in the given location.
+ * The component will receive the `rowItem` prop which is the data object for the row,
+ * e.g. the `Product` object if used in the `product-list` table.
+ *
+ * @example
+ * ```ts title="components/SlugWithLink.tsx"
+ * import { ReactDataTableComponentProps } from '\@vendure/admin-ui/react';
+ * import React from 'react';
+ *
+ * export function SlugWithLink({ rowItem }: ReactDataTableComponentProps) {
+ *     return (
+ *         <a href={`https://example.com/products/${rowItem.slug}`} target="_blank">
+ *             {rowItem.slug}
+ *         </a>
+ *     );
+ * }
+ * ```
+ *
+ * ```ts title="providers.ts"
+ * import { registerReactDataTableComponent } from '\@vendure/admin-ui/react';
+ * import { SlugWithLink } from './components/SlugWithLink';
+ *
+ * export default [
+ *     registerReactDataTableComponent({
+ *         component: SlugWithLink,
+ *         tableId: 'product-list',
+ *         columnId: 'slug',
+ *         props: {
+ *         foo: 'bar',
+ *         },
+ *     }),
+ * ];
+ * ```
+ */
+export function registerReactDataTableComponent(config: ReactDataTableComponentConfig) {
+    return {
+        provide: APP_INITIALIZER,
+        multi: true,
+        useFactory: (dataTableCustomComponentService: DataTableCustomComponentService) => () => {
+            dataTableCustomComponentService.registerCustomComponent({
+                ...config,
+                component: ReactCustomColumnComponent,
+                providers: [
+                    {
+                        provide: REACT_CUSTOM_COLUMN_COMPONENT_OPTIONS,
+                        useValue: {
+                            component: config.component,
+                            props: config.props,
+                        },
+                    },
+                ],
+            });
+        },
+        deps: [DataTableCustomComponentService],
+    };
+}

+ 10 - 0
packages/dev-server/test-plugins/experimental-ui/components/CustomColumnComponent.tsx

@@ -0,0 +1,10 @@
+import { ReactDataTableComponentProps } from '@vendure/admin-ui/react';
+import React from 'react';
+
+export function SlugWithLink({ rowItem }: ReactDataTableComponentProps) {
+    return (
+        <a href={`https://example.com/products/${rowItem.slug}`} target="_blank">
+            {rowItem.slug}
+        </a>
+    );
+}

+ 14 - 1
packages/dev-server/test-plugins/experimental-ui/providers.ts

@@ -1,7 +1,12 @@
 import { addNavMenuSection, registerDataTableComponent } from '@vendure/admin-ui/core';
-import { registerReactFormInputComponent, registerReactCustomDetailComponent } from '@vendure/admin-ui/react';
+import {
+    registerReactFormInputComponent,
+    registerReactCustomDetailComponent,
+    registerReactDataTableComponent,
+} from '@vendure/admin-ui/react';
 
 import { CustomTableComponent } from './components/custom-table.component';
+import { CustomColumnComponent } from './components/CustomColumnComponent';
 import { CustomDetailComponent } from './components/CustomDetailComponent';
 import { ReactNumberInput } from './components/ReactNumberInput';
 
@@ -40,4 +45,12 @@ export default [
         tableId: 'product-list',
         columnId: 'slug',
     }),
+    registerReactDataTableComponent({
+        component: CustomColumnComponent,
+        tableId: 'product-list',
+        columnId: 'id',
+        props: {
+            foo: 'bar',
+        },
+    }),
 ];