Skip to content

Commit

Permalink
refactor(admin-ui): Export Link component, add docs, update public api
Browse files Browse the repository at this point in the history
michaelbromley committed Sep 4, 2023

Verified

This commit was signed with the committer’s verified signature.
m4tx Mateusz Maćkowski
1 parent 83d5756 commit 3e67837
Showing 18 changed files with 268 additions and 52 deletions.
8 changes: 6 additions & 2 deletions packages/admin-ui/scripts/build-public-api.js
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ const path = require('path');

console.log('Generating public apis...');
const SOURCES_DIR = path.join(__dirname, '/../src/lib');
const APP_SOURCE_FILE_PATTERN = /\.ts$/;
const APP_SOURCE_FILE_PATTERN = /\.tsx?$/;
const EXCLUDED_PATTERNS = [/(public_api|spec|mock)\.ts$/];

const MODULES = [
@@ -31,7 +31,11 @@ for (const moduleDir of MODULES) {
const excluded = EXCLUDED_PATTERNS.reduce((result, re) => result || re.test(filename), false);
if (!excluded) {
const relativeFilename =
'.' + filename.replace(modulePath, '').replace(/\\/g, '/').replace(/\.ts$/, '');
'.' +
filename
.replace(modulePath, '')
.replace(/\\/g, '/')
.replace(/\.tsx?$/, '');
files.push(relativeFilename);
}
});
Original file line number Diff line number Diff line change
@@ -40,8 +40,8 @@ export class PageTitleComponent implements OnInit, OnChanges {
}

