diff --git a/readme.hints.md b/readme.hints.md index f7b50e8..53d3f94 100644 --- a/readme.hints.md +++ b/readme.hints.md @@ -12,10 +12,16 @@ The properties panel had timing issues detecting rendered elements because: 1. Added a 100ms initial delay to allow render completion 2. Implemented recursive element search that: - Searches through nested children up to 5 levels deep - - Checks shadow roots of elements - - Handles complex DOM structures + - Checks both light DOM and shadow DOM for all elements + - Handles complex DOM structures generically + - Works with any wrapper elements, not specific to dees-demowrapper 3. Added retry mechanism with up to 5 attempts (200ms between retries) 4. Improved error messages to show retry count +5. Comprehensive error handling: + - Errors in element search don't break the update cycle + - Individual property errors don't prevent other properties from rendering + - scheduleUpdate always completes even if createProperties fails + - Clears warnings and property content appropriately on errors ### Code Flow 1. Dashboard renders element demo into viewport using `render(anonItem.demo(), viewport)` diff --git a/readme.plan.md b/readme.plan.md index feb47d3..a466ac9 100644 --- a/readme.plan.md +++ b/readme.plan.md @@ -61,4 +61,28 @@ The properties panel has timing issues detecting rendered elements because: - Access children via wrapper.children property - Updated documentation with correct import path (lowercase 'demotools') - Examples show how to use querySelector for powerful element selection -- Added clarifying comment about querySelector working on slotted content \ No newline at end of file +- Added clarifying comment about querySelector working on slotted content + +## Fixed Properties Panel Compatibility: +- Made element search generic - works with any container elements +- Searches both light DOM and shadow DOM recursively +- Improved error handling to prevent breaking the update cycle +- Errors in one property don't prevent others from rendering +- Detection continues working even after errors occur +- Maintains compatibility with all element structures + +# Test Elements Created (COMPLETED) + +## Created comprehensive test elements: +1. **test-noprops** - Element with no @property decorators +2. **test-complextypes** - Element with arrays, objects, dates, and complex nested data +3. **test-withwrapper** - Element that uses dees-demowrapper in its demo +4. **test-edgecases** - Element with edge cases (null, undefined, NaN, Infinity, circular refs) +5. **test-nested** - Element with deeply nested structure to test recursive search + +These test various scenarios: +- Properties panel handling of elements without properties +- Complex data type display and editing +- Element detection inside dees-demowrapper +- Error handling for problematic values +- Deep nesting and shadow DOM traversal \ No newline at end of file diff --git a/test/elements/index.ts b/test/elements/index.ts index 62bfddb..61a66eb 100644 --- a/test/elements/index.ts +++ b/test/elements/index.ts @@ -1 +1,6 @@ export * from './test-demoelement.js'; +export * from './test-noprops.js'; +export * from './test-complextypes.js'; +export * from './test-withwrapper.js'; +export * from './test-edgecases.js'; +export * from './test-nested.js'; diff --git a/test/elements/test-complextypes.ts b/test/elements/test-complextypes.ts new file mode 100644 index 0000000..80b28eb --- /dev/null +++ b/test/elements/test-complextypes.ts @@ -0,0 +1,137 @@ +import { + DeesElement, + customElement, + type TemplateResult, + html, + property, + css, +} from '@design.estate/dees-element'; + +interface IComplexData { + name: string; + age: number; + tags: string[]; + metadata: { + created: Date; + modified: Date; + author: string; + }; +} + +@customElement('test-complextypes') +export class TestComplexTypes extends DeesElement { + public static demo = () => html` + + `; + + @property({ type: Array }) + public stringArray: string[] = ['apple', 'banana', 'cherry']; + + @property({ type: Array }) + public numberArray: number[] = [1, 2, 3, 4, 5]; + + @property({ attribute: false }) + public complexData: IComplexData = { + name: 'Default Name', + age: 0, + tags: [], + metadata: { + created: new Date(), + modified: new Date(), + author: 'Unknown' + } + }; + + @property({ type: Object }) + public simpleObject = { + key1: 'value1', + key2: 'value2', + key3: 123 + }; + + @property({ attribute: false }) + public functionProperty = () => { + console.log('This is a function property'); + }; + + @property({ type: Date }) + public dateProperty = new Date(); + + public static styles = [ + css` + :host { + display: block; + padding: 20px; + background: #f5f5f5; + border: 2px solid #ddd; + border-radius: 8px; + font-family: monospace; + } + .section { + margin: 10px 0; + padding: 10px; + background: white; + border-radius: 4px; + } + .label { + font-weight: bold; + color: #333; + } + .value { + color: #666; + margin-left: 10px; + } + pre { + background: #f0f0f0; + padding: 8px; + border-radius: 4px; + overflow-x: auto; + } + ` + ]; + + public render() { + return html` +
+ String Array: + ${this.stringArray.join(', ')} +
+ +
+ Number Array: + ${this.numberArray.join(', ')} +
+ +
+ Complex Data: +
${JSON.stringify(this.complexData, null, 2)}
+
+ +
+ Simple Object: +
${JSON.stringify(this.simpleObject, null, 2)}
+
+ +
+ Date Property: + ${this.dateProperty.toLocaleString()} +
+ +
+ Function Property: + ${typeof this.functionProperty} +
+ `; + } +} \ No newline at end of file diff --git a/test/elements/test-edgecases.ts b/test/elements/test-edgecases.ts new file mode 100644 index 0000000..ea031c7 --- /dev/null +++ b/test/elements/test-edgecases.ts @@ -0,0 +1,195 @@ +import { + DeesElement, + customElement, + type TemplateResult, + html, + property, + css, +} from '@design.estate/dees-element'; + +@customElement('test-edgecases') +export class TestEdgeCases extends DeesElement { + public static demo = () => html``; + + // Property with null value + @property({ type: String }) + public nullableString: string | null = null; + + // Property with undefined value + @property({ type: Number }) + public undefinedNumber: number | undefined = undefined; + + // Very long string + @property({ type: String }) + public longString: string = 'Lorem ipsum '.repeat(50); + + // Property with special characters + @property({ type: String }) + public specialChars: string = '!@#$%^&*()_+-=[]{}|;\':",./<>?`~'; + + // Property that could cause rendering issues + @property({ type: String }) + public htmlString: string = 'Bold text'; + + // Numeric edge cases + @property({ type: Number }) + public infinityNumber: number = Infinity; + + @property({ type: Number }) + public nanNumber: number = NaN; + + @property({ type: Number }) + public veryLargeNumber: number = Number.MAX_SAFE_INTEGER; + + @property({ type: Number }) + public verySmallNumber: number = Number.MIN_SAFE_INTEGER; + + @property({ type: Number }) + public floatNumber: number = 3.14159265359; + + // Boolean-like values + @property({ type: String }) + public booleanString: string = 'false'; + + @property({ type: Number }) + public booleanNumber: number = 0; + + // Empty values + @property({ type: String }) + public emptyString: string = ''; + + @property({ type: Array }) + public emptyArray: any[] = []; + + @property({ type: Object }) + public emptyObject: {} = {}; + + // Circular reference (should not break properties panel) + @property({ attribute: false }) + public circularRef: any = (() => { + const obj: any = { name: 'circular' }; + obj.self = obj; + return obj; + })(); + + public static styles = [ + css` + :host { + display: block; + padding: 20px; + background: #fff3e0; + border: 2px solid #ff9800; + border-radius: 8px; + font-family: monospace; + font-size: 12px; + } + .warning { + background: #ffe0b2; + padding: 10px; + border-radius: 4px; + margin-bottom: 10px; + color: #e65100; + } + .property { + margin: 5px 0; + padding: 5px; + background: white; + border-radius: 2px; + word-break: break-all; + } + .label { + font-weight: bold; + color: #f57c00; + } + .value { + color: #666; + } + .special { + background: #ffccbc; + padding: 2px 4px; + border-radius: 2px; + } + ` + ]; + + private formatValue(value: any): string { + if (value === null) return 'null'; + if (value === undefined) return 'undefined'; + if (value === Infinity) return 'Infinity'; + if (Number.isNaN(value)) return 'NaN'; + if (typeof value === 'string' && value.length > 50) { + return value.substring(0, 50) + '...'; + } + if (typeof value === 'object') { + try { + return JSON.stringify(value); + } catch (e) { + return '[Circular Reference]'; + } + } + return String(value); + } + + public render() { + return html` +
+ ⚠️ This element tests edge cases and problematic values +
+ +
+ Nullable String: + ${this.formatValue(this.nullableString)} +
+ +
+ Undefined Number: + ${this.formatValue(this.undefinedNumber)} +
+ +
+ Long String: + ${this.formatValue(this.longString)} +
+ +
+ Special Characters: + ${this.specialChars} +
+ +
+ HTML String (escaped): + ${this.htmlString} +
+ +
+ Infinity: + ${this.formatValue(this.infinityNumber)} +
+ +
+ NaN: + ${this.formatValue(this.nanNumber)} +
+ +
+ Very Large Number: + ${this.veryLargeNumber} +
+ +
+ Float Number: + ${this.floatNumber} +
+ +
+ Empty String: + [empty] +
+ +
+ Circular Reference: + ${this.formatValue(this.circularRef)} +
+ `; + } +} \ No newline at end of file diff --git a/test/elements/test-nested.ts b/test/elements/test-nested.ts new file mode 100644 index 0000000..ca8c61c --- /dev/null +++ b/test/elements/test-nested.ts @@ -0,0 +1,127 @@ +import { + DeesElement, + customElement, + type TemplateResult, + html, + property, + css, +} from '@design.estate/dees-element'; + +// Helper component for nesting +@customElement('test-nested-wrapper') +class TestNestedWrapper extends DeesElement { + public render() { + return html` +
+ +
+ `; + } +} + +// The actual test element deeply nested +@customElement('test-nested-target') +class TestNestedTarget extends DeesElement { + @property({ type: String }) + public message: string = 'I am deeply nested!'; + + @property({ type: Number }) + public depth: number = 0; + + @property({ type: Boolean }) + public found: boolean = false; + + public static styles = [ + css` + :host { + display: block; + padding: 15px; + background: #e1f5fe; + border: 2px solid #0288d1; + border-radius: 4px; + margin: 5px; + } + .info { + font-family: monospace; + color: #01579b; + } + ` + ]; + + public render() { + return html` +
+ Nested Target Element
+ Message: ${this.message}
+ Depth: ${this.depth}
+ Found by properties panel: ${this.found ? '✅' : '❌'} +
+ `; + } +} + +@customElement('test-nested') +export class TestNested extends DeesElement { + public static demo = () => html` + + `; + + @property({ type: String }) + public testId: string = 'nested-test'; + + public static styles = [ + css` + :host { + display: block; + padding: 20px; + background: #f5f5f5; + border: 2px solid #999; + border-radius: 8px; + } + .explanation { + background: #fff; + padding: 10px; + border-radius: 4px; + margin-bottom: 10px; + } + .structure { + background: #f0f0f0; + padding: 10px; + border-radius: 4px; + } + ` + ]; + + public render() { + return html` +
+

Nested Structure Test

+

The actual element with properties is nested deep inside multiple layers:

+
+ +
+ +
+ +
+ +
+ + +
+
+
+
+
+
+
+ +
+ Properties panel should find the test-nested-target element despite the deep nesting. +
+ `; + } +} \ No newline at end of file diff --git a/test/elements/test-noprops.ts b/test/elements/test-noprops.ts new file mode 100644 index 0000000..344f595 --- /dev/null +++ b/test/elements/test-noprops.ts @@ -0,0 +1,37 @@ +import { + DeesElement, + customElement, + type TemplateResult, + html, + css, +} from '@design.estate/dees-element'; + +@customElement('test-noprops') +export class TestNoProps extends DeesElement { + public static demo = () => html``; + + public static styles = [ + css` + :host { + display: block; + padding: 20px; + background: #f0f0f0; + border: 2px solid #ccc; + border-radius: 8px; + } + .message { + font-family: monospace; + color: #666; + } + ` + ]; + + public render() { + return html` +
+ This element has no @property decorators. + Properties panel should handle this gracefully. +
+ `; + } +} \ No newline at end of file diff --git a/test/elements/test-withwrapper.ts b/test/elements/test-withwrapper.ts new file mode 100644 index 0000000..7e3db79 --- /dev/null +++ b/test/elements/test-withwrapper.ts @@ -0,0 +1,111 @@ +import { + DeesElement, + customElement, + type TemplateResult, + html, + property, + css, +} from '@design.estate/dees-element'; + +// Import from local demotools +import '../../ts_demotools/demotools.js'; + +@customElement('test-withwrapper') +export class TestWithWrapper extends DeesElement { + public static demo = () => html` + { + console.log('DemoWrapper: Found wrapper element', wrapper); + + const testElement = wrapper.querySelector('test-withwrapper'); + if (testElement) { + console.log('DemoWrapper: Found test-withwrapper element'); + testElement.dynamicValue = 'Set by demo wrapper!'; + testElement.counter = 100; + + // Test querySelector functionality + const innerDiv = wrapper.querySelector('.inner-content'); + console.log('DemoWrapper: Found inner div:', innerDiv); + + // Test querySelectorAll + const allButtons = wrapper.querySelectorAll('button'); + console.log(`DemoWrapper: Found ${allButtons.length} buttons`); + } + }}> + +
+ This div is also inside the wrapper +
+
+ `; + + @property({ type: String }) + public dynamicValue: string = 'Initial value'; + + @property({ type: Number }) + public counter: number = 0; + + @property({ type: Boolean }) + public isActive: boolean = false; + + public static styles = [ + css` + :host { + display: block; + padding: 20px; + background: #e8f5e9; + border: 2px solid #4caf50; + border-radius: 8px; + } + .wrapper-info { + background: #c8e6c9; + padding: 10px; + border-radius: 4px; + margin-bottom: 10px; + } + .inner-content { + background: white; + padding: 15px; + border-radius: 4px; + margin: 10px 0; + } + button { + background: #4caf50; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + margin-right: 10px; + } + button:hover { + background: #45a049; + } + .status { + margin-top: 10px; + font-family: monospace; + } + ` + ]; + + public render() { + return html` +
+ This element is wrapped with dees-demowrapper in its demo +
+ +
+

Dynamic Value: ${this.dynamicValue}

+

Counter: ${this.counter}

+

Active: ${this.isActive ? 'Yes' : 'No'}

+ + + + +
+ +
+ Properties panel should detect this element inside the wrapper +
+ `; + } +} \ No newline at end of file diff --git a/ts_web/elements/wcc-properties.ts b/ts_web/elements/wcc-properties.ts index 81c6c37..7480b26 100644 --- a/ts_web/elements/wcc-properties.ts +++ b/ts_web/elements/wcc-properties.ts @@ -229,23 +229,28 @@ export class WccProperties extends DeesElement { private async findElementRecursively(container: Element, elementClass: any, maxDepth: number = 5): Promise { if (maxDepth <= 0) return null; - // Check direct children - for (const child of Array.from(container.children)) { - if (child instanceof elementClass) { - return child as HTMLElement; - } - } - - // Check shadow roots of children - for (const child of Array.from(container.children)) { - if (child.shadowRoot) { - const found = await this.findElementRecursively(child.shadowRoot as any, elementClass, maxDepth - 1); - if (found) return found; + try { + // Check direct children + for (const child of Array.from(container.children)) { + if (child instanceof elementClass) { + return child as HTMLElement; + } } - // Also check nested children - const found = await this.findElementRecursively(child, elementClass, maxDepth - 1); - if (found) return found; + // Search in all children recursively + for (const child of Array.from(container.children)) { + // First, always check the light DOM children + const found = await this.findElementRecursively(child, elementClass, maxDepth - 1); + if (found) return found; + + // Also check shadow root if it exists + if (child.shadowRoot) { + const shadowFound = await this.findElementRecursively(child.shadowRoot as any, elementClass, maxDepth - 1); + if (shadowFound) return shadowFound; + } + } + } catch (error) { + console.error('Error in findElementRecursively:', error); } return null; @@ -254,6 +259,9 @@ export class WccProperties extends DeesElement { public async createProperties() { console.log('creating properties for:'); console.log(this.selectedItem); + + // Clear any previous warnings + this.warning = null; const isEnumeration = (propertyArg): boolean => { const keys = Object.keys(propertyArg.type); const values = []; @@ -315,15 +323,20 @@ export class WccProperties extends DeesElement { let retries = 0; while (!firstFoundInstantiatedElement && retries < 5) { await new Promise(resolve => setTimeout(resolve, 200)); - firstFoundInstantiatedElement = await this.findElementRecursively( - viewport, - this.selectedItem as any - ); + try { + firstFoundInstantiatedElement = await this.findElementRecursively( + viewport, + this.selectedItem as any + ); + } catch (error) { + console.error('Error during element search retry:', error); + } retries++; } if (!firstFoundInstantiatedElement) { this.warning = `no first instantiated element found for >>${anonItem.name}<< after ${retries} retries`; + this.propertyContent = []; return; } const classProperties: Map = anonItem.elementProperties; @@ -337,9 +350,10 @@ export class WccProperties extends DeesElement { if (key === 'goBright' || key === 'domtools') { continue; } - const property = classProperties.get(key); - const propertyTypeString = await determinePropertyType(property); - propertyArray.push( + try { + const property = classProperties.get(key); + const propertyTypeString = await determinePropertyType(property); + propertyArray.push( html`
${key} / ${propertyTypeString}
@@ -392,6 +406,10 @@ export class WccProperties extends DeesElement {
` ); + } catch (error) { + console.error(`Error processing property ${key}:`, error); + // Continue with next property even if this one fails + } } this.propertyContent = propertyArray; } else { @@ -413,7 +431,14 @@ export class WccProperties extends DeesElement { } public async scheduleUpdate() { - await this.createProperties(); + try { + await this.createProperties(); + } catch (error) { + console.error('Error creating properties:', error); + // Clear property content on error to show clean state + this.propertyContent = []; + } + // Always call super.scheduleUpdate to ensure component updates super.scheduleUpdate(); }