Skip to content

Commit

Permalink
feat(admin-ui): Support for React-based custom detail components
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbromley committed Sep 6, 2023
1 parent c588a1f commit 55d9ffc
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Type } from '@angular/core';
import { Provider, Type } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { Observable } from 'rxjs';

Expand All @@ -25,4 +25,5 @@ export interface CustomDetailComponent {
export interface CustomDetailComponentConfig {
locationId: CustomDetailComponentLocationId;
component: Type<CustomDetailComponent>;
providers?: Provider[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export class CustomDetailComponentHostComponent implements OnInit, OnDestroy {

constructor(
private viewContainerRef: ViewContainerRef,
private componentFactoryResolver: ComponentFactoryResolver,
private customDetailComponentService: CustomDetailComponentService,
private injector: Injector,
) {}

ngOnInit(): void {
Expand All @@ -41,8 +41,12 @@ export class CustomDetailComponentHostComponent implements OnInit, OnDestroy {
);

for (const config of customComponents) {
const factory = this.componentFactoryResolver.resolveComponentFactory(config.component);
const componentRef = this.viewContainerRef.createComponent(factory);
const componentRef = this.viewContainerRef.createComponent(config.component, {
injector: Injector.create({
parent: this.injector,
providers: config.providers ?? [],
}),
});
componentRef.instance.entity$ = this.entity$;
componentRef.instance.detailForm = this.detailForm;
this.componentRefs.push(componentRef);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Component, inject, InjectionToken, OnInit, ViewEncapsulation } from '@angular/core';
import { FormGroup, UntypedFormGroup } from '@angular/forms';
import { CustomDetailComponent } from '@vendure/admin-ui/core';
import { ElementType } from 'react';
import { Observable } from 'rxjs';
import { ReactComponentHostDirective } from '../react-component-host.directive';

export const REACT_CUSTOM_DETAIL_COMPONENT_OPTIONS = new InjectionToken<{
component: ElementType;
props?: Record<string, any>;
}>('REACT_CUSTOM_DETAIL_COMPONENT_OPTIONS');

export interface ReactCustomDetailComponentContext {
detailForm: FormGroup;
entity$: Observable<any>;
}

@Component({
selector: 'vdr-react-custom-detail-component',
template: ` <div [vdrReactComponentHost]="reactComponent" [context]="context" [props]="props"></div> `,
styleUrls: ['./react-global-styles.scss'],
encapsulation: ViewEncapsulation.None,
standalone: true,
imports: [ReactComponentHostDirective],
})
export class ReactCustomDetailComponent implements CustomDetailComponent, OnInit {
detailForm: UntypedFormGroup;
entity$: Observable<any>;
protected props = inject(REACT_CUSTOM_DETAIL_COMPONENT_OPTIONS).props ?? {};
protected reactComponent = inject(REACT_CUSTOM_DETAIL_COMPONENT_OPTIONS).component;
protected context: ReactCustomDetailComponentContext;

ngOnInit() {
this.context = {
detailForm: this.detailForm,
entity$: this.entity$,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useContext, useEffect, useState } from 'react';
import { ReactCustomDetailComponentContext } from '../components/react-custom-detail.component';
import { HostedComponentContext } from '../react-component-host.directive';
import { HostedReactComponentContext } from '../types';

/**
* @description
* Provides the data available to React-based CustomDetailComponents.
*
* @example
* ```ts
* import { Card, useDetailComponentData } from '@vendure/admin-ui/react';
* import React from 'react';
*
* export function CustomDetailComponent(props: any) {
* const { entity, detailForm } = useDetailComponentData();
* const updateName = () => {
* detailForm.get('name')?.setValue('New name');
* detailForm.markAsDirty();
* };
* return (
* <Card title={'Custom Detail Component'}>
* <button className="button" onClick={updateName}>
* Update name
* </button>
* <pre>{JSON.stringify(entity, null, 2)}</pre>
* </Card>
* );
* }
* ```
*
* @docsCategory react-hooks
*/
export function useDetailComponentData() {
const context = useContext(
HostedComponentContext,
) as HostedReactComponentContext<ReactCustomDetailComponentContext>;

if (!context.detailForm || !context.entity$) {
throw new Error(`The useDetailComponentData hook can only be used within a CustomDetailComponent`);
}

const [entity, setEntity] = useState(null);

useEffect(() => {
const subscription = context.entity$.subscribe(value => {
setEntity(value);
});
return () => subscription.unsubscribe();
}, []);

return {
entity,
detailForm: context.detailForm,
};
}
70 changes: 69 additions & 1 deletion packages/admin-ui/src/lib/react/src/providers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import { APP_INITIALIZER, FactoryProvider } from '@angular/core';
import { ComponentRegistryService } from '@vendure/admin-ui/core';
import {
ComponentRegistryService,
CustomDetailComponentLocationId,
CustomDetailComponentService,
} from '@vendure/admin-ui/core';
import { ElementType } from 'react';
import {
REACT_CUSTOM_DETAIL_COMPONENT_OPTIONS,
ReactCustomDetailComponent,
} from './components/react-custom-detail.component';
import { ReactFormInputComponent } from './components/react-form-input.component';

/**
* @description
* Registers a React component to be used as a {@link FormInputComponent}.
*
* @docsCategory react-extensions
*/
export function registerReactFormInputComponent(id: string, component: ElementType): FactoryProvider {
return {
provide: APP_INITIALIZER,
Expand All @@ -13,3 +27,57 @@ export function registerReactFormInputComponent(id: string, component: ElementTy
deps: [ComponentRegistryService],
};
}

/**
* @description
* Configures a React-based component to be placed in a detail page in the given location.
*
* @docsCategory react-extensions
*/
export interface ReactCustomDetailComponentConfig {
/**
* @description
* The id of the detail page location in which to place the component.
*/
locationId: CustomDetailComponentLocationId;
/**
* @description
* The React component to render.
*/
component: ElementType;
/**
* @description
* Optional props to pass to the React component.
*/
props?: Record<string, any>;
}

/**
* @description
* Registers a React component to be rendered in a detail page in the given location.
* Components used as custom detail components can make use of the {@link useDetailComponentData} hook.
*
* @docsCategory react-extensions
*/
export function registerReactCustomDetailComponent(config: ReactCustomDetailComponentConfig) {
return {
provide: APP_INITIALIZER,
multi: true,
useFactory: (customDetailComponentService: CustomDetailComponentService) => () => {
customDetailComponentService.registerCustomDetailComponent({
component: ReactCustomDetailComponent,
locationId: config.locationId,
providers: [
{
provide: REACT_CUSTOM_DETAIL_COMPONENT_OPTIONS,
useValue: {
component: config.component,
props: config.props,
},
},
],
});
},
deps: [CustomDetailComponentService],
};
}
2 changes: 2 additions & 0 deletions packages/admin-ui/src/lib/react/src/public_api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// This file was generated by the build-public-api.ts script
export * from './components/react-custom-detail.component';
export * from './components/react-form-input.component';
export * from './components/react-route.component';
export * from './hooks/use-detail-component-data';
export * from './hooks/use-form-control';
export * from './hooks/use-injector';
export * from './hooks/use-page-metadata';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ type RegisterReactRouteComponentOptions<
props?: Record<string, any>;
};

/**
* @description
* Registers a React component to be used as a route component.
*
* @docsCategory react-extensions
*/
export function registerReactRouteComponent<
Entity extends { id: string; updatedAt?: string },
T extends DocumentNode | TypedDocumentNode<any, { id: string }>,
Expand Down

0 comments on commit 55d9ffc

Please sign in to comment.