UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

319 lines (318 loc) 12.8 kB
/** * ======================================================================== * ARCHITECTURE: FormPropertyManager * ======================================================================== * * FormPropertyManager is the core property management layer for DataForm. * It handles all get/set operations for form data across three possible * backing stores, abstracting the complexity of property access from the * UI rendering layer. * * KEY CONCEPTS: * * 1. THREE BACKING STORES: * - dataPropertyObject: IDataPropertyObject - uses getProperty()/ensureProperty() pattern * - getsetPropertyObject: IGetSetPropertyObject - uses getProperty()/setProperty() pattern * - directObject: plain JavaScript object - direct property access * * Only one is typically used at a time, but the manager checks all three. * * 2. SCALAR VS OBJECT REPRESENTATION: * Some form fields can be represented as either a scalar value OR an object. * For example, a damage field might be: * - Scalar: 5 * - Object: { amount: 5, type: "fire" } * * The manager handles "upscaling" (scalar → object) and "downscaling" * (object → scalar when only default values remain). * * 3. TYPE COERCION: * Input from form controls is always strings. The manager converts to * the appropriate type based on the field's dataType (int, float, boolean, etc.) * * ======================================================================== * UPSCALE / DOWNSCALE STRATEGY * ======================================================================== * * The "upscale on edit, downscale on persist" pattern is a core design * principle throughout this form system. It allows the UI to handle a * canonical (most complex) representation while persisting the simplest * valid format. * * THE PATTERN: * * ┌────────────────┐ upscale() ┌────────────────┐ * │ File/Stored │ ───────────────▶ │ Edit Form │ * │ (simplest) │ │ (canonical) │ * └────────────────┘ └────────────────┘ * ▲ │ * │ downscale() │ * └──────────────────────────────────────┘ * * WHY THIS MATTERS: * * Minecraft JSON often supports multiple representations of the same data. * For example, the `repair_items` field in `minecraft:repairable`: * * Simple: "minecraft:iron_ingot" * Array: ["minecraft:iron_ingot", "minecraft:gold_ingot"] * Object: { "items": ["minecraft:iron_ingot"], "repair_amount": 50 } * * Without upscaling, the editor would need to handle all three forms. * With upscaling, we always edit the object form and simplify on save. * * TWO LAYERS OF UPSCALING: * * 1. DATA LAYER (FormPropertyManager): * - upscaleDirectObject(): Converts scalars to objects * Example: 5 → { __scalar: 5 } or { amount: 5 } * - downscaleDirectObject(): Collapses back to scalar if only defaults * Example: { amount: 5 } → 5 (if amount is the only non-default) * * 2. UI LAYER (DataFormUtilities.selectFieldForValue): * - Selects the appropriate field alternate based on actual value type * - Ensures object values use object editors, arrays use array editors * - Works with field "alternates" that define multiple type representations * * FIELD ALTERNATES: * * A field definition can have multiple type representations via `alternates`. * Each alternate is a complete field definition with a different dataType. * * Example from repair_items: * Primary: { id: "repair_items", dataType: stringArray } * Alternate: { dataType: objectArray, subForm: repairItemsSubForm } * * selectFieldForValue() examines the actual data and picks the right variant. * * IMPLEMENTATION LOCATIONS: * * - FormPropertyManager.upscaleDirectObject() - Scalar → Object * - FormPropertyManager.downscaleDirectObject() - Object → Scalar * - FormPropertyManager.setPropertyValue() - Uses upscale → edit → downscale * - DataFormUtilities.selectFieldForValue() - Picks field variant for value type * - DataForm.tsx render - Uses selectFieldForValue() for rendering * * EXTENDING THE PATTERN: * * When adding new field types or component editors: * 1. Define all possible representations as field alternates * 2. Ensure selectFieldForValue() handles the new types * 3. The downscale logic automatically simplifies on persist * 4. Test with both simple and complex input data * * ======================================================================== * * USAGE: * const manager = new FormPropertyManager(definition, { * dataPropertyObject: myDataPropertyObject, * getsetPropertyObject: myGetSetPropertyObject, * directObject: myDirectObject, * }); * * // Get a property value * const damage = manager.getProperty("damage", 0); * * // Set a property value (returns updated directObject if applicable) * const updatedObj = manager.setPropertyValue("damage", 10, currentDirectObject); * * RELATED FILES: * - src/dataformux/DataForm.tsx - Main UI component that uses this manager * - src/dataform/IField.ts - Field definitions including FieldDataType * - src/dataform/IFormDefinition.ts - Form structure definitions * - src/dataform/DataFormUtilities.ts - Static utilities for form operations * * ======================================================================== */ import IFormDefinition from "./IFormDefinition"; import IField from "./IField"; import IProperty from "./IProperty"; /** * Interface for objects that provide property access via getProperty/ensureProperty pattern. * Used by block-style data objects. */ export interface IDataPropertyObject { getProperty(name: string): IProperty | undefined; ensureProperty(name: string): IProperty; onPropertyChanged: { subscribe(callback: () => void): void; unsubscribe(callback: () => void): void; }; } /** * Interface for objects that provide property access via getProperty/setProperty pattern. * Used by simpler data objects. */ export interface IGetSetPropertyObject { getProperty(name: string): any; setProperty(name: string, value: any): void; setBaseValue?(value: any): void; } /** * Configuration options for FormPropertyManager. */ export interface IFormPropertyManagerOptions { /** Property object using getProperty/ensureProperty pattern */ dataPropertyObject?: IDataPropertyObject; /** Property object using getProperty/setProperty pattern */ getsetPropertyObject?: IGetSetPropertyObject; /** Plain JavaScript object for direct property access */ directObject?: any; } /** * Result of a property update operation. */ export interface IPropertyUpdateResult { /** The updated direct object (after upscaling/downscaling) */ updatedDirectObject: any; /** The property that was changed */ property: IProperty; /** The new value */ newValue: any; } /** * FormPropertyManager handles all property get/set operations for DataForm. * * It abstracts the complexity of: * - Multiple backing stores (dataPropertyObject, getsetPropertyObject, directObject) * - Scalar to object conversion (upscaling) and back (downscaling) * - Type coercion from string inputs to typed values * - Default value handling */ export default class FormPropertyManager { private _definition; private _dataPropertyObject; private _getsetPropertyObject; constructor(definition: IFormDefinition, options?: IFormPropertyManagerOptions); /** * Gets the form definition. */ get definition(): IFormDefinition; /** * Updates the form definition. */ set definition(value: IFormDefinition); /** * Updates the backing store objects. */ updateBackingStores(options: IFormPropertyManagerOptions): void; /** * Gets a field definition by its ID. * * @param id - The field ID to look up * @returns The field definition, or undefined if not found */ getFieldById(id: string): IField | undefined; /** * Gets a property value from the backing stores. * * Checks all three backing stores in order: * 1. dataPropertyObject * 2. getsetPropertyObject * 3. directObject * * @param name - The property name * @param defaultValue - Default value if property not found * @param directObject - The current direct object (may be updated state) * @returns The property value, or defaultValue if not found */ getProperty(name: string, defaultValue?: any, directObject?: any): any; /** * Gets a property value as an integer. * * @param name - The property name * @param defaultValue - Default value if property not found or not convertible * @param directObject - The current direct object * @returns The property value as an integer */ getPropertyAsInt(name: string, defaultValue?: number, directObject?: any): number | undefined; /** * Gets a property value as a boolean. * * @param name - The property name * @param defaultValue - Default value if property not found * @param directObject - The current direct object * @returns The property value as a boolean */ getPropertyAsBoolean(name: string, defaultValue?: boolean, directObject?: any): boolean; /** * Converts a string value to the appropriate typed value based on field data type. * * @param field - The field definition * @param value - The value to convert (typically a string from form input) * @returns The typed value */ getTypedData(field: IField, value: any): any; /** * Converts a scalar value to an object representation. * * This is needed when a field can be represented as either: * - A scalar: 5 * - An object: { amount: 5, type: "fire" } * * @param directObject - The value to upscale (may be scalar or object) * @returns An object representation */ upscaleDirectObject(directObject: { [propName: string]: any; } | string | number | boolean): { [name: string]: any; }; /** * Checks if the object has any values besides scalar and default values. * * Used to determine if an object can be collapsed back to a scalar. * * @param directObject - The object to check * @returns True if the object has unique values */ directObjectHasUniqueValuesBesidesScalar(directObject: { [propName: string]: any; }): boolean; /** * Converts an object representation back to a scalar if possible. * * This collapses objects that only contain: * - Empty values * - Default values * - The scalar field value * * @param directObject - The object to potentially downscale * @returns Either the original object or a scalar value */ downscaleDirectObject(directObject: { [propName: string]: any; }): any; /** * Sets a property value across all applicable backing stores. * * @param id - The property ID to set * @param val - The new value * @param currentDirectObject - The current direct object state * @returns The update result with the new direct object state */ setPropertyValue(id: string, val: any, currentDirectObject?: any): IPropertyUpdateResult; /** * Processes an input update from a form control. * * This is the main entry point for handling user input. It: * 1. Finds the field definition * 2. Converts the string input to the appropriate type * 3. Updates all backing stores * * @param id - The field ID being updated * @param data - The string value from the form control * @param currentDirectObject - The current direct object state * @returns The update result, or undefined if the update failed */ processInputUpdate(id: string, data: string, currentDirectObject?: any): IPropertyUpdateResult | undefined; /** * Toggles a boolean property value. * * @param id - The property ID to toggle * @param defaultValue - The default value to use if property is not set * @param currentDirectObject - The current direct object state * @returns The update result */ toggleBooleanProperty(id: string, defaultValue: boolean, currentDirectObject?: any): IPropertyUpdateResult; }