@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
319 lines (318 loc) • 12.8 kB
TypeScript
/**
* ========================================================================
* 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;
}