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 Type-Safe Service Calls.

See Also