-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(admin-ui): Add initial React support for UI extensions
- Loading branch information
1 parent
8385ce8
commit 1075dd7
Showing
16 changed files
with
363 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,7 @@ const MODULES = [ | |
'order', | ||
'settings', | ||
'system', | ||
'react', | ||
]; | ||
|
||
for (const moduleDir of MODULES) { | ||
|
12 changes: 7 additions & 5 deletions
12
...ages/admin-ui/src/lib/core/src/providers/component-registry/component-registry.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,25 @@ | ||
import { Injectable, Type } from '@angular/core'; | ||
import { Injectable, InjectionToken, Type } from '@angular/core'; | ||
|
||
import { FormInputComponent, InputComponentConfig } from '../../common/component-registry-types'; | ||
|
||
export const INPUT_COMPONENT_OPTIONS = new InjectionToken<{ component?: any }>('INPUT_COMPONENT_OPTIONS'); | ||
|
||
@Injectable({ | ||
providedIn: 'root', | ||
}) | ||
export class ComponentRegistryService { | ||
private inputComponentMap = new Map<string, Type<FormInputComponent<any>>>(); | ||
private inputComponentMap = new Map<string, { type: Type<FormInputComponent<any>>; options?: any }>(); | ||
|
||
registerInputComponent(id: string, component: Type<FormInputComponent<any>>) { | ||
registerInputComponent(id: string, component: Type<FormInputComponent<any>>, options?: any) { | ||
if (this.inputComponentMap.has(id)) { | ||
throw new Error( | ||
`Cannot register an InputComponent with the id "${id}", as one with that id already exists`, | ||
); | ||
} | ||
this.inputComponentMap.set(id, component); | ||
this.inputComponentMap.set(id, { type: component, options }); | ||
} | ||
|
||
getInputComponent(id: string): Type<FormInputComponent<any>> | undefined { | ||
getInputComponent(id: string): { type: Type<FormInputComponent<any>>; options?: any } | undefined { | ||
return this.inputComponentMap.get(id); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"lib": { | ||
"styleIncludePaths": [ | ||
"../static/styles" | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { APP_INITIALIZER, Component, FactoryProvider, inject, OnInit } from '@angular/core'; | ||
import { FormControl } from '@angular/forms'; | ||
import { | ||
ComponentRegistryService, | ||
CustomField, | ||
FormInputComponent, | ||
INPUT_COMPONENT_OPTIONS, | ||
} from '@vendure/admin-ui/core'; | ||
import { ElementType } from 'react'; | ||
import { ReactComponentHostDirective } from './react-component-host.directive'; | ||
import { ReactFormInputProps } from './types'; | ||
|
||
@Component({ | ||
selector: 'vdr-react-form-input-component', | ||
template: ` <div [vdrReactComponentHost]="reactComponent" [props]="props"></div> `, | ||
standalone: true, | ||
imports: [ReactComponentHostDirective], | ||
}) | ||
class ReactFormInputComponent implements FormInputComponent, OnInit { | ||
static readonly id: string = 'react-form-input-component'; | ||
readonly: boolean; | ||
formControl: FormControl; | ||
config: CustomField & Record<string, any>; | ||
|
||
protected props: ReactFormInputProps; | ||
|
||
protected reactComponent = inject(INPUT_COMPONENT_OPTIONS).component; | ||
|
||
ngOnInit() { | ||
this.props = { | ||
formControl: this.formControl, | ||
readonly: this.readonly, | ||
config: this.config, | ||
}; | ||
} | ||
} | ||
|
||
export function registerReactFormInputComponent(id: string, component: ElementType): FactoryProvider { | ||
return { | ||
provide: APP_INITIALIZER, | ||
multi: true, | ||
useFactory: (registry: ComponentRegistryService) => () => { | ||
registry.registerInputComponent(id, ReactFormInputComponent, { component }); | ||
}, | ||
deps: [ComponentRegistryService], | ||
}; | ||
} |
44 changes: 44 additions & 0 deletions
44
packages/admin-ui/src/lib/react/src/hooks/use-form-control.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { CustomFieldType } from '@vendure/common/lib/shared-types'; | ||
import React, { useContext, useEffect, useState } from 'react'; | ||
import { HostedComponentContext } from '../react-component-host.directive'; | ||
|
||
/** | ||
* @description | ||
* Provides access to the current FormControl value and a method to update the value. | ||
*/ | ||
export function useFormControl() { | ||
const context = useContext(HostedComponentContext); | ||
if (!context) { | ||
throw new Error('No HostedComponentContext found'); | ||
} | ||
const { formControl, config } = context; | ||
const [value, setValue] = useState(formControl.value ?? 0); | ||
|
||
useEffect(() => { | ||
const subscription = formControl.valueChanges.subscribe(v => { | ||
setValue(v); | ||
}); | ||
return () => { | ||
subscription.unsubscribe(); | ||
}; | ||
}, []); | ||
|
||
function setFormValue(newValue: any) { | ||
formControl.setValue(coerceFormValue(newValue, config.type as CustomFieldType)); | ||
formControl.markAsDirty(); | ||
} | ||
|
||
return { value, setFormValue }; | ||
} | ||
|
||
function coerceFormValue(value: any, type: CustomFieldType) { | ||
switch (type) { | ||
case 'int': | ||
case 'float': | ||
return Number(value); | ||
case 'boolean': | ||
return Boolean(value); | ||
default: | ||
return value; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { useContext } from 'react'; | ||
import { HostedComponentContext } from '../react-component-host.directive'; | ||
|
||
export function useInjector(token: any) { | ||
const context = useContext(HostedComponentContext); | ||
const instance = context?.injector.get(token); | ||
if (!instance) { | ||
throw new Error(`Could not inject ${token.name ?? token.toString()}`); | ||
} | ||
return instance; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { TypedDocumentNode } from '@graphql-typed-document-node/core'; | ||
import { DataService } from '@vendure/admin-ui/core'; | ||
import { DocumentNode } from 'graphql/index'; | ||
import { useContext, useState, useCallback, useEffect } from 'react'; | ||
import { Observable } from 'rxjs'; | ||
import { HostedComponentContext } from '../react-component-host.directive'; | ||
|
||
export function useQuery<T, V extends Record<string, any> = Record<string, any>>( | ||
query: DocumentNode | TypedDocumentNode<T, V>, | ||
variables?: V, | ||
) { | ||
const { data, loading, error, refetch } = useDataService( | ||
dataService => dataService.query(query, variables).stream$, | ||
); | ||
return { data, loading, error, refetch }; | ||
} | ||
|
||
export function useMutation<T, V extends Record<string, any> = Record<string, any>>( | ||
mutation: DocumentNode | TypedDocumentNode<T, V>, | ||
) { | ||
const { data, loading, error, refetch } = useDataService(dataService => dataService.mutate(mutation)); | ||
return { data, loading, error, refetch }; | ||
} | ||
|
||
function useDataService<T, V extends Record<string, any> = Record<string, any>>( | ||
operation: (dataService: DataService) => Observable<T>, | ||
) { | ||
const context = useContext(HostedComponentContext); | ||
const dataService = context?.injector.get(DataService); | ||
if (!dataService) { | ||
throw new Error('No DataService found in HostedComponentContext'); | ||
} | ||
|
||
const [data, setData] = useState<T>(); | ||
const [error, setError] = useState<string>(); | ||
const [loading, setLoading] = useState(false); | ||
|
||
const runQuery = useCallback(() => { | ||
setLoading(true); | ||
operation(dataService).subscribe({ | ||
next: (res: any) => { | ||
setData(res.data); | ||
}, | ||
error: err => { | ||
setError(err.message); | ||
setLoading(false); | ||
}, | ||
}); | ||
}, []); | ||
|
||
useEffect(() => { | ||
runQuery(); | ||
}, [runQuery]); | ||
|
||
return { data, loading, error, refetch: runQuery }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
// This file was generated by the build-public-api.ts script | ||
export * from './adapters'; | ||
export * from './hooks/use-form-control'; | ||
export * from './hooks/use-injector'; | ||
export * from './hooks/use-query'; | ||
export * from './react-component-host.directive'; | ||
export * from './types'; |
44 changes: 44 additions & 0 deletions
44
packages/admin-ui/src/lib/react/src/react-component-host.directive.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { Directive, ElementRef, Injector, Input } from '@angular/core'; | ||
import { ComponentProps, createContext, createElement, ElementType } from 'react'; | ||
import { createRoot, Root } from 'react-dom/client'; | ||
import { HostedReactComponentContext } from './types'; | ||
|
||
export const HostedComponentContext = createContext<HostedReactComponentContext | null>(null); | ||
|
||
/** | ||
* Based on https://netbasal.com/using-react-in-angular-applications-1bb907ecac91 | ||
*/ | ||
@Directive({ | ||
selector: '[vdrReactComponentHost]', | ||
standalone: true, | ||
}) | ||
export class ReactComponentHostDirective<Comp extends ElementType> { | ||
@Input('vdrReactComponentHost') reactComponent: Comp; | ||
@Input() props: ComponentProps<Comp>; | ||
|
||
private root: Root | null = null; | ||
|
||
constructor(private host: ElementRef, private injector: Injector) {} | ||
|
||
async ngOnChanges() { | ||
const Comp = this.reactComponent; | ||
|
||
if (!this.root) { | ||
this.root = createRoot(this.host.nativeElement); | ||
} | ||
|
||
this.root.render( | ||
createElement( | ||
HostedComponentContext.Provider, | ||
{ | ||
value: { ...this.props, injector: this.injector }, | ||
}, | ||
createElement(Comp, this.props), | ||
), | ||
); | ||
} | ||
|
||
ngOnDestroy() { | ||
this.root?.unmount(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { Injector } from '@angular/core'; | ||
import { FormControl } from '@angular/forms'; | ||
import { CustomField } from '@vendure/admin-ui/core'; | ||
|
||
export interface ReactFormInputProps { | ||
formControl: FormControl; | ||
readonly: boolean; | ||
config: CustomField & Record<string, any>; | ||
} | ||
|
||
export interface HostedReactComponentContext extends ReactFormInputProps { | ||
injector: Injector; | ||
} |
Oops, something went wrong.