Source

model/decorators.ts

import { UIKeys } from "../ui/constants";
import {
  UIHandlerMetadata,
  UIListModelMetadata,
  UIModelMetadata,
} from "../ui/types";
import { UIMediaBreakPoints } from "../ui/constants";
import { IPagedComponentProperties } from "../ui/interfaces";
import { apply, Metadata, metadata } from "@decaf-ts/decoration";

/**
 * @description Decorator that tags a class as a UI model
 * @summary Adds rendering capabilities to a model class by providing a render method
 * This decorator applies metadata to the class that enables it to be rendered by the UI rendering engine.
 * The model will be rendered with the specified tag and properties.
 *
 * @param {string} [tag] The HTML tag to use when rendering this model (defaults to class name)
 * @param {Record<string, any>} [props] Additional properties to pass to the rendered element
 * @return {Function} A class decorator function
 *
 * @function uimodel
 * @category Class Decorators
 *
 * @example
 * // Basic usage with default tag (class name)
 * @uimodel()
 * class UserProfile extends Model {
 *   @attribute()
 *   name: string;
 *
 *   @attribute()
 *   email: string;
 * }
 *
 * // Usage with custom tag and properties
 * @uimodel('div', { class: 'user-card' })
 * class UserCard extends Model {
 *   @attribute()
 *   username: string;
 * }
 *
 * @mermaid
 * sequenceDiagram
 *   participant System
 *   participant uimodel
 *   participant constructor
 *   participant instance
 *   System->>uimodel:do(constructor)
 *   uimodel->>constructor: Executes the constructor
 *   constructor->>uimodel: returns instance
 *   uimodel->>instance: adds the render method
 *   uimodel->>System: returns UIModel instance
 */
export function uimodel(tag?: string, props?: Record<string, any>) {
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  return (original: any, propertyKey?: any) => {
    const meta: UIModelMetadata = {
      tag: tag || original.name,
      props: props,
    };
    return metadata(
      Metadata.key(UIKeys.REFLECT, UIKeys.UIMODEL),
      meta
    )(original);
  };
}

/**
 * @description Decorator that specifies which rendering engine to use for a model
 * @summary Associates a model with a specific rendering engine implementation
 * This decorator allows you to override the default rendering engine for a specific model class,
 * enabling different rendering strategies for different models.
 *
 * @param {string} engine The name of the rendering engine to use
 * @return {Function} A class decorator function
 *
 * @function renderedBy
 * @category Class Decorators
 *
 * @example
 * // Specify a custom rendering engine for a model
 * @uimodel()
 * @renderedBy('react')
 * class ReactComponent extends Model {
 *   @attribute()
 *   title: string;
 * }
 *
 * @mermaid
 * sequenceDiagram
 *   participant System
 *   participant renderedBy
 *   participant Model
 *   participant RenderingEngine
 *   System->>renderedBy: apply to Model
 *   renderedBy->>Model: adds engine metadata
 *   Model->>RenderingEngine: uses specified engine
 *   RenderingEngine->>System: renders with custom engine
 */
export function renderedBy(engine: string) {
  return apply(
    metadata(Metadata.key(UIKeys.REFLECT, UIKeys.RENDERED_BY), engine)
  );
}

/**
 * @description Decorator that tags a model as a list item for UI rendering
 * @summary Specifies how a model should be rendered when displayed in a list context
 * This decorator applies metadata to the class that enables it to be rendered as a list item
 * by the UI rendering engine. The model will be rendered with the specified tag and properties
 * when it appears in a list.
 *
 * @param {string} [tag] The HTML tag to use when rendering this model as a list item (defaults to class name)
 * @param {Record<string, any>} [props] Additional properties to pass to the rendered list item element
 * @return {Function} A class decorator function
 *
 * @function uilistmodel
 * @category Class Decorators
 *
 * @example
 * // Basic usage with default tag (class name)
 * @uimodel()
 * @uilistmodel()
 * class TodoItem extends Model {
 *   @attribute()
 *   title: string;
 *
 *   @attribute()
 *   completed: boolean;
 * }
 *
 * // Usage with custom tag and properties
 * @uimodel()
 * @uilistmodel('li', { class: 'list-group-item' })
 * class ListItem extends Model {
 *   @attribute()
 *   text: string;
 * }
 *
 * @mermaid
 * sequenceDiagram
 *   participant System
 *   participant uilistmodel
 *   participant Model
 *   participant RenderingEngine
 *   System->>uilistmodel: apply to Model
 *   uilistmodel->>Model: adds list item metadata
 *   Model->>RenderingEngine: uses list item metadata when in list context
 *   RenderingEngine->>System: renders with list item styling
 */