ngOnChanges(changes: SimpleChanges) {
if (changes.value) {
this.titleChange$.next(changes.value.currentValue);
if (changes.title) {
this.titleChange$.next(changes.title.currentValue);
}
}
}
Original file line number Diff line number Diff line change
@@ -2,11 +2,11 @@ import { Component, inject, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { CustomField, FormInputComponent, INPUT_COMPONENT_OPTIONS } from '@vendure/admin-ui/core';
import { ReactComponentHostDirective } from '../react-component-host.directive';
import { ReactFormInputProps } from '../types';
import { ReactFormInputOptions } from '../types';

@Component({
selector: 'vdr-react-form-input-component',
template: ` <div [vdrReactComponentHost]="reactComponent" [props]="props"></div> `,
template: ` <div [vdrReactComponentHost]="reactComponent" [context]="context" [props]="context"></div> `,
standalone: true,
imports: [ReactComponentHostDirective],
})
@@ -16,12 +16,12 @@ export class ReactFormInputComponent implements FormInputComponent, OnInit {
formControl: FormControl;
config: CustomField & Record<string, any>;

protected props: ReactFormInputProps;
protected context: ReactFormInputOptions;

protected reactComponent = inject(INPUT_COMPONENT_OPTIONS).component;

ngOnInit() {
this.props = {
this.context = {
formControl: this.formControl,
readonly: this.readonly,
config: this.config,
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import { Component, inject, InjectionToken } from '@angular/core';
import { SharedModule } from '@vendure/admin-ui/core';
import { ReactComponentHostDirective } from '../react-component-host.directive';
import { ReactRouteComponentOptions } from '../types';

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

@Component({
selector: 'vdr-react-route-component',
template: `
<vdr-page-header>
<vdr-page-title *ngIf="title" [title]="title"></vdr-page-title>
<vdr-page-title *ngIf="title$ | async as title" [title]="title"></vdr-page-title>
</vdr-page-header>
<vdr-page-body><div [vdrReactComponentHost]="reactComponent" [props]="props"></div></vdr-page-body>
<vdr-page-body
><div [vdrReactComponentHost]="reactComponent" [props]="props" [context]="context"></div
></vdr-page-body>
`,
standalone: true,
imports: [ReactComponentHostDirective, SharedModule],
})
export class ReactRouteComponent {
protected title = inject(ROUTE_COMPONENT_OPTIONS).title;
protected title$ = inject(ROUTE_COMPONENT_OPTIONS).title$;
protected props = inject(ROUTE_COMPONENT_OPTIONS).props;
protected context = inject(ROUTE_COMPONENT_OPTIONS);
protected reactComponent = inject(ROUTE_COMPONENT_OPTIONS).component;
}
25 changes: 23 additions & 2 deletions packages/admin-ui/src/lib/react/src/hooks/use-form-control.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
import { CustomFieldType } from '@vendure/common/lib/shared-types';
import { useContext, useEffect, useState } from 'react';
import { HostedComponentContext } from '../react-component-host.directive';
import { HostedReactComponentContext, ReactFormInputProps } from '../types';
import { HostedReactComponentContext, ReactFormInputOptions } from '../types';

/**
* @description
* Provides access to the current FormControl value and a method to update the value.
*
* @example
* ```ts
* import { useFormControl, ReactFormInputProps } from '@vendure/admin-ui/react';
* import React from 'react';
*
* export function ReactNumberInput({ readonly }: ReactFormInputProps) {
* const { value, setFormValue } = useFormControl();
*
* const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
* setFormValue(val);
* };
* return (
* <div>
* <input readOnly={readonly} type="number" onChange={handleChange} value={value} />
* </div>
* );
* }
* ```
*
* @docsCategory react-hooks
*/
export function useFormControl() {
const context = useContext(HostedComponentContext);
@@ -37,7 +58,7 @@ export function useFormControl() {

function isFormInputContext(
context: HostedReactComponentContext,
): context is HostedReactComponentContext<ReactFormInputProps> {
): context is HostedReactComponentContext<ReactFormInputOptions> {
return context.config && context.formControl;
}

22 changes: 22 additions & 0 deletions packages/admin-ui/src/lib/react/src/hooks/use-injector.ts
Original file line number Diff line number Diff line change
@@ -2,6 +2,28 @@ import { ProviderToken } from '@angular/core';
import { useContext } from 'react';
import { HostedComponentContext } from '../react-component-host.directive';

/**
* @description
* Exposes the Angular injector which allows the injection of services into React components.
*
* @example
* ```ts
* import { useInjector } from '@vendure/admin-ui/react';
* import { NotificationService } from '@vendure/admin-ui/core';
*
* export const MyComponent = () => {
* const notificationService = useInjector(NotificationService);
*
* const handleClick = () => {
* notificationService.success('Hello world!');
* };
* // ...
* return <div>...</div>;
* }
* ```
*
* @docsCategory react-hooks
*/
export function useInjector<T = any>(token: ProviderToken<T>): T {
const context = useContext(HostedComponentContext);
const instance = context?.injector.get(token);
46 changes: 46 additions & 0 deletions packages/admin-ui/src/lib/react/src/hooks/use-page-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { BreadcrumbValue } from '@vendure/admin-ui/core';
import { useContext } from 'react';
import { HostedComponentContext } from '../react-component-host.directive';
import { HostedReactComponentContext, ReactRouteComponentOptions } from '../types';

/**
* @description
* Provides functions for setting the current page title and breadcrumb.
*
* @example
* ```ts
* import { usePageMetadata } from '@vendure/admin-ui/react';
* import { useEffect } from 'react';
*
* export const MyComponent = () => {
* const { setTitle, setBreadcrumb } = usePageMetadata();
* useEffect(() => {
* setTitle('My Page');
* setBreadcrumb([
* { link: ['./parent'], label: 'Parent Page' },
* { link: ['./'], label: 'This Page' },
* ]);
* }, []);
* // ...
* return <div>...</div>;
* }
* ```
*
* @docsCategory react-hooks
*/
export function usePageMetadata() {
const context = useContext(
HostedComponentContext,
) as HostedReactComponentContext<ReactRouteComponentOptions>;
const setBreadcrumb = (newValue: BreadcrumbValue) => {
context.breadcrumb$.next(newValue);
};
const setTitle = (newTitle: string) => {
context.title$.next(newTitle);
};

return {
setBreadcrumb,
setTitle,
};
}
83 changes: 81 additions & 2 deletions packages/admin-ui/src/lib/react/src/hooks/use-query.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,45 @@
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 { firstValueFrom, lastValueFrom, Observable } from 'rxjs';
import { useCallback, useContext, useEffect, useState } from 'react';
import { firstValueFrom, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { HostedComponentContext } from '../react-component-host.directive';

/**
* @description
* A React hook which provides access to the results of a GraphQL query.
*
* @example
* ```ts
* import { useQuery } from '@vendure/admin-ui/react';
* import { gql } from 'graphql-tag';
*
* const GET_PRODUCT = gql`
* query GetProduct($id: ID!) {
* product(id: $id) {
* id
* name
* description
* }
* }`;
*
* export const MyComponent = () => {
* const { data, loading, error } = useQuery(GET_PRODUCT, { id: '1' });
*
* if (loading) return <div>Loading...</div>;
* if (error) return <div>Error! { error }</div>;
* return (
* <div>
* <h1>{data.product.name}</h1>
* <p>{data.product.description}</p>
* </div>
* );
* };
* ```
*
* @docsCategory react-hooks
*/
export function useQuery<T, V extends Record<string, any> = Record<string, any>>(
query: DocumentNode | TypedDocumentNode<T, V>,
variables?: V,
@@ -22,6 +56,51 @@ export function useQuery<T, V extends Record<string, any> = Record<string, any>>
return { data, loading, error, refetch } as const;
}

/**
* @description
* A React hook which allows you to execute a GraphQL mutation.
*
* @example
* ```ts
* import { useMutation } from '@vendure/admin-ui/react';
* import { gql } from 'graphql-tag';
*
* const UPDATE_PRODUCT = gql`
* mutation UpdateProduct($input: UpdateProductInput!) {
* updateProduct(input: $input) {
* id
* name
* }
* }`;
*
* export const MyComponent = () => {
* const [updateProduct, { data, loading, error }] = useMutation(UPDATE_PRODUCT);
*
* const handleClick = () => {
* updateProduct({
* input: {
* id: '1',
* name: 'New name',
* },
* }).then(result => {
* // do something with the result
* });
* };
*
* if (loading) return <div>Loading...</div>;
* if (error) return <div>Error! { error }</div>;
*
* return (
* <div>
* <button onClick={handleClick}>Update product</button>
* {data && <div>Product updated!</div>}
* </div>
* );
* };
* ```
*
* @docsCategory react-hooks
*/
export function useMutation<T, V extends Record<string, any> = Record<string, any>>(
mutation: DocumentNode | TypedDocumentNode<T, V>,
) {
15 changes: 10 additions & 5 deletions packages/admin-ui/src/lib/react/src/providers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { APP_INITIALIZER, FactoryProvider } from '@angular/core';
import { Route } from '@angular/router';
import { ComponentRegistryService } from '@vendure/admin-ui/core';
import { BreadcrumbValue, ComponentRegistryService } from '@vendure/admin-ui/core';
import { ElementType } from 'react';
import { BehaviorSubject } from 'rxjs';
import { ReactFormInputComponent } from './components/react-form-input.component';
import { ReactRouteComponent, ROUTE_COMPONENT_OPTIONS } from './components/react-route.component';
import { ReactRouteComponentOptions } from './types';

export function registerReactFormInputComponent(id: string, component: ElementType): FactoryProvider {
return {
@@ -19,26 +21,29 @@ export function registerReactFormInputComponent(id: string, component: ElementTy
export function registerReactRouteComponent(options: {
component: ElementType;
title?: string;
breadcrumb?: string;
breadcrumb?: BreadcrumbValue;
path?: string;
props?: Record<string, any>;
routeConfig?: Route;
}): Route {
const breadcrumbSubject$ = new BehaviorSubject<BreadcrumbValue>(options.breadcrumb ?? '');
const titleSubject$ = new BehaviorSubject<string | undefined>(options.title);
return {
path: options.path ?? '',
providers: [
{
provide: ROUTE_COMPONENT_OPTIONS,
useValue: {
component: options.component,
title: options.title,
title$: titleSubject$,
breadcrumb$: breadcrumbSubject$,
props: options.props,
},
} satisfies ReactRouteComponentOptions,
},
...(options.routeConfig?.providers ?? []),
],
data: {
breadcrumb: options.breadcrumb,
breadcrumb: breadcrumbSubject$,
...(options.routeConfig?.data ?? {}),
},
...(options.routeConfig ?? {}),
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
@@ -3,7 +3,9 @@ export * from './components/react-form-input.component';
export * from './components/react-route.component';
export * from './hooks/use-form-control';
export * from './hooks/use-injector';
export * from './hooks/use-page-metadata';
export * from './hooks/use-query';
export * from './providers';
export * from './react-component-host.directive';
export * from './react-components/Link';
export * from './types';
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ export const HostedComponentContext = createContext<HostedReactComponentContext
export class ReactComponentHostDirective<Comp extends ElementType> {
@Input('vdrReactComponentHost') reactComponent: Comp;
@Input() props: ComponentProps<Comp>;
@Input() context: Record<string, any> = {};

private root: Root | null = null;

@@ -31,7 +32,7 @@ export class ReactComponentHostDirective<Comp extends ElementType> {
createElement(
HostedComponentContext.Provider,
{
value: { ...this.props, injector: this.injector },
value: { ...this.props, ...this.context, injector: this.injector },
},
createElement(Comp, this.props),
),
Loading

0 comments on commit 3e67837

Please sign in to comment.