---
title: Configure editor templates
slug: cms-13/docs/configure-editor-templates
docTags: 
createdAt: 2026-07-30T20:22:09.287Z
---

Custom editor templates provide React-based UIs for audience criteria, giving editors a richer experience than the default input elements. Use custom templates when a criterion requires specialized controls such as autocomplete, date ranges, or multi-select inputs.

## Criteria editor templates

:::hint{type="warning"}
**Important**
The following sample uses TypeScript. The custom implementation requires TypeScript to transpile into JavaScript.
:::

:::hint{type="info"}
- **Prerequisites**
  Node
- npm
:::

Install packages using npm in the terminal: `npm install`

### Custom script

The `VisitorGroupCriterion` attribute has a `ScriptUrl` property that specifies a script file used when creating the criterion UI. The `ScriptUrl` refers to a JavaScript file that contains a valid `CommonJS` module exporting a React component as `default`.

The component uses shared packages that do not need to be bundled with the editor:

- `React ^16`
- `axios ^0.22`
- `@episerver/ui-framework ^0.17`

### Props

The `props` passed to the component use the `CriteriaEditorProps` interface:

```typescript
import React, {useEffect} from 'react';
import { TextField, ExposedDropdownMenu } from "@episerver/ui-framework";

export interface CriteriaEditorProps {
  /** Id of this criterion in database */
  id: string;
  /** An id unique for this instance */
  uiGuid: string;
  /** The display name */
  name: string;
  /** Criteria description */
  description: string;
  /** The selected audience's id */
  visitorGroupId: string;
  /** The url where the remote editor component can be downloaded */
  scriptUrl: string;
  /** Props to pass along to the remote editor component */
  editorConfig: Array<CriteriaEditorModelPropertyConfig>;
  /** Stored settings for a criteria */
  settings: any;
  /** Points value */
  points: number;
  /** List of criterion errors */
  errors: CriterionError[];
  /** Callback when the value has changed */
  onValueChange: (value: string) => void;
  /** Callback when the criteria editor is deleted */
  onEditorDelete: (id: string) => void;
  /** Flag indicating if the criteria is required or not */
  required: boolean;
  /** Localization function */
  localization(key: string, fallbackText?: string, values?: any[]): string;
  /** Init function for partner localizations */
  initLocalization(path: string): any;
  /** Property indicating whether partner translations has been loaded or not */
  translationsLoaded: boolean;
}

export interface CriterionError {
  type: CriterionErrorType;
  message: string;
}
export enum CriterionErrorType {
  Error,
  Warning,
  Info,
}

interface CriteriaEditorModelPropertyConfig {
  defaultValue: any;
  required: boolean;
  range: ValidRange;
  pattern: string;
  stringLength: ValidRange;
  selectItems: any;
  additionalProps: any;
  label: string;
  typeName: string;
  order: number;
  }
  
interface ValidRange {
  min: number;
  max: number;
}
```

The `editorConfig` property on `CriteriaEditorProps` is an array of `CriteriaEditorModelPropertyConfig` containing information about each model property. The keys map to the model's property names in `camelCase`. Attribute decorations and rules for each property are populated in the config. The `additionalProps` property contains the configuration from `CriterionPropertyEditor` as JSON.

To use front-end localization, call `props.initLocalization('path.to.xml.node')` with the path to the XML node. Translations are fetched and stored in state. The `translationsLoaded` boolean indicates when the call completes. Call the localization function to retrieve translations. For details, go to [Localize the audience criterion](docId\:qtFPiyzL2JvkodXMGg30z).

When the model property uses an `ISelectionFactory`, the items are available in the `selectItems` property.

The `settings` property holds the saved data for the criterion. It matches the object emitted through the `onValueChange` function described in the following section.

## Save data

Persisting data from a custom editor requires passing updated state through the `onValueChange` callback. When local state changes, call `props.onValueChange` with the updated state object.

```typescript
const CustomCriteriaEditor = (props: CriteriaEditorProps) => {
  const [localModelState, setLocalModelState] = React.useState<any>({});

  useEffect(() => {
    const state = { ...props.settings };
    setLocalModelState(state);
  }, []);

  const emitValueChange = (newState: any) => {
    setLocalModelState(newState);
    props.onValueChange(newState);
  };

  const editorConfig = props.editorConfig as any;

  return (
    <>
      <ExposedDropdownMenu
        label="Language"
        options={
          props &&
          editorConfig.language.selectItems.map((item: any) => {
            return {
              label: item.text ?? "",
              value: item.value?.toString() ?? "",
            };
          })
        }
        value={localModelState.language?.toString()}
        onValueChange={(value) => {
          let newState = { ...localModelState };
          newState.language = value;
          emitValueChange(newState);
        }}
      />
    </>
  );
};

export default CustomCriteriaEditor;
```

## Bundle

Bundling produces a `CommonJS` module that CMS loads as a remote component in the audience editor. Use `webpack` to bundle the JavaScript files. Other tools such as `vite` exist, but only `webpack` is verified. Specify the `libraryTarget` as `commonjs` in the output configuration. Webpack treats React, Axios, and UI Framework as externals that do not need bundling.

`webpack.config.js`

```javascript
const path = require('path');

module.exports = {
	entry: './src/CustomCriteriaEditor.tsx',
	module: {
		rules: [{
			test: /\.tsx?$/,
			use: 'ts-loader',
			exclude: /node_modules/,
		}, ],
	},
	resolve: {
		extensions: ['.tsx', '.ts', '.js'],
	},
	output: {
		filename: 'CustomCriteriaEditor.js',
		libraryTarget: "commonjs",
		path: path.resolve(__dirname, 'dist'),
	},
	externals: {
		react: "react",
		reactDOM: "react-dom",
		"@episerver/ui-framework": "@episerver/ui-framework"
	},
	mode: 'development'
};
```

The `output.filename` is the JavaScript file to reference in the `ScriptUrl` property of the `VisitorGroupCriterionAttribute` that decorates the `CriterionBase` implementation.

`tsconfig.json`

```json
{
    "compilerOptions": {
      "outDir": "dist",
      "target": "es5",
      "jsx": "react",
      "esModuleInterop": true
    }
  }
```

The following C# classes demonstrate the server-side implementation for this React editor sample.

```csharp
[VisitorGroupCriterion(
  Category = "Custom Criteria",
  DisplayName = "Custom Audience",
  Description = "Sample of a custom critera",
  ScriptUrl = "CustomCriteria/main.js")]
public class CustomCriterion: CriterionBase<CustomCriterionModel> {
  public override bool IsMatch(IPrincipal principal, HttpContext httpContext) {
    return System.Threading.Thread.CurrentThread.CurrentUICulture.Name == Model.Language;
  }
}
```

```csharp
public class SimpleLanguageSelectionFactory: ISelectionFactory {
  public IEnumerable<SelectListItem> GetSelectListItems(Type propertyType) {
    return new List<SelectListItem> {
      new SelectListItem {
        Text = "English",
          Value = "en"
      },
      new SelectListItem {
        Text = "Swedish",
          Value = "sv"
      }
    };
  }
}
```

```csharp
[EPiServerDataStore(AutomaticallyCreateStore = true, AutomaticallyRemapStore = true)]
public class CustomCriterionModel: CriterionModelBase, IValidateCriterionModel {
  [CriterionPropertyEditor(SelectionFactoryType = typeof (SimpleLanguageSelectionFactory))]
  public string Language {
    get;
    set;
  }

  public override ICriterionModel Copy() {
    return ShallowCopy();
  }

  public CriterionValidationResult Validate(VisitorGroup currentGroup) {
    if (string.IsNullOrWhiteSpace(Language)) {
      return new CriterionValidationResult(false, "Please select a Language");
    }

    return new CriterionValidationResult(true);
  }
}
```