export function uilistmodel(name?: string, props?: Record<string, any>) {
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  return (original: any, propertyKey?: any) => {
    const meta: UIListModelMetadata = {
      item: {
        tag: name || original.name,
        props: props,
      },
    };
    return metadata(
      Metadata.key(UIKeys.REFLECT, UIKeys.UILISTMODEL),
      meta
    )(original);
  };
}

/**
 * @description Decorator that adds event handlers to a UI model
 * @summary Specifies event handlers that should be attached to the rendered model
 * This decorator allows you to define event handlers that will be automatically
 * attached to the rendered UI element. The handlers are passed as properties
 * to the rendering engine.
 *
 * @param {Record<string, any>} [props] Object containing event handler functions and other properties
 * @return {Function} A class decorator function
 *
 * @function uihandlers
 * @category Class Decorators
 *
 * @example
 * // Add event handlers to a model
 * @uimodel('button')
 * @uihandlers({
 *   onClick: (event) => console.log('Button clicked'),
 *   onMouseOver: (event) => console.log('Mouse over button'),
 *   disabled: false
 * })
 * class ClickableButton extends Model {
 *   @attribute()
 *   label: string;
 * }
 *
 * // Add form submission handlers
 * @uimodel('form')
 * @uihandlers({
 *   onSubmit: (event) => {
 *     event.preventDefault();
 *     console.log('Form submitted');
 *   },
 *   onReset: (event) => console.log('Form reset')
 * })
 * class ContactForm extends Model {
 *   @attribute()
 *   email: string;
 * }
 *
 * @mermaid
 * sequenceDiagram
 *   participant System
 *   participant uihandlers
 *   participant Model
 *   participant RenderingEngine
 *   participant UI
 *   System->>uihandlers: apply to Model
 *   uihandlers->>Model: adds handler metadata
 *   Model->>RenderingEngine: requests rendering with handlers
 *   RenderingEngine->>UI: renders element with event handlers attached
 *   UI->>Model: triggers handlers on events
 */
export function uihandlers(props?: Record<string, any>) {
  return (original: any) => {
    const meta: UIHandlerMetadata = {
      handlers: props,
    };
    return metadata(
      Metadata.key(UIKeys.REFLECT, UIKeys.HANDLERS),
      meta
    )(original);
  };
}

/**
 * @description Decorator that creates a layout container with grid specifications
 * @summary Combines UI model functionality with layout grid configuration
 * This decorator creates a UI model that acts as a layout container with specified
 * column and row configurations. It's a convenience decorator that combines.
 * @uimodel with layout-specific properties for responsive grid layouts.
 *
 * @param {string} tag The HTML tag to use for the layout container
 * @param {number|boolean} [colsMode=1] Number of columns in the grid layout or a boolean for flex mode
 * @param {number|string[]} [rows=1] Number of rows or array of row definitions
 * @param {UIMediaBreakPoints} [breakpoint='m'] Media breakpoint for responsive behavior
 * @return {Function} A class decorator function
 *
 * @function uilayout
 * @category Class Decorators
 *
 * @example
 * // Create a simple 2-column layout
 * @uilayout('div', 2, 3)
 * class TwoColumnLayout extends Model {
 *   @attribute()
 *   @uilayoutprop(1, 1)
 *   header: string;
 *
 *   @attribute()
 *   @uilayoutprop(1, 2)
 *   leftContent: string;
 *
 *   @attribute()
 *   @uilayoutprop(2, 2)
 *   rightContent: string;
 * }
 *
 * // Create a responsive layout with custom breakpoint
 * @uilayout('section', true, 2, 'l')
 * class ResponsiveLayout extends Model {
 *   @attribute()
 *   @uilayoutprop(1, 1)
 *   title: string;
 *
 *   @attribute()
 *   @uilayoutprop(2, 1)
 *   subtitle: string;
 * }
 *
 * @mermaid
 * sequenceDiagram
 *   participant System
 *   participant uilayout
 *   participant uimodel
 *   participant Model
 *   participant RenderingEngine
 *   System->>uilayout: apply to Model
 *   uilayout->>uimodel: call with layout props
 *   uimodel->>Model: adds model metadata with layout config
 *   Model->>RenderingEngine: requests rendering as layout container
 *   RenderingEngine->>System: renders grid layout with specified dimensions
 */
export function uilayout(
  tag: string,
  colsMode: number | boolean = 1,
  rows: number | string[] = 1,
  props: any = {}
) {
  return (original: any, propertyKey?: any) => {
    return uimodel(
      tag,
      Object.assign({
        ...(typeof colsMode === "boolean"
          ? {
              flexMode: colsMode,
              cols: 1,
            }
          : {
              flexMode: false,
              cols: colsMode,
            }),
        rows,
        ...Object.assign({ breakpoint: UIMediaBreakPoints.LARGE }, props),
      })
    )(original, propertyKey);
  };
}

/**
 * @description Decorator that creates a multi-step form model with page navigation
 * @summary Combines UI model functionality with stepped/wizard form configuration
 * This decorator creates a UI model that acts as a multi-step container with page navigation
 * capabilities. It is designed for wizard-style forms or multi-page workflows where users
 * progress through sequential steps. The decorator combines @uimodel with page-specific
 * properties and optional pagination controls.
 *
 * @param {string} tag - The HTML tag to use for the stepped form container
 * @param {number | IPagedComponentProperties[]} [pages=1] - Number of pages or an array of page definitions with metadata
 * @param {boolean} [paginated=false] - Whether to show pagination controls (e.g., prev/next buttons)
 * @param {any} [props={}] - Additional properties to pass to the container element
 * @return {Function} A class decorator function
 *
 * @function uisteppedmodel
 * @category Class Decorators
 *
 * @example
 * // Create a simple 3-step wizard form
 * @uisteppedmodel('div', 3, true)
 * class RegistrationWizard extends Model {
 *   // Page 1: Personal Info
 *   @attribute()
 *   @uipageprop(1)
 *   firstName: string;
 *
 *   @attribute()
 *   @uipageprop(1)
 *   lastName: string;
 *
 *   // Page 2: Contact Info
 *   @attribute()
 *   @uipageprop(2)
 *   email: string;
 *
 *   @attribute()
 *   @uipageprop(2)
 *   phone: string;
 *
 *   // Page 3: Confirmation
 *   @attribute()
 *   @uipageprop(3)
 *   acceptTerms: boolean;
 * }
 *
 * // Create a wizard with custom page definitions
 * @uisteppedmodel('section', [
 *   { title: 'Personal', icon: 'user' },
 *   { title: 'Contact', icon: 'email' },
 *   { title: 'Review', icon: 'check' }
 * ], true, { class: 'wizard-container' })
 * class OnboardingWizard extends Model {
 *   @attribute()
 *   @uipageprop(1)
 *   username: string;
 *
 *   @attribute()
 *   @uipageprop(2)
 *   email: string;
 *
 *   @attribute()
 *   @uipageprop(3)
 *   preferences: string;
 * }
 *
 * @mermaid
 * sequenceDiagram
 *   participant System
 *   participant uisteppedmodel
 *   participant uimodel
 *   participant Model
 *   participant RenderingEngine
 *   participant PaginationController
 *   System->>uisteppedmodel: apply to Model
 *   uisteppedmodel->>uimodel: call with page config
 *   uimodel->>Model: adds model metadata with pages and pagination
 *   Model->>RenderingEngine: requests rendering as stepped form
 *   RenderingEngine->>PaginationController: initialize page navigation
 *   PaginationController->>System: renders current page with navigation controls
 */
export function uisteppedmodel(
  tag: string,
  pages: number | IPagedComponentProperties[] = 1,
  paginated: boolean = false,
  props: any = {}
) {
  let pageTitles: IPagedComponentProperties[] = [];
  if (typeof pages === "object") {
    pageTitles = pages as IPagedComponentProperties[];
    pages = pageTitles.length;
  }
  return uimodel(tag, {
    pages,
    paginated,
    pageTitles,
    props,
  });
}