Source

ui/decorators.ts

import "reflect-metadata";
import { UIKeys } from "./constants";
import { propMetadata } from "@decaf-ts/decorator-validation";
import {
  CrudOperationKeys,
  UIElementMetadata,
  UIListPropMetadata,
  UIPropMetadata,
} from "./types";
import { RenderingEngine } from "./Rendering";
import { OperationKeys } from "@decaf-ts/db-decorators";

/**
 * @description Decorator that hides a property during specific CRUD operations
 * @summary Controls property visibility based on operation type
 * This decorator allows you to specify which CRUD operations should hide a property
 * in the UI. The property will only be visible during operations not specified.
 *
 * @param operations - The CRUD operations during which the property should be hidden
 * @return {Function} A property decorator function
 *
 * @function hideOn
 * @category Property Decorators
 *
 * @example
 * // Hide the password field during READ operations
 * class User {
 *   @attribute()
 *   username: string;
 *
 *   @attribute()
 *   @hideOn(OperationKeys.READ)
 *   password: string;
 * }
 *
 * @mermaid
 * sequenceDiagram
 *   participant Model
 *   participant hideOn
 *   participant RenderingEngine
 *   participant UI
 *   Model->>hideOn: Apply to property
 *   hideOn->>Model: Add hidden metadata
 *   RenderingEngine->>Model: Check if property should be hidden
 *   Model->>RenderingEngine: Return hidden operations
 *   RenderingEngine->>UI: Render or hide based on current operation
 */
export function hideOn(...operations: CrudOperationKeys[]) {
  return propMetadata<CrudOperationKeys[]>(
    RenderingEngine.key(UIKeys.HIDDEN),
    operations
  );
}

/**
 * @description Decorator that completely hides a property in all UI operations
 * @summary Makes a property invisible in all CRUD operations
 * This decorator is a convenience wrapper around hideOn that hides a property
 * during all CRUD operations (CREATE, READ, UPDATE, DELETE).
 *
 * @return {Function} A property decorator function
 *
 * @function hidden
 * @category Property Decorators
 *
 * @example
 * // Completely hide the internalId field in the UI
 * class Product {
 *   @attribute()
 *   name: string;
 *
 *   @attribute()
 *   @hidden()
 *   internalId: string;
 * }
 *
 * @mermaid
 * sequenceDiagram
 *   participant Model
 *   participant hidden
 *   participant hideOn
 *   participant RenderingEngine
 *   Model->>hidden: Apply to property
 *   hidden->>hideOn: Call with all operations
 *   hideOn->>Model: Add hidden metadata
 *   RenderingEngine->>Model: Check if property should be hidden
 *   Model->>RenderingEngine: Return all operations
 *   RenderingEngine->>UI: Always hide property
 */
export function hidden() {
  return hideOn(
    OperationKeys.CREATE,
    OperationKeys.READ,
    OperationKeys.UPDATE,
    OperationKeys.DELETE
  );
}

/**
 * @description Decorator that specifies how a property should be rendered as a UI element
 * @summary Maps a model property to a specific UI element with custom properties
 * This decorator allows you to define which HTML element or component should be used
 * to render a specific property, along with any additional properties to pass to that element.
 *
 * @param {string} tag The HTML element or component tag name to use for rendering
 * @param {Record<string, any>} [props] Additional properties to pass to the element
 * @param {boolean} [serialize=false] Whether the property should be serialized
 * @return {Function} A property decorator function
 *
 * @function uielement
 * @category Property Decorators
 *
 * @example
 * // Render a property as a text input
 * class LoginForm {
 *   @attribute()
 *   @uielement('input', { type: 'text', placeholder: 'Enter username' })
 *   username: string;
 *
 *   @attribute()
 *   @uielement('input', { type: 'password', placeholder: 'Enter password' })
 *   password: string;
 *
 *   @attribute()
 *   @uielement('button', { class: 'btn-primary' })
 *   submit: string = 'Login';
 * }
 *
 * @mermaid
 * sequenceDiagram
 *   participant Model
 *   participant uielement
 *   participant RenderingEngine
 *   participant UI
 *   Model->>uielement: Apply to property
 *   uielement->>Model: Add element metadata
 *   RenderingEngine->>Model: Get element metadata
 *   Model->>RenderingEngine: Return tag and props
 *   RenderingEngine->>UI: Render with specified element
 */
export function uielement(
  tag: string,
  props?: Record<string, any>,
  serialize: boolean = false
) {
  return (original: any, propertyKey?: any) => {
    const metadata: UIElementMetadata = {
      tag: tag,
      serialize: serialize,
      props: Object.assign({}, props || {}, {
        name: propertyKey,
      }),
    };

    return propMetadata(RenderingEngine.key(UIKeys.ELEMENT), metadata)(
      original,
      propertyKey
    );
  };
}

