Custom Form Inputs
You can define custom Angular or React components which can be used to render Custom Fields you have defined on your entities as well as configurable args used by custom Configurable Operations.
For Custom Fields
Let's say you define a custom "intensity" field on the Product entity:
import { VendureConfig } from '@vendure/core';
export const config: VendureConfig = {
// ...
customFields: {
Product: [
{ name: 'intensity', type: 'int', min: 0, max: 100, defaultValue: 0 },
],
},
}
By default, the "intensity" field will be displayed as a number input:
But let's say we want to display a range slider instead.
1. Define a component
First we need to define a new Angular or React component to render the slider:
- Angular
- React
Angular components will have the readonly
, config
and formControl
properties populated automatically.
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { IntCustomFieldConfig, SharedModule, FormInputComponent } from '@vendure/admin-ui/core';
@Component({
template: `
<input
type="range"
[min]="config.min || 0"
[max]="config.max || 100"
[formControl]="formControl" />
{{ formControl.value }}
`,
standalone: true,
imports: [SharedModule],
})
export class SliderControlComponent implements FormInputComponent<IntCustomFieldConfig> {
readonly: boolean;
config: IntCustomFieldConfig;
formControl: FormControl;
}
React components can use the useFormControl
hook to access the form control and set its value. The
component will also receive config
and readonly
data as props.
import React from 'react';
import { useFormControl, ReactFormInputOptions, useInjector } from '@vendure/admin-ui/react';
export function SliderFormInput({ readonly, config }: ReactFormInputOptions) {
const { value, setFormValue } = useFormControl();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = +e.target.value;
setFormValue(val);
};
return (
<>
<input
type="range"
readOnly={readonly}
min={config.min || 0}
max={config.max || 100}
value={value}
onChange={handleChange}
/>
{value}
</>
);
};
2. Register the component
Next we will register this component in our providers.ts
file and give it a unique ID, 'slider-form-input'
:
- Angular
- React
import { registerFormInputComponent } from '@vendure/admin-ui/core';
import { SliderControlComponent } from './components/slider-form-input/slider-form-input.component';
export default [
registerFormInputComponent('slider-form-input', SliderControlComponent),
];
import { registerReactFormInputComponent } from '@vendure/admin-ui/react';
import { SliderControl } from './components/SliderFormInput';
export default [
registerReactFormInputComponent('slider-form-input', SliderFormInput),
];
3. Register the providers
The providers.ts
is then passed to the compileUiExtensions()
function as described in the UI Extensions Getting Started guide:
import * as path from 'path';
import { VendureConfig } from '@vendure/core';
import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
export const config: VendureConfig = {
// ...
plugins: [
AdminUiPlugin.init({
port: 3302,
app: compileUiExtensions({
outputPath: path.join(__dirname, '../admin-ui'),
extensions: [{
id: 'common',
extensionPath: path.join(__dirname, 'plugins/common/ui'),
providers: ['providers.ts'],
}],
}),
}),
],
};
4. Update the custom field config
Once registered, this new slider input can be used in our custom field config:
customFields: {
Product: [
{
name: 'intensity', type: 'int', min: 0, max: 100, defaultValue: 0,
ui: {component: 'slider-form-input'}
},
],
}
As we can see, adding the ui
property to the custom field config allows us to specify our custom slider component.
The component id 'slider-form-input' must match the string passed as the first argument to registerFormInputComponent()
.
If we want, we can also pass any other arbitrary data in the ui
object, which will then be available in our component as this.config.ui.myField
. Note that only JSON-compatible data types are permitted, so no functions or class instances.
Re-compiling the Admin UI will result in our SliderControl now being used for the "intensity" custom field:
Custom Field Controls for Relations
If you have a custom field of the relation
type (which allows you to relate entities with one another), you can also define custom field controls for them. The basic mechanism is exactly the same as with primitive custom field types (i.e. string
, int
etc.), but there are a couple of important points to know:
- The value of the
formControl
will be the related entity object rather than an id. The Admin UI will internally take care of converting the entity object into an ID when performing the create/update mutation. - Your control will most likely need to fetch data in order to display a list of selections for the user.
Here's an example of a custom field control for a relation
field which relates a Product to a custom ProductReview
entity:
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { RelationCustomFieldConfig } from '@vendure/common/lib/generated-types';
import { FormInputComponent, DataService, SharedModule } from '@vendure/admin-ui/core';
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { GET_REVIEWS_FOR_PRODUCT } from '../product-reviews-list/product-reviews-list.graphql';
@Component({
selector: 'relation-review-input',
template: `
<div *ngIf="formControl.value as review">
<vdr-chip>{{ review.rating }} / 5</vdr-chip>
{{ review.summary }}
<a [routerLink]="['/extensions', 'product-reviews', review.id]">
<clr-icon shape="link"></clr-icon>
</a>
</div>
<select [formControl]="formControl" [compareWith]="compareFn">
<option [ngValue]="null">Select a review...</option>
<option *ngFor="let item of reviews$ | async" [ngValue]="item">
<b>{{ item.summary }}</b>
{{ item.rating }} / 5
</option>
</select>
`,
standalone: true,
imports: [SharedModule],
})
export class RelationReviewInputComponent implements OnInit, FormInputComponent<RelationCustomFieldConfig> {
readonly: boolean;
formControl: FormControl;
config: RelationCustomFieldConfig;
reviews$: Observable<any[]>;
constructor(private dataService: DataService, private route: ActivatedRoute) {}
ngOnInit() {
this.reviews$ = this.route.data.pipe(
switchMap(data => data.entity),
switchMap((product: any) => {
return this.dataService
.query(GET_REVIEWS_FOR_PRODUCT, { productId: product.id })
.mapSingle(({ product }) => product?.reviews.items ?? []);
}),
);
}
compareFn(item1: { id: string } | null, item2: { id: string } | null) {
return item1 && item2 ? item1.id === item2.id : item1 === item2;
}
}
For ConfigArgs
ConfigArgs are used by classes which extend Configurable Operations (such as ShippingCalculator or PaymentMethodHandler). These ConfigArgs allow user-input values to be passed to the operation's business logic.
They are configured in a very similar way to custom fields, and likewise can use custom form inputs by specifying the ui
property.
Here's an example:
export const orderFixedDiscount = new PromotionOrderAction({
code: 'order_fixed_discount',
args: {
discount: {
type: 'int',
ui: {
component: 'currency-form-input',
},
},
},
execute(ctx, order, args) {
return -args.discount;
},
description: [{languageCode: LanguageCode.en, value: 'Discount order by fixed amount'}],
});