UNIVERSAL PROPERTY SYSTEM
Comprehensive guidelines for AI-assisted development within the Editor platform
🎯 Universal Property System Architecture
Problem Statement
Current Issue: Property updates are widget-specific and inconsistent.
- Font properties wrapped in complex format
- Custom widgets need special handling
- Changes don't always propagate correctly
- Difficult to debug and maintain
User Requirement:
"We must have an independent set of editors based on the property group or property - otherwise, it will take forever to ensure each widget has the correct styles/structure plus our core architecture of reusability will not work."
🏗️ Solution: Universal Property Update System
Core Principle
ONE property update mechanism that works for ALL properties across ALL widgets.
Architecture
1. Property Value Path System
Every property has a canonical path that defines where it's stored:
interface PropertyPath {
// Where the value lives
location: 'properties' | 'styles' | 'position' | 'size';
// Path within that location
path: string; // e.g., 'fontFamily', 'backgroundColor', 'base.backgroundColor'
// Whether it's responsive
responsive: boolean;
// Type for validation
type: 'string' | 'number' | 'boolean' | 'color' | 'select' | 'custom';
}
Examples:
// Font Family
{
location: 'properties',
path: 'fontFamily',
responsive: false,
type: 'string'
}
// Background Color (Custom Styles)
{
location: 'styles',
path: 'backgroundColor', // Will become 'base.backgroundColor' for responsive
responsive: true,
type: 'color'
}
// Button Text
{
location: 'properties',
path: 'text',
responsive: false,
type: 'string'
}
2. Universal Property Update Service
@Injectable({ providedIn: 'root' })
export class UniversalPropertyService {
/**
* Updates ANY property on ANY widget using a standard format
*/
updateProperty(params: PropertyUpdateParams): Widget {
const { widget, location, path, value, responsive, breakpoint } = params;
// Validate
if (!widget) throw new Error('Widget is required');
// Update based on location
switch (location) {
case 'properties':
return this.updateWidgetProperties(widget, path, value);
case 'styles':
return responsive
? this.updateResponsiveStyle(widget, path, value, breakpoint || 'base')
: this.updateNonResponsiveStyle(widget, path, value);
case 'position':
return this.updatePosition(widget, path, value, responsive, breakpoint);
case 'size':
return this.updateSize(widget, path, value, responsive, breakpoint);
}
}
/**
* Gets ANY property value from ANY widget
*/
getProperty(params: PropertyGetParams): any {
const { widget, location, path, responsive, breakpoint } = params;
switch (location) {
case 'properties':
return this.getNestedValue(widget.properties, path);
case 'styles':
return responsive
? this.getResponsiveValue(widget.styles, path, breakpoint || 'base')
: this.getNestedValue(widget.styles, path);
// ... similar for position, size
}
}
// Helper methods
private updateWidgetProperties(widget: Widget, path: string, value: any): Widget {
const newProperties = this.setNestedValue(widget.properties || {}, path, value);
return { ...widget, properties: newProperties };
}
private updateResponsiveStyle(widget: Widget, path: string, value: any, breakpoint: BreakpointKey): Widget {
const styles = widget.styles || {};
if (typeof styles === 'object' && 'base' in styles) {
// Already responsive
const breakpointStyles = styles[breakpoint] || styles.base || {};
const updatedBreakpointStyles = this.setNestedValue(breakpointStyles, path, value);
return {
...widget,
styles: {
...(styles as ResponsiveValue<WidgetStyles>),
[breakpoint]: updatedBreakpointStyles
} as ResponsiveValue<WidgetStyles>
};
} else {
// Convert to responsive
return {
...widget,
styles: {
base: this.setNestedValue(styles as WidgetStyles, path, value)
} as ResponsiveValue<WidgetStyles>
};
}
}
private setNestedValue(obj: any, path: string, value: any): any {
const keys = path.split('.');
if (keys.length === 1) {
return { ...obj, [path]: value };
}
const [first, ...rest] = keys;
return {
...obj,
[first]: this.setNestedValue(obj[first] || {}, rest.join('.'), value)
};
}
private getNestedValue(obj: any, path: string): any {
if (!obj) return undefined;
const keys = path.split('.');
return keys.reduce((current, key) => current?.[key], obj);
}
private getResponsiveValue(styles: any, path: string, breakpoint: BreakpointKey): any {
if (typeof styles === 'object' && 'base' in styles) {
const breakpointStyles = styles[breakpoint] || styles.base || {};
return this.getNestedValue(breakpointStyles, path);
}
return this.getNestedValue(styles, path);
}
}
3. Property Definition Enhancement
Enhance PropertyDefinition to include the path information:
export interface PropertyDefinition {
key: string; // Display key (e.g., 'properties.fontSize')
label: string;
type: PropertyControlType;
// ✅ NEW: Canonical location and path
location: 'properties' | 'styles' | 'position' | 'size';
path: string; // Path within location (e.g., 'fontSize', 'backgroundColor')
responsive?: boolean;
// DEPRECATED: getValue/setValue (use UniversalPropertyService instead)
getValue?: (widget: Widget, breakpoint?: BreakpointKey) => any;
setValue?: (widget: Widget, value: any, breakpoint?: BreakpointKey) => Widget;
// ... other properties
}
4. Standard Property Editor
Create ONE standard property editor that works for all property types:
@Component({
selector: 'lib-universal-property-editor',
template: `
<div class="property-editor">
<label>{{ property().label }}</label>
@switch (property().type) {
@case ('string') {
<input type="text"
[ngModel]="currentValue()"
(ngModelChange)="onValueChange($event)" />
}
@case ('number') {
<input type="number"
[ngModel]="currentValue()"
(ngModelChange)="onValueChange($event)"
[min]="property().min"
[max]="property().max"
[step]="property().step || 1" />
}
@case ('color') {
<lib-color-picker
[value]="currentValue()"
(valueChange)="onValueChange($event)" />
}
@case ('select') {
<select [ngModel]="currentValue()" (ngModelChange)="onValueChange($event)">
@for (option of property().options; track option.value) {
<option [value]="option.value">{{ option.label }}</option>
}
</select>
}
@case ('custom') {
<ng-container [ngComponentOutlet]="property().component"
[ngComponentOutletInputs]="getCustomInputs()" />
}
}
</div>
`
})
export class UniversalPropertyEditorComponent {
private readonly universalPropService = inject(UniversalPropertyService);
private readonly editorService = inject(EditorService);
readonly widget = input.required<Widget>();
readonly property = input.required<PropertyDefinition>();
readonly breakpoint = input<BreakpointKey>('base');
// ✅ Use UniversalPropertyService to get value
readonly currentValue = computed(() => {
return this.universalPropService.getProperty({
widget: this.widget(),
location: this.property().location,
path: this.property().path,
responsive: this.property().responsive,
breakpoint: this.breakpoint()
});
});
// ✅ Use UniversalPropertyService to set value
onValueChange(value: any): void {
const updatedWidget = this.universalPropService.updateProperty({
widget: this.widget(),
location: this.property().location,
path: this.property().path,
value,
responsive: this.property().responsive,
breakpoint: this.breakpoint()
});
// Save via EditorService (flash-persisted)
this.editorService.updateWidget(updatedWidget);
console.log(`✅ [UniversalPropertyEditor] Updated ${this.property().path}:`, value);
}
}
5. Property Group Definitions (Simplified)
With the universal system, property definitions become MUCH simpler:
// ✅ NEW: Simple, declarative
export const BUTTON_WIDGET_GROUP_CONTENT: PropertyGroup = {
id: 'button-content',
title: 'Content',
properties: [
{
key: 'properties.text',
label: 'Text',
type: 'string',
location: 'properties', // ✅ Where it's stored
path: 'text', // ✅ Path within location
responsive: false
},
{
key: 'properties.fontFamily',
label: 'Font Family',
type: 'select',
location: 'properties',
path: 'fontFamily',
responsive: false,
options: [
{ value: 'Arial, sans-serif', label: 'Arial' },
{ value: 'Roboto, sans-serif', label: 'Roboto' }
]
},
{
key: 'properties.fontSize',
label: 'Font Size',
type: 'number',
location: 'properties',
path: 'fontSize',
responsive: false,
min: 8,
max: 96,
step: 1
}
]
};
export const BUTTON_WIDGET_GROUP_CUSTOM_STYLES: PropertyGroup = {
id: 'button-custom-styles',
title: 'Custom Styles',
properties: [
{
key: 'styles.backgroundColor',
label: 'Background Color',
type: 'color',
location: 'styles', // ✅ Stored in styles
path: 'backgroundColor', // ✅ Simple path
responsive: true // ✅ Handles responsive automatically
}
]
};
No more custom getValue/setValue functions! The UniversalPropertyService handles everything.
Benefits
For Developers
- ✅ Consistent: Same pattern for ALL properties
- ✅ Simple: Just define location + path
- ✅ Type-safe: TypeScript validates everything
- ✅ Debuggable: One place to add logging
- ✅ Maintainable: Changes in one place affect everything
For Widgets
- ✅ Automatic: Font, colors, sizes all work the same way
- ✅ Responsive: Handled automatically if
responsive: true - ✅ Flash-persistent: Automatic via
EditorService - ✅ Reusable: Define once, works everywhere
For Users
- ✅ Reliable: Properties always work
- ✅ Predictable: Consistent behavior across widgets
- ✅ Fast: No complex wrapping/unwrapping
- ✅ Reactive: Changes appear instantly
Migration Plan
Phase 1: Create Universal System (NEXT)
- ✅ Create
UniversalPropertyService - ✅ Create
UniversalPropertyEditorComponent - ✅ Add
locationandpathtoPropertyDefinition - ✅ Test with simple properties (text, fontSize)
Phase 2: Migrate Button Widget
- Convert button property groups to use new format
- Remove custom
getValue/setValue - Test all properties work correctly
- Verify flash-persistence
Phase 3: Migrate All Widgets
- Apply pattern to remaining widgets
- Remove old property update mechanisms
- Delete deprecated code
Phase 4: Enhanced Editors
- Create
ColorPickerComponent(theme tokens + manual) - Create
SpacingEditorComponent - Create
BorderEditorComponent - Create
ShadowEditorComponent
Testing
Test Scenarios
- Simple String Property: Button text
- Simple Number Property: Font size
- Responsive Style Property: Background color
- Nested Property: Border width within styles
- Custom Component Property: Font properties group
All should:
- ✅ Update immediately
- ✅ Persist via flash-persistence
- ✅ Work across page refreshes
- ✅ Log changes clearly
Current Debug Instructions
For now, test font size changes and check console for:
🔤 [FontPropertiesGroup] updateFontProperty called: { propertyKey: 'properties.fontSize', value: 18, ... }
🔤 [FontPropertiesGroup] Emitting onChange with: { propertyKey: 'font', value: { 'properties.fontSize': 18 } }
🔤 [FontGroup] setValue called with: { data: { ... }, breakpoint: 'base', currentWidget: { ... } }
🔤 [FontGroup] Processing: { propertyKey: 'properties.fontSize', propertyValue: 18 }
✅ EditorService.updateWidget: Updated widget "Button Widget test" (...)
🔤 [ButtonWidget] Font properties: { fontFamily: ..., fontSize: 18, ... }
If ANY of these logs are missing, we know WHERE the chain breaks.
Priority: BUILD THIS SYSTEM NOW
This is the foundation for:
- ✅ Reliable property updates
- ✅ Widget reusability
- ✅ Consistent UX
- ✅ Easy maintenance
- ✅ Platform-agnostic architecture
The user is absolutely right - without this, we're just patching holes forever.
Status: 🚧 IN PROGRESS - Building universal system
Next: Implement UniversalPropertyService and test with font properties