Frontend Patterns Cookbook
This page collects complete, working examples of common frontend patterns in Serene/StartSharp applications. Each example is based on real template code.
An EntityGrid
A typical generated grid extends EntityGrid and wires up its columns, dialog, row, and service:
import { EntityGrid } from "@serenity-is/corelib";
import { RoleColumns, RoleRow, RoleService } from "../../ServerTypes/Administration";
import { RoleDialog } from "./RoleDialog";
export class RoleGrid extends EntityGrid<RoleRow> {
static override[Symbol.typeInfo] = this.registerClass(nsAdministration);
protected override getColumnsKey() { return RoleColumns.columnsKey; }
protected override getDialogType() { return RoleDialog; }
protected override getRowDefinition() { return RoleRow; }
protected override getService() { return RoleService.baseUrl; }
}
See Creating and Configuring Grids and EntityGrid CRUD.
An EntityDialog with Lookups
A typical generated dialog extends EntityDialog and wires up its form, row, and service:
import { EntityDialog } from "@serenity-is/corelib";
import { LanguageForm, LanguageRow, LanguageService } from "../../ServerTypes/Administration";
export class LanguageDialog<P = {}> extends EntityDialog<LanguageRow, P> {
static override[Symbol.typeInfo] = this.registerClass(nsAdministration);
protected override getFormKey() { return LanguageForm.formKey; }
protected override getRowDefinition() { return LanguageRow; }
protected override getService() { return LanguageService.baseUrl; }
}
The form uses lookup editors for foreign keys. For example, the generated UserForm declares a Roles lookup editor:
export interface UserForm {
Roles: LookupEditor;
// ...
}
See EntityDialog and CRUD Workflows and Lookup Editors.
A Custom Widget
A custom widget extends Widget, registers with registerClass, and overrides renderContents():
import { Widget } from "@serenity-is/corelib";
export class MyCoolWidget extends Widget {
static override[Symbol.typeInfo] = this.registerClass("MyApp.MyCoolWidget");
constructor(props: { element: HTMLElement }) {
super(props);
this.element.on("click", () => {
this.element.addClass("clicked");
});
}
}
See Widget Class.
A Custom Component (JSX)
A function component is a plain function that returns JSX, using signals for state:
import { signal } from "@serenity-is/corelib";
function Counter() {
const count = signal(0);
return (
<div>
<span>Count: {count}</span>
<button onClick={() => count.value++}>Increment</button>
</div>
);
}
See Components and Hooks and JSX with DomWise.
A Custom Editor
A custom editor extends Widget, implements value interfaces, and registers with registerEditor:
import { EditorProps, IDoubleValue, IReadOnly, Widget } from "@serenity-is/corelib";
export interface MyEditorOptions {
// options
}
export class MyEditor<P extends MyEditorOptions = MyEditorOptions>
extends Widget<P> implements IReadOnly {
static override[Symbol.typeInfo] = this.registerEditor("MyApp.MyEditor", [IDoubleValue, IReadOnly]);
get_value(): number {
// read the value
return 0;
}
set_value(value: number): void {
// set the value
}
}
See Custom Editors.
A Custom Formatter
A custom formatter implements Formatter and registers with formatterTypeInfo:
import { FormatterContext, FormatterResult } from "@serenity-is/sleekgrid";
import { formatterTypeInfo, registerType } from "@serenity-is/corelib";
export class MyFormatter implements Formatter {
static [Symbol.typeInfo] = formatterTypeInfo("MyApp.MyFormatter");
static { registerType(this); }
format(ctx: FormatterContext): FormatterResult {
return ctx.value ? "Yes" : "No";
}
}
See Formatter Types.
Dependent Fields (Cascaded Editors)
To make one editor depend on another (e.g. City depends on Country), use the cascadeFrom and cascadeField options on the server side:
[LookupEditor(typeof(CityRow), CascadeFrom = "CountryID", CascadeField = "CountryId")]
public string CityID { get; set; }
When the parent (CountryID) changes, the child (CityID) reloads and filters its options to those matching the parent value.
See Lookup Editors and How To: Setup Cascaded Editors.
Calling a Service
Generated service clients provide type-safe methods:
import { LanguageService } from "./ServerTypes/Administration";
const response = await LanguageService.List({});
const row = await LanguageService.Retrieve({ EntityId: 123 });
await LanguageService.Create({ Entity: { LanguageName: "English" } });
See Also
- Frontend Troubleshooting — common issues and fixes.
- Frontend Framework Overview — the three client-side packages and how they fit together.