Skip to content

Commit

Permalink
docs(core): Update & expand docs on custom form input components
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbromley committed Nov 25, 2021
1 parent 0b09598 commit c90033b
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 25 deletions.
33 changes: 31 additions & 2 deletions docs/content/developer-guide/customizing-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,32 @@ mutation {
}
```

## CustomField UI Form Inputs

By default, the Admin UI will use an appropriate input component depending on the `type` of the custom field.
For instance, `string` fields will use a `<input type="text">` component, and `boolean` fields will use a `<input type="checkbox">` component.

These defaults can be overridden by using the `ui` property of the custom field config object. For example, if we want a number to be displayed as a currency input:

```TypeScript {hl_lines=[8]}
const config = {
// ...
customFields: {
ProductVariant: [
{
name: 'rrp',
type: 'int',
ui: { component: 'currency-form-input' },
},
]
}
}
```

The built-in form inputs are listed in the [DefaultFormConfigHash docs]({{< relref "default-form-config-hash" >}}).

If you want to use a completely custom form input component which is not provided by the Admin UI, you'll need to create a plugin which [extends the Admin UI]({{< relref "extending-the-admin-ui" >}}) with [custom form inputs]({{< relref "custom-form-inputs" >}}).

## TypeScript Typings

Because custom fields are generated at run-time, TypeScript has no way of knowing about them based on your
Expand Down Expand Up @@ -273,12 +299,15 @@ mutation {
}
}
```
#### UI for relation type

{{% alert %}}
**UI for relation type**

The Admin UI app has built-in selection components for "relation" custom fields which reference certain common entity types, such as Asset, Product, ProductVariant and Customer. If you are relating to an entity not covered by the built-in selection components, you will instead see the message:

```text
No input component configured for "<entity>" type
```

In this case, you will need to create a UI extension which defines a custom field control for that custom field. You can read more about this in the [CustomField Controls guide]({{< relref "custom-field-controls" >}})
In this case, you will need to create a UI extension which defines a custom field control for that custom field. You can read more about this in the [custom form input guide]({{< relref "custom-form-inputs" >}})
{{< /alert >}}
2 changes: 1 addition & 1 deletion docs/content/plugins/extending-the-admin-ui/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Angular uses the concept of modules ([NgModules](https://angular.io/guide/ngmodu

When creating your UI extensions, you can set your module to be either `lazy` or `shared`. Shared modules are loaded _eagerly_, i.e. their code is bundled up with the main app and loaded as soon as the app loads.

As a rule, modules defining new routes should be lazily loaded (so that the code is only loaded once that route is activated), and modules defining [new navigations items]({{< relref "adding-navigation-items" >}}) and [CustomField controls]({{< relref "custom-field-controls" >}}) should be set to `shared`.
As a rule, modules defining new routes should be lazily loaded (so that the code is only loaded once that route is activated), and modules defining [new navigations items]({{< relref "adding-navigation-items" >}}) and [custom form input]({{< relref "custom-form-inputs" >}}) should be set to `shared`.

## Dev mode

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
---
title: 'CustomField Controls'
title: 'Custom Form Inputs'
weight: 5
---

# CustomField Controls
# Custom Form Inputs

Another way to extend the Admin UI app is to define custom form control components for manipulating any [Custom Fields]({{< ref "/docs/typescript-api/custom-fields" >}}) you have defined on your entities.
Another way to extend the Admin UI app is to define custom form input components for manipulating any [Custom Fields]({{< ref "/docs/typescript-api/custom-fields" >}}) you have defined on your entities as well as [configurable args]({{< relref "config-args" >}}) used by custom [ConfigurableOperationDefs]({{< relref "configurable-operation-def" >}}).

## For Custom Fields

Let's say you define a custom "intensity" field on the Product entity:

Expand All @@ -23,51 +25,74 @@ By default, the "intensity" field will be displayed as a number input:

{{< figure src="./ui-extensions-custom-field-default.jpg" >}}

But let's say we want to display a range slider instead. Here's how we can do this using our shared extension module combined with the `registerCustomFieldComponent()` function:
But let's say we want to display a range slider instead. Here's how we can do this using our shared extension module combined with the `registerFormInputComponent()` function:

```TypeScript
import { NgModule, Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { SharedModule, CustomFieldControl,
CustomFieldConfigType, registerCustomFieldComponent } from '@vendure/admin-ui/core';
import { CustomFieldConfig } from '@vendure/common/lib/generated-types';
import { SharedModule, FormInputComponent, registerFormInputComponent } from '@vendure/admin-ui/core';

@Component({
template: `
<input
type="range"
[min]="config.intMin"
[max]="config.intMax"
[min]="config.min || 0"
[max]="config.max || 100"
[formControl]="formControl" />
{{ formControl.value }}
`,
})
export class SliderControl implements CustomFieldControl {
export class SliderControl implements FormInputComponent<CustomFieldConfig> {
readonly: boolean;
config: CustomFieldConfigType;
config: CustomFieldConfig;
formControl: FormControl;
}

@NgModule({
imports: [SharedModule],
declarations: [SliderControl],
providers: [
registerCustomFieldComponent('Product', 'intensity', SliderControl),
registerFormInputComponent('slider-form-input', SliderControl),
]
})
export class SharedExtensionModule { }
export class SharedExtensionModule {}
```

Once registered, this new slider input can be used in our custom field config:

```TypeScript {hl_lines=[7]}
// project/vendure-config.ts

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()`.

{{% alert %}}
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.
{{< /alert >}}


Re-compiling the Admin UI will result in our SliderControl now being used for the "intensity" custom field:

{{< figure src="./ui-extensions-custom-field-slider.jpg" >}}

To recap the steps involved:

1. Create an Angular Component which implements the `CustomFieldControl` interface.
1. Create an Angular Component which implements the `FormInputComponent` interface.
2. Add this component to your shared extension module's `declarations` array.
3. Use `registerCustomFieldComponent()` to register your component for the given entity & custom field name.
3. Use `registerFormInputComponent()` to register your component for the given entity & custom field name.
4. Specify this component's ID in your custom field config.

## Custom Field Controls for Relations
### 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:

Expand All @@ -77,9 +102,10 @@ If you have a custom field of the `relation` type (which allows you to relate en
Here is a simple example taken from the [real-world-vendure](https://github.com/vendure-ecommerce/real-world-vendure/blob/master/src/plugins/reviews/ui/components/featured-review-selector/featured-review-selector.component.ts) repo:

```TypeScript
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { RelationCustomFieldConfig } from '@vendure/common/lib/generated-types';
import { CustomFieldControl, DataService } from '@vendure/admin-ui/core';
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
Expand Down Expand Up @@ -107,10 +133,10 @@ import { GetReviewForProduct, ProductReviewFragment } from '../../generated-type
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RelationReviewInputComponent implements OnInit, CustomFieldControl {
@Input() readonly: boolean;
@Input() formControl: FormControl;
@Input() config: any;
export class RelationReviewInputComponent implements OnInit, FormInputComponent<RelationCustomFieldConfig> {
readonly: boolean;
formControl: FormControl;
config: RelationCustomFieldConfig;

reviews$: Observable<ProductReviewFragment[]>;

Expand All @@ -133,3 +159,53 @@ export class RelationReviewInputComponent implements OnInit, CustomFieldControl
}
}
```

### Legacy `registerCustomFieldComponent`

Prior to v1.4, the function `registerCustomFieldComponent()` was used to register a form control for a custom field. This function has now been deprecated in favour of `registerFormInputComponent()`, but is kept for backward-compatibility and will be removed in v2.0.

`registerCustomFieldComponent` is used like this:

```TypeScript
import { NgModule, Component } from '@angular/core';
import { SharedModule, registerCustomFieldComponent } from '@vendure/admin-ui/core';

// SliderControl component definition as above

@NgModule({
imports: [SharedModule],
declarations: [SliderControl],
providers: [
registerCustomFieldComponent('Product', 'intensity', SliderControl),
]
})
export class SharedExtensionModule { }
```

## For ConfigArgs

[ConfigArgs]({{< relref "config-args" >}}) are used by classes which extend [ConfigurableOperationDef]({{< relref "configurable-operation-def" >}}) (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:

```TypeScript {hl_lines=[6,7,8]}
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' }],
});
```


Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type CustomFieldEntityName = Exclude<keyof CustomFields, '__typename'>;

/**
* This service allows the registration of custom controls for customFields.
*
* @deprecated The ComponentRegistryService now handles custom field components directly.
*/
@Injectable({
providedIn: 'root',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export const defaultFormInputs = [
/**
* @description
* Registers a custom FormInputComponent which can be used to control the argument inputs
* of a {@link ConfigurableOperationDef} (e.g. CollectionFilter, ShippingMethod etc)
* of a {@link ConfigurableOperationDef} (e.g. CollectionFilter, ShippingMethod etc) or for
* a custom field.
*
* @example
* ```TypeScript
Expand Down Expand Up @@ -82,6 +83,8 @@ export function registerFormInputComponent(id: string, component: Type<FormInput
* })
* export class MyUiExtensionModule {}
* ```
*
* @deprecated use `registerFormInputComponent()` in combination with the customField `ui` config instead.
*/
export function registerCustomFieldComponent(
entity: CustomFieldEntityName,
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/shared-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export type UiComponentConfig =
component: 'product-selector-form-input';
} & DefaultFormComponentConfig<'product-selector-form-input'>)
| ({ component: 'customer-group-form-input' } & DefaultFormComponentConfig<'customer-group-form-input'>)
| { component: string; [prop: string]: any };
| { component: string; [prop: string]: Json };

export type CustomFieldsObject = { [key: string]: any };

Expand Down

0 comments on commit c90033b

Please sign in to comment.