/**
 * @description Decorator that maps a model property to a UI component property
 * @summary Specifies how a property should be passed to a UI component
 * This decorator allows you to define how a model property should be mapped to
 * a property of the UI component when rendering. It requires the class to be
 * decorated with @uimodel.
 *
 * @param {string} [propName] The name of the property to pass to the component (defaults to the property key)
 * @param {boolean} [stringify=false] Whether to stringify the property value
 * @return {Function} A property decorator function
 *
 * @function uiprop
 * @category Property Decorators
 *
 * @example
 * // Map model properties to component properties
 * @uimodel('user-profile')
 * class UserProfile {
 *   @attribute()
 *   @uiprop() // Will be passed as 'fullName' to the component
 *   fullName: string;
 *
 *   @attribute()
 *   @uiprop('userEmail') // Will be passed as 'userEmail' to the component
 *   email: string;
 *
 *   @attribute()
 *   @uiprop('userData', true) // Will be passed as stringified JSON
 *   userData: Record<string, any>;
 * }
 *
 * @mermaid
 * sequenceDiagram
 *   participant Model
 *   participant uiprop
 *   participant RenderingEngine
 *   participant Component
 *   Model->>uiprop: Apply to property
 *   uiprop->>Model: Add prop metadata
 *   RenderingEngine->>Model: Get prop metadata
 *   Model->>RenderingEngine: Return prop name and stringify flag
 *   RenderingEngine->>Component: Pass property with specified name
 */
export function uiprop(
  propName: string | undefined = undefined,
  stringify: boolean = false
) {
  return (target: any, propertyKey: string) => {
    const metadata: UIPropMetadata = {
      name: propName || propertyKey,
      stringify: stringify,
    };
    propMetadata(RenderingEngine.key(UIKeys.PROP), metadata)(
      target,
      propertyKey
    );
  };
}

/**
 * @description Decorator that maps a nested model property to a UI component property.
 * @summary Defines how a parent component should render the child model when nested.
 *
 * This decorator is used to decorate properties that are nested models.
 * When applied, it allows overriding the default tag of the child model with the provided one,
 * enabling different rendering behavior when the model acts as a child (nested)
 * compared to when it is rendered as the parent model.
 *
 * It requires the class to be decorated with `@uimodel`.
 *
 * @param {string} clazz The model class name to pass to the component (defaults to the property key).
 * @param {string} tag The HTML element or component tag name to override the UI tag of the nested model
 * @param {Record<string, any>} [props] Additional properties to pass to the element
 * @param {boolean} [serialize=false] Whether the property should be serialized
 * @return {Function} A property decorator function.
 *
 * @function uichild
 * @category Property Decorators
 *
 * @example
 * // Map a nested model to a component property with a different tag when nested
 * @uimodel('address-component')
 * class Address {
 *   @attribute()
 *   street: string;
 *
 *   @attribute()
 *   city: string;
 * }
 *
 * @uimodel('user-profile')
 * class UserProfile {
 *   @attribute()
 *   @uichild(Address.name, 'address-child-component')
 *   address: Address;
 * }
 *
 * // In this example, the Address model has the default tag 'address-component' when rendered as a root component,
 * // but when used inside UserProfile, it is rendered with the overridden tag 'address-child-component'
 *
 * @mermaid
 * sequenceDiagram
 *   participant Model
 *   participant uichild
 *   participant RenderingEngine
 *   participant Component
 *   Model->>uichild: Apply to property
 *   uichild->>Model: Add child metadata
 *   RenderingEngine->>Model: Get child metadata
 *   Model->>RenderingEngine: Return prop name, stringify flag, and child tag override
 *   RenderingEngine->>Component: Pass property with specified name and render with overridden tag if nested
 */

export function uichild(
  clazz: string,
  tag: string,
  props: Record<string, any> = {},
  serialize: boolean = false
) {
  return (target: any, propertyKey: string) => {
    const metadata: UIElementMetadata = {
      tag: tag,
      serialize: serialize,
      props: Object.assign({}, props || {}, {
        name: clazz || propertyKey,
      }),
    };

    propMetadata(RenderingEngine.key(UIKeys.CHILD), metadata)(
      target,
      propertyKey
    );
  };
}

/**
 * @description Decorator that maps a model property to a list item component
 * @summary Specifies how a property should be rendered in a list context
 * This decorator allows you to define how a model property containing a list
 * should be rendered. It requires the class to be decorated with @uilistitem.
 *
 * @param {string} [propName] The name of the property to pass to the list component (defaults to the property key)
 * @param {Record<string, any>} [props] Additional properties to pass to the list container
 * @return {Function} A property decorator function
 *
 * @function uilistprop
 * @category Property Decorators
 *
 * @example
 * // Define a list property with custom rendering
 * @uimodel('todo-list')
 * class TodoList {
 *   @attribute()
 *   title: string;
 *
 *   @attribute()
 *   @uilistprop('items', { class: 'todo-items-container' })
 *   items: TodoItem[];
 * }
 *
 * @uilistitem('li', { class: 'todo-item' })
 * class TodoItem extends Model {
 *   @attribute()
 *   text: string;
 *
 *   @attribute()
 *   completed: boolean;
 * }
 *
 * @mermaid
 * sequenceDiagram
 *   participant Model
 *   participant uilistprop
 *   participant RenderingEngine
 *   participant ListContainer
 *   participant ListItems
 *   Model->>uilistprop: Apply to property
 *   uilistprop->>Model: Add list prop metadata
 *   RenderingEngine->>Model: Get list prop metadata
 *   Model->>RenderingEngine: Return prop name and container props
 *   RenderingEngine->>ListContainer: Create container with props
 *   RenderingEngine->>ListItems: Render each item using @uilistitem
 *   ListContainer->>RenderingEngine: Return rendered list
 */
export function uilistprop(
  propName: string | undefined = undefined,
  props?: Record<string, any>
) {
  return (target: any, propertyKey: string) => {
    const metadata: Partial<UIListPropMetadata> = {
      name: propName || propertyKey,
      props: props || {},
    };
    propMetadata(RenderingEngine.key(UIKeys.UILISTPROP), metadata)(
      target,
      propertyKey
    );
  };
}