---
title: Set up editor templates
slug: cms-12/docs/editor-templates
docTags: 
createdAt: 2026-08-10T15:55:57.000Z
---

## Criteria editor templates

Describes how to set up custom templates for the criteria editor.

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

:::hint{type="warning"}
**Important**
The sample below uses TypeScript. To use this example, the custom implementation needs to install and use TypeScript to transpile into JavaScript that can be used.
:::

Install packages using npm in your terminal.
`npm install`

### Custom script

The `VisitorGroupCriterion` attribute has a `ScriptUrl` property where you can specify a script file used when creating the user interface for criteria. The `ScriptUrl` should refer to a JavaScript file that contains a valid `CommonJS` module which exports a React component as `default`.

Your component can rely on shared packages that do not have to be bundled with the editor. These shared packages are currently:

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

### Props

The `props` passed to your component is a `CriteriaEditorProps` that looks like this:

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

export interface CriteriaEditorProps {
  /** Id of this criteria 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 property on the criteria model. The keys are mapped to your model’s property names in `camelCase`. Attribute decorations and rules for each property are populated here. The property `additionalProps` contains your additional configuration defined in the `CriterionPropertyEditor` as JSON.

To use front-end localization, you have to call `props.initLocalization(‘path.to.xml.node’)` with the path to your XML node. Your translations are fetched and put in the state. There is a Boolean property, `translationsLoaded`, which indicates when the call is complete. You can then call the localization function to get your translations. This is described further in Localizing the audience criterion.

If your model property is an `ISelectionFactory`, the items are available in the `selectItems` property.

The `settings` property holds the saved data for your criteria. It matches the object you emit in the `onValueChange` function described below.

## Save data

For your custom component to be able to save data, you have to pass the data on using the `onValueChange` prop. Whenever you change your local state, also called`props.onValueChange`, passing in the state.

```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;
```

## Bundling

To serve your custom criteria editor as a remote component that is useable in audiences, you should use `webpack` to bundle the JavaScript files. There are other options like `vite`; currently, you have only verified `webpack` functionality. The bundling process should as mentioned earlier output a `CommonJS` module. This is specified in the example below by specifying the `libraryTarget` to `commonjs`. Webpack is also instructed to treat react, axios, and ui-framework as externals, not needing to be bundled.

`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` represents the js file to reference in your `VisitorGroupCriterionAttribute` in the `ScriptUrl` property, that decorates your C# implementation that inherits `CriterionBase`.

`tsconfig.json`

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

Minimal configuration for TypeScript.

Example C# classes needed to run this React 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);
  }
}
```
