Configure editor templates
Build React-based custom editor UIs for audience criteria with TypeScript and webpack.
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
ImportantThe following sample uses TypeScript. The custom implementation requires TypeScript to transpile into JavaScript.
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 ^16axios ^0.22@episerver/ui-framework ^0.17
Props
The props passed to the component use the CriteriaEditorProps interface:
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.
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.
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
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
{
"compilerOptions": {
"outDir": "dist",
"target": "es5",
"jsx": "react",
"esModuleInterop": true
}
}The following C# classes demonstrate the server-side implementation for this React editor sample.
[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;
}
}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"
}
};
}
}[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);
}
}Updated 6 days ago
