# withAttrs Attribute Patterns
## Parsing Attributes with `parseWithAttrs`
The `parseWithAttrs` function provides a declarative way to read and parse HTML attributes and pass the parsed values into the spawned enhancement constructor.
### Automatic Integration with Enhancement Spawning
**Important**: When using the `enh.get()`, `enh.set`, or `assignGingerly()` methods with registry items, you typically **do not need to call `parseWithAttrs()` manually**. The attribute parsing happens automatically during enhancement spawning when you include a `withAttrs` property in your registry item configuration.
```html
```
```TypeScript
import 'assign-gingerly/object-extension.js';
class MyEnhancement {
elementRef;
ctx;
count = 0;
theme = 'light';
constructor(oElement, ctx, initVals) {
this.element = new WeakRef(oElement);
this.ctx = ctx;
// initVals automatically contains parsed attributes!
if (initVals) {
Object.assign(this, initVals);
}
}
}
const element = document.querySelector('my-element');
const enhancementConfig = {
spawn: MyEnhancement,
enhKey: 'myEnh',
withAttrs: {
base: 'my-enhancement',
count: '${base}-count',
_count: { instanceOf: 'Number' },
theme: '${base}-theme'
}
};
// Spawn the enhancement - attributes are automatically parsed!
const instance = element.enh.get(enhancementConfig);
console.log(instance.count); // 42 (parsed from attribute)
console.log(instance.theme); // 'dark' (parsed from attribute)
```
Example without enhKey
```TypeScript
// withAttrs works even without enhKey
class SimpleEnhancement {
element;
ctx;
value = null;
constructor(oElement, ctx, initVals) {
this.element = oElement;
this.ctx = ctx;
if (initVals) {
Object.assign(this, initVals);
}
}
}
const element = document.createElement('div');
element.setAttribute('data-value', 'test123');
const config = {
spawn: SimpleEnhancement,
// No enhKey - attributes still parsed!
withAttrs: {
base: 'data-',
value: '${base}value'
}
};
const instance = element.enh.get(config);
console.log(instance.value); // 'test123' (parsed from attribute)
```
How it works
1. When an enhancement is spawned via `enh.get()`, `enh.set`, or `assignGingerly()`
2. If the registry item has a `withAttrs` property defined
3. `parseWithAttrs(element, registryItem.withAttrs)` is automatically called
4. The parsed attributes are passed to the enhancement constructor as `initVals`
5. If the registry item also has an `enhKey`, the parsed attributes are merged with any existing values from `element.enh[enhKey]` (existing values take precedence)
> [!NOTE]
> `withAttrs` works with or without `enhKey`. When there's no `enhKey`, the parsed attributes are passed directly to the constructor. When there is an `enhKey`, they're merged with any pre-existing values on the enh container.
### The `enh-` Prefix for Attribute Isolation
The `parseWithAttrs` function supports an `enh-` prefix for attributes to provide better isolation and avoid conflicts, especially for custom elements and SVG elements.
**Behavior by Element Type:**
- **Built-in HTML elements** (div, span, etc.): The `enh-` prefix acts as an **alias**. The function tries `enh-` prefixed attributes first, then falls back to unprefixed attributes.
```html
```
- **Custom elements and SVG elements**: The `enh-` prefix is **strictly enforced** by default. Only `enh-` prefixed attributes are read.
```html
```
**Overriding with `allowUnprefixed`:**
For custom elements and SVG, you can opt-in to reading unprefixed attributes by specifying a pattern (string or RegExp) that the element's tag name must match:
```TypeScript
// Allow unprefixed for elements matching pattern
registry.push({
spawn: MyEnhancement,
enhKey: 'myEnh',
allowUnprefixed: '^my-', // Only for elements starting with "my-"
withAttrs: {
base: 'data-',
count: '${base}count',
_count: { instanceOf: 'Number' }
}
});
// Or use RegExp for more complex patterns
registry.push({
spawn: MyEnhancement,
enhKey: 'myEnh',
allowUnprefixed: /^(my-|app-)/, // For "my-*" or "app-*" elements
withAttrs: {
base: 'data-',
count: '${base}count',
_count: { instanceOf: 'Number' }
}
});
```
Why use `enh-` prefix?
1. **Avoid conflicts**: Custom elements may use unprefixed attributes for their own purposes
2. **Clear intent**: Makes it obvious which attributes are for enhancements
3. **Future-proof**: Protects against future attribute additions to custom elements
4. **Consistency**: Provides a standard convention across all enhanced elements
5. **Selective override**: Pattern-based `allowUnprefixed` lets you opt-in specific element families while maintaining strict isolation for others
Manual Usage
While automatic parsing is the recommended approach, you can also call `parseWithAttrs()` manually when needed.
When calling `parseWithAttrs()` manually, pass the pattern as the third (optional) parameter:
```TypeScript
// Allow unprefixed only for elements matching pattern
const result = parseWithAttrs(element, attrPatterns, '^my-');
// Or with RegExp
const result = parseWithAttrs(element, attrPatterns, /^(my-|app-)/);
```
**Pattern Matching:**
- The pattern is tested against the element's **lowercase tag name**
- String patterns are automatically converted to RegExp
- If the tag name matches, unprefixed attributes are allowed (but `enh-` still takes precedence)
- If the tag name doesn't match, only `enh-` prefixed attributes are read
**Example:**
```html
```
```TypeScript
// Pattern: '^my-' (only matches "my-widget")
const result1 = parseWithAttrs(
document.querySelector('my-widget'),
{ base: 'data-', count: '${base}count', _count: { instanceOf: 'Number' } },
'^my-'
);
// result1.count = 42 (unprefixed allowed because tag matches)
const result2 = parseWithAttrs(
document.querySelector('other-widget'),
{ base: 'data-', count: '${base}count', _count: { instanceOf: 'Number' } },
'^my-'
);
// result2.count = undefined (unprefixed ignored because tag doesn't match)
```
### Basic Usage
```TypeScript
import { parseWithAttrs } from 'assign-gingerly/parseWithAttrs';
const element = document.querySelector('#myElement');
const config = parseWithAttrs(element, {
base: 'data-',
count: '${base}count',
_count: {
instanceOf: 'Number',
mapsTo: 'itemCount'
}
});
```
### Error Handling
The function throws descriptive errors for common issues:
```TypeScript
// Circular reference
parseWithAttrs(element, {
a: '${b}',
b: '${a}' // Error: Circular reference detected
});
// Undefined variable
parseWithAttrs(element, {
name: '${missing}' // Error: Undefined template variable: missing
});
// Invalid JSON
// HTML:
parseWithAttrs(element, {
base: 'data-',
obj: '${base}obj',
_obj: { instanceOf: 'Object' }
// Error: Failed to parse JSON: "{invalid}"
});
// Invalid number
// HTML:
parseWithAttrs(element, {
base: 'data-',
count: '${base}count',
_count: { instanceOf: 'Number' }
// Error: Failed to parse number: "abc"
});
```
**Base Attribute Validation:**
The `base` attribute must contain either a dash (`-`) or a non-ASCII character to prevent conflicts with native attributes:
```TypeScript
// Valid base attributes
const enhConfig1 = { base: 'data-config' }; // Has dash
const enhConfig2 = { base: '??-theme' }); // Has non-ASCII (and dash)
// Invalid - throws error
const enhConig3 = { base: 'config' }; // No dash or non-ASCII
```
AttrPatterns Configuration
The `parseWithAttrs` function accepts an `AttrPatterns` object that defines:
1. **Attribute name templates**: String values with `${variable}` placeholders
2. **Configuration objects**: Properties prefixed with `_` that specify parsing behavior
```TypeScript
interface AttrPatterns {
base?: string; // Base attribute name prefix
_base?: AttrConfig; // Configuration for base attribute
[key: string]: string | AttrConfig; // Other attributes and configs
}
interface AttrConfig {
mapsTo?: keyof T | '.'; // Target property name (or '.' to spread)
sourceOfTruth?: boolean; // Mark attribute as source-of-truth (mirrored by property)
instanceOf?: string | Function; // Type for default parser
parser?:
| ((v: string | null) => any) // Inline parser function
| string // Named parser from globalParserRegistry
| [string, string] // [CustomElementName, StaticMethodName]
| { name: string; options?: any }; // Named class parser with constructor options
}
```
### Template Variables
Attribute names support template variables using `${varName}` syntax:
```TypeScript
// HTML:
const result = parseWithAttrs(element, {
base: 'data-',
user: '${base}user',
name: '${user}-name',
age: '${user}-age'
});
// Result: { name: 'Alice', age: '30' }
```
**Deep Nesting:**
Template variables can reference other template variables to any depth, creating hierarchical attribute naming patterns:
```TypeScript
// HTML:
const result = parseWithAttrs(element, {
base: 'data-',
app: '${base}app',
user: '${app}-user',
profile: '${user}-profile',
name: '${profile}-name',
email: '${profile}-email'
});
// Result: { name: 'Alice', email: 'alice@example.com' }
// The resolution chain: base ? app ? user ? profile ? name/email
// Resolves to: data-app-user-profile-name and data-app-user-profile-email
```
**Benefits of hierarchical variables:**
- Build complex attribute names from simple parts
- Maintain consistency across related attributes
- Easy to refactor by changing a single variable
- Self-documenting attribute structure
Template variables are resolved recursively and cached for performance. Circular references are detected and throw an error.
### Type Parsing with instanceOf
The `instanceOf` property determines how attribute values are parsed:
```TypeScript
// HTML:
const result = parseWithAttrs(element, {
base: 'data-',
count: '${base}count',
_count: { instanceOf: 'Number' },
active: '${base}active',
_active: { instanceOf: 'Boolean' }, // Presence check
tags: '${base}-tags',
_tags: { instanceOf: 'Array' }
});
// Result: { count: 42, active: true, tags: ['a', 'b'] }
```
**Built-in type parsers:**
- `String`: Identity (default)
- `Number`: Parses numeric values, throws on invalid numbers
- `Boolean`: Presence check (attribute exists = true)
- `Object`: Parses JSON objects
- `Array`: Parses JSON arrays
### Custom Parsers
Provide a custom `parser` function for specialized parsing:
```TypeScript
// HTML:
const result = parseWithAttrs(element, {
base: 'data-',
timestamp: '${base}timestamp',
_timestamp: {
mapsTo: 'createdAt',
parser: (v) => v ? new Date(v).getTime() : null
}
});
// Result: { createdAt: 1705315800000 }
```
### Named Parsers for Reusability and JSON Serialization
Instead of inline functions, you can reference parsers by name, making configs JSON serializable and parsers reusable:
```TypeScript
import { globalParserRegistry, parseWithAttrs } from 'assign-gingerly';
// Register function-based parsers once (typically in app initialization)
globalParserRegistry.register('timestamp', (v) =>
v ? new Date(v).getTime() : null
);
// Register class-based parsers the same way
globalParserRegistry.register('my-splitter', MySplitParser);
// Use by name - config is now JSON serializable!
const config = {
base: 'data-',
created: '${base}created',
_created: {
parser: 'timestamp' // Function parser by name
},
tags: '${base}tags',
_tags: {
parser: 'my-splitter' // Class parser by name
}
};
// Can serialize to JSON
const json = JSON.stringify(config);
// Use the config
const result = parseWithAttrs(element, config);
```
#### **Built-in Named Parsers:**
The following parsers are pre-registered in `globalParserRegistry`:
- `'timestamp'` - Parses ISO date string to Unix timestamp (milliseconds)
- `'date'` - Parses string to Date object
- `'int'` - Parses integer with `parseInt(v, 10)`
- `'float'` - Parses float with `parseFloat(v)`
- `'boolean'` - Presence check (same as `instanceOf: 'Boolean'`)
- `'json'` - Parses JSON (same as `instanceOf: 'Object'` or `'Array'`)
- `'splitter'` - Splits an attribute value into an array. A class parser with options:
- `delimiter?: string | { pattern: string; flags?: string }` - defaults to `/\s+/`
- `trim?: boolean` - default `true`
- `skipEmpty?: boolean` - default `true`
- `dedupe?: boolean` - default `false`
**Using the `splitter` parser:**
```TypeScript
// HTML:
// Use the default delimiter (/\s+/)
const result1 = parseWithAttrs(element, {
base: 'data-',
tags: '${base}tags',
_tags: {
parser: 'splitter'
}
});
// result1.tags === ['alpha', 'beta', 'gamma']
// HTML:
// Use a custom literal delimiter
const result2 = parseWithAttrs(element, {
base: 'data-',
tags: '${base}tags',
_tags: {
parser: 'splitter',
parserOptions: { delimiter: ',' }
}
});
// result2.tags === ['alpha', 'beta', 'gamma']
// Same thing using the object form (self-contained options)
const result3 = parseWithAttrs(element, {
base: 'data-',
tags: '${base}tags',
_tags: {
parser: { name: 'splitter', options: { delimiter: ',' } }
}
});
// result3.tags === ['alpha', 'beta', 'gamma']
```
**Custom Element Static Method Parsers:**
You can reference static methods on custom elements using tuple syntax `[elementName, methodName]`:
```TypeScript
class MyWidget extends HTMLElement {
static parseSpecialFormat(v) {
return v ? v.toUpperCase() : null;
}
static parseWithPrefix(v) {
return v ? `PREFIX:${v}` : null;
}
}
customElements.define('my-widget', MyWidget);
// Reference custom element parsers using tuple syntax
const config = {
base: 'data-',
value: '${base}value',
_value: {
parser: ['my-widget', 'parseSpecialFormat'] // [element-name, methodName]
},
title: '${base}title',
_title: {
parser: ['my-widget', 'parseWithPrefix']
}
};
const result = parseWithAttrs(element, config);
```
**Parser Resolution:**
When a parser is specified, it can be:
1. **Inline function** - `parser: (v) => v.toUpperCase()` - Used directly
2. **String reference** - `parser: 'timestamp'` - Looks up in scoped registry (if available) then `globalParserRegistry`. If the registered value is a class with a `parse` method, it is instantiated using `parserOptions`.
3. **Object reference** - `parser: { name: 'splitter', options: { delimiter: ',' } }` - Looks up the named class parser and instantiates it with the provided options.
4. **Tuple reference** - `parser: ['my-widget', 'parseMethod']` - Looks up a static method on the custom element constructor
**Error Handling:**
The tuple syntax provides clear error messages:
```TypeScript
// Element not found
parser: ['non-existent', 'method']
// Error: Cannot resolve parser [non-existent, method]: custom element "non-existent" not found
// Method not found
parser: ['my-widget', 'nonExistent']
// Error: Cannot resolve parser [my-widget, nonExistent]: static method "nonExistent" not found on custom element "my-widget"
// String not found in registry
parser: 'unknown'
// Error: Parser "unknown" not found. Checked global registry.
```
**Example: Organizing Parsers**
```TypeScript
// parsers.js - Centralized parser definitions
export function registerCommonParsers(registry) {
registry.register('uppercase', (v) => v ? v.toUpperCase() : null);
registry.register('lowercase', (v) => v ? v.toLowerCase() : null);
registry.register('trim', (v) => v ? v.trim() : null);
registry.register('phone', (v) => v ? v.replace(/\D/g, '') : null);
}
// app.js - Register at startup
import { globalParserRegistry } from 'assign-gingerly';
import { registerCommonParsers } from './parsers.js';
registerCommonParsers(globalParserRegistry);
// Now all configs can use these parsers by name
```
**Benefits of Named Parsers:**
- ? **JSON serializable** - Configs can be stored/transmitted as JSON
- ? **Reusable** - Define once, use everywhere
- ? **Maintainable** - Update parser logic in one place
- ? **Testable** - Test parsers independently
- ? **Discoverable** - `globalParserRegistry.getNames()` lists all available parsers
- ? **Backward compatible** - Inline functions still work
**Mixing Inline and Named Parsers:**
```TypeScript
const config = {
base: 'data-',
created: '${base}created',
_created: {
parser: 'timestamp' // Named parser
},
special: '${base}special',
_special: {
parser: (v) => v ? v.split('').reverse().join('') : null // Inline
}
};
```
### Property Mapping with mapsTo
The `mapsTo` property controls where parsed values are placed:
```TypeScript
// HTML:
const result = parseWithAttrs(element, {
base: 'data-',
count: '${base}count',
_count: {
instanceOf: 'Number',
mapsTo: 'itemCount' // Maps to different property name
}
});
// Result: { itemCount: 5 }
```
**Special value `'.'`**: Spreads the parsed object into the root:
```TypeScript
// HTML:
const result = parseWithAttrs(element, {
base: 'data-config',
_base: {
instanceOf: 'Object',
mapsTo: '.' // Spread into root
}
});
// Result: { theme: 'dark', lang: 'en' }
```
### Default Values with valIfNull
The `valIfNull` property allows us to specify default values when attributes are missing:
```TypeScript
// HTML: (no attributes)
const result = parseWithAttrs(element, {
base: 'data-',
theme: '${base}theme',
_theme: {
instanceOf: 'String',
valIfNull: 'light' // Default when attribute is missing
},
count: '${base}count',
_count: {
instanceOf: 'Number',
valIfNull: 0 // Default to 0
}
});
// Result: { theme: 'light', count: 0 }
```
**How it works:**
- **Attribute missing**: If the attribute doesn't exist and `valIfNull` is defined, the default value is used **without calling the parser**
- **Attribute present**: If the attribute exists (even if empty string), the parser is called normally and `valIfNull` is ignored
- **No valIfNull**: If `valIfNull` is undefined and the attribute is missing, the property is not added to the result (current behavior)
**Important notes:**
1. **Parser is bypassed**: When `valIfNull` is used, the parser is NOT called - the default value is used as-is
2. **Empty string vs missing**: `valIfNull` only applies when the attribute is completely absent. If the attribute exists but is empty (`data-count=""`), the parser IS called
3. **Any value allowed**: `valIfNull` can be any JavaScript value: string, number, boolean, object, array, null, etc.
4. **Falsy values work**: Even falsy values like `0`, `false`, `''`, or `null` are valid defaults
**Examples with different types:**
```TypeScript
// Object default
const result1 = parseWithAttrs(element, {
base: 'config-',
settings: '${base}settings',
_settings: {
instanceOf: 'Object',
valIfNull: { enabled: false, mode: 'auto' }
}
});
// Result: { settings: { enabled: false, mode: 'auto' } }
// Boolean default
const result2 = parseWithAttrs(element, {
base: 'feature-',
enabled: '${base}enabled',
_enabled: {
instanceOf: 'Boolean',
valIfNull: false
}
});
// Result: { enabled: false }
// Array default
const result3 = parseWithAttrs(element, {
base: 'data-',
items: '${base}items',
_items: {
instanceOf: 'Array',
valIfNull: []
}
});
// Result: { items: [] }
// null as default
const result4 = parseWithAttrs(element, {
base: 'data-',
value: '${base}value',
_value: {
instanceOf: 'String',
valIfNull: null
}
});
// Result: { value: null }
```
**Comparison: Empty string vs missing attribute:**
```html
```
```TypeScript
const config = {
base: 'data-',
count: '${base}count',
_count: {
instanceOf: 'Number',
valIfNull: 99
}
};
// Missing attribute - uses valIfNull
const result1 = parseWithAttrs(document.querySelector('div:nth-child(1)'), config);
// Result: { count: 99 }
// Empty string - calls parser (returns null for empty Number)
const result2 = parseWithAttrs(document.querySelector('div:nth-child(2)'), config);
// Result: { count: null }
```
### Performance Optimization with parseCache
The `parseCache` property enables caching of parsed attribute values to improve performance when the same attribute values appear repeatedly throughout the document:
```TypeScript
// HTML: Multiple elements with same attribute values
//
//
//
const config = {
base: 'data-',
config: '${base}config',
_config: {
instanceOf: 'Object',
parseCache: 'shared' // Cache and reuse parsed objects
}
};
// First parse - parses and caches
const result1 = parseWithAttrs(element1, config);
// Subsequent parses - returns cached value (no parsing)
const result2 = parseWithAttrs(element2, config);
const result3 = parseWithAttrs(element3, config);
```
**Cache Strategies:**
1. **`'shared'`**: Returns the same object reference from cache
- **Fastest**: No cloning overhead
- **Risk**: Enhancements that mutate the object will affect all instances
- **Best for**: Immutable data or when you trust enhancements not to mutate
2. **`'cloned'`**: Returns a structural clone of the cached object
- **Safer**: Each instance gets its own copy
- **Slower**: Uses `structuredClone()` which has overhead
- **Best for**: Mutable data or when enhancements might modify values
**Examples:**
```TypeScript
// Shared cache - fast but requires discipline
const sharedConfig = {
base: 'data-',
settings: '${base}settings',
_settings: {
instanceOf: 'Object',
parseCache: 'shared' // All instances share same object
}
};
// Cloned cache - safer for mutable data
const clonedConfig = {
base: 'data-',
state: '${base}state',
_state: {
instanceOf: 'Object',
parseCache: 'cloned' // Each instance gets a copy
}
};
// Custom parser with caching
let parseCount = 0;
const customConfig = {
base: 'data-',
timestamp: '${base}timestamp',
_timestamp: {
parser: (v) => {
parseCount++; // Track parse calls
return v ? new Date(v).getTime() : null;
},
parseCache: 'shared' // Parser only called once per unique value
}
};
```
**Important Notes:**
1. **Parser purity**: Parsers should be pure functions (no side effects) when using caching
2. **Boolean types**: Caching is skipped for Boolean types (presence check doesn't benefit)
3. **Cache scope**: Cache is module-level and persists across all `parseWithAttrs()` calls
4. **Cache key**: Values are cached per `(instanceOf, parserType, attributeValue)` tuple
5. **Memory**: Cache grows with unique attribute values encountered (no automatic cleanup)
6. **Browser support**: `'cloned'` strategy requires `structuredClone()` (modern browsers)
**Performance Considerations:**
- **Shared cache**: Best for simple objects, arrays, or when parsing is expensive
- **Cloned cache**: Overhead may negate benefits for simple values (strings, numbers)
- **No cache**: Better for unique values or when parsing is trivial
- **Custom parsers**: Caching is most beneficial when parser does expensive operations (Date parsing, complex transformations)
**Example: Shared cache mutation risk**
```TypeScript
const config = {
base: 'data-',
items: '${base}items',
_items: {
instanceOf: 'Array',
parseCache: 'shared'
}
};
// HTML:
const result1 = parseWithAttrs(element1, config);
result1.items.push(4); // Mutation!
const result2 = parseWithAttrs(element2, config);
console.log(result2.items); // [1,2,3,4] - mutation is visible!
```
**Example: Cloned cache safety**
```TypeScript
const config = {
base: 'data-',
items: '${base}items',
_items: {
instanceOf: 'Array',
parseCache: 'cloned' // Safe from mutations
}
};
const result1 = parseWithAttrs(element1, config);
result1.items.push(4); // Mutation
const result2 = parseWithAttrs(element2, config);
console.log(result2.items); // [1,2,3] - original value preserved
```
### Source of truth
The `sourceOfTruth` flag on an `AttrConfig` marks an attribute whose value should stay in sync with a host property of the same name. `parseWithAttrs` itself does not enforce this synchrony; it only records the intent in the parsed configuration so that higher-level features can act on it.
```TypeScript
withAttrs: {
name: 'name',
_name: { sourceOfTruth: true }
}
```
A common consumer is the [truth-sourcer](https://github.com/bahrus/truth-sourcer) feature. When a custom element declares a source-of-truth attribute:
1. The host element lists the attribute in `static observedAttributes`.
2. The host property is initialized to a non-null value so the feature can infer its type (`string`, `boolean`, or `number`).
3. The host dispatches a propagator event when the property changes.
4. The feature reflects the property value back to the attribute via `setAttribute` (or `removeAttribute` for `null` / `false`).
5. When the attribute changes (for example from server-side rendering or DOM manipulation), the feature coerces the string value to the inferred type and sets the host property.
This gives a small, controlled set of attributes that mirror properties exactly, which is useful for attributes like the platform's native `name` that genuinely need to be present in the DOM. For most custom element state, prefer one-way initial attributes or internal properties instead.
See the [truth-sourcer README](https://github.com/bahrus/truth-sourcer) for a complete working example.
### Base Attribute
The special `base` property handles a single attribute that spreads into the result:
```TypeScript
// HTML:
const result = parseWithAttrs(element, {
base: 'data-greetings'
// Default: spreads into root with Object parser
});
// Result: { hello: 'world', goodbye: 'Mars' }
// With custom mapsTo:
const result2 = parseWithAttrs(element, {
base: 'data-greetings',
_base: {
mapsTo: 'greetings',
instanceOf: 'Object'
}
});
// Result: { greetings: { hello: 'world', goodbye: 'Mars' } }
```
### Best Practices
1. **Use base for common prefixes**: Reduces repetition in attribute names
2. **Leverage template variables**: Build complex attribute names from simple parts
3. **Specify instanceOf**: Ensures proper type conversion
4. **Use mapsTo for clarity**: Map attribute names to meaningful property names
5. **Combine with assignGingerly**: Use nested paths (`?.`) for deep property assignment
6. **Handle missing attributes**: Non-existent attributes are skipped (except Boolean types)
### Nested Paths with assignGingerly
Combine `parseWithAttrs` with `assignGingerly` for nested property assignment:
```TypeScript
// HTML:
const element = document.createElement('div');
const attrs = parseWithAttrs(element, {
base: 'data-',
height: '${base}height',
_height: {
mapsTo: '?.style?.height'
},
isHappy: '${base}-is-happy',
_isHappy: {
instanceOf: 'Boolean',
mapsTo: '?.moods?.personIsHappy'
}
});
assignGingerly(element, attrs);
// element.style.height === '100px'
// element.moods.personIsHappy === true
```
## Attribute patterns for custom element features
Custom element features use `withAttrs` in the same way as enhancements, but attributes are always read unprefixed (there is no `enh-` prefix for features). The parsed values are merged with any programmatic `initVals`; programmatic values take precedence.
```JavaScript
customElements.assignFeatures(ClubMember, {
photoTaker: {
spawn: PhotoTakerImpl,
withAttrs: {
base: 'photo',
resolution: '${base}-resolution',
format: '${base}-format'
}
}
});
```
```HTML
```
This parses into `initVals = { resolution: '4k', format: 'png' }`. By default, non-underscore keys are assumed to be strings with `mapsTo` equal to the key name. The `_key` form is only needed to override defaults -- for example, to parse as `Number`, map to a different property name, or use a custom parser:
```JavaScript
withAttrs: {
base: 'photo',
resolution: '${base}-resolution',
// Override: parse as Number instead of String
_resolution: { instanceOf: 'Number', mapsTo: 'resolutionPx' },
format: '${base}-format'
// No _format needed -- defaults to String, mapsTo: 'format'
}
```
**Merge priority (lowest to highest):**
1. Attribute-parsed values (`withAttrs`)
2. Programmatic `initVals` (from `captureFeatureInitVals`)
## Building CSS Queries with `buildCSSQuery`
The `buildCSSQuery` function generates CSS selector strings that match elements with attributes defined in an enhancement configuration's `withAttrs`. This is particularly useful for libraries like mount-observer that need to find elements that should be enhanced.
### Basic Usage
```TypeScript
import { buildCSSQuery } from 'assign-gingerly';
const config = {
spawn: MyEnhancement,
withAttrs: {
base: 'my-component',
theme: '${base}-theme'
}
};
const query = buildCSSQuery(config, 'div, span');
console.log(query);
// 'div[my-component], span[my-component], div[enh-my-component], span[enh-my-component],
// div[my-component-theme], span[my-component-theme], div[enh-my-component-theme], span[enh-my-component-theme]'
// Use with querySelector
const elements = document.querySelectorAll(query);
```
**Without selectors (matches any element):**
```TypeScript
// Omit the selectors parameter
const query = buildCSSQuery(config);
// or explicitly pass empty string
const query = buildCSSQuery(config, '');
console.log(query);
// '[my-component], [enh-my-component], [my-component-theme], [enh-my-component-theme]'
// Matches any element with these attributes
const elements = document.querySelectorAll(query);
```
### How It Works
`buildCSSQuery` creates a cross-product of:
1. **Selectors**: The CSS selectors you provide (e.g., `'div, span'`)
2. **Attributes**: All attribute names from `withAttrs` (resolving template variables)
3. **Prefixes**: Both unprefixed and `enh-` prefixed versions
This ensures you find all elements that might be enhanced, regardless of whether they use the `enh-` prefix or not.
### Template Variable Resolution
Template variables in `withAttrs` are automatically resolved:
```TypeScript
const config = {
spawn: BeABeacon,
withAttrs: {
base: 'be-a-beacon',
theme: '${base}-theme',
size: '${base}-size'
}
};
buildCSSQuery(config, 'template, script');
// Returns selectors for: be-a-beacon, be-a-beacon-theme, be-a-beacon-size
// Each with both prefixed and unprefixed versions
```
### Complex Selectors
The function supports any valid CSS selector:
```TypeScript
const config = {
spawn: MyEnhancement,
withAttrs: {
base: 'data-enhanced'
}
};
// Classes and IDs
buildCSSQuery(config, 'div.highlight, span#special');
// 'div.highlight[data-enhanced], span#special[data-enhanced], ...'
// Combinators
buildCSSQuery(config, 'div > span, ul li');
// 'div > span[data-enhanced], ul li[data-enhanced], ...'
// Pseudo-classes
buildCSSQuery(config, 'div:hover, span:first-child');
// 'div:hover[data-enhanced], span:first-child[data-enhanced], ...'
// Attribute selectors
buildCSSQuery(config, 'div[existing-attr]');
// 'div[existing-attr][data-enhanced], ...'
```
### Underscore-Prefixed Keys Excluded
Configuration keys starting with `_` are excluded from the query:
```TypeScript
const config = {
spawn: MyEnhancement,
withAttrs: {
base: 'my-attr',
_base: {
mapsTo: 'something' // Config only, not an attribute
},
theme: '${base}-theme',
_theme: {
instanceOf: 'String' // Config only
}
}
};
buildCSSQuery(config, 'div');
// Only includes: my-attr and my-attr-theme
// Does NOT include: _base or _theme
```
### Edge Cases
**Omitting or empty selectors return attribute-only selectors:**
```TypeScript
const config = {
spawn: MyClass,
withAttrs: {
base: 'my-attr',
theme: '${base}-theme'
}
};
buildCSSQuery(config); // Omit selectors parameter
// or
buildCSSQuery(config, ''); // Empty string
// Both return: '[my-attr], [enh-my-attr], [my-attr-theme], [enh-my-attr-theme]'
// Matches any element with these attributes
```
**Empty withAttrs returns empty string:**
```TypeScript
buildCSSQuery({ spawn: MyClass }, 'div'); // '' (no withAttrs)
buildCSSQuery({ spawn: MyClass, withAttrs: {} }, 'div'); // '' (empty withAttrs)
```
**Deduplication:**
```TypeScript
buildCSSQuery(config, 'div, div, div');
// Duplicates are removed automatically
```
**Whitespace handling:**
```TypeScript
buildCSSQuery(config, ' div , span , p ');
// Whitespace is trimmed automatically
```
### Use Cases
1. **Mount Observer Integration**: Find elements that need enhancement
```TypeScript
// Match any element with the attributes
const matching = buildCSSQuery(enhancementConfig);
const observer = new MountObserver({
matching,
do: (mountedElement) => {
enhance(mountedElement);
}
});
```
See [Mount-Observer](https://github.com/bahrus/mount-observer).
2. **Specific Element Types**: Enhance only certain element types
```TypeScript
const query = buildCSSQuery(config, 'template, script');
document.querySelectorAll(query).forEach(el => {
const instance = el.enh.get(config);
});
```
3. **Conditional Enhancement**: Find elements in specific contexts
```TypeScript
const query = buildCSSQuery(config, '.container > div');
const elements = document.querySelectorAll(query);
```
### API Reference
```TypeScript
function buildCSSQuery(
config: EnhancementConfig,
selectors?: string
): string
```
**Parameters:**
- `config`: Enhancement configuration with `withAttrs` property
- `selectors` (optional): Comma-separated CSS selectors (e.g., `'div, span'`)
- If omitted or empty string, returns attribute selectors without element prefix
- This matches any element with the specified attributes
**Returns:**
- CSS query string with cross-product of selectors and attributes
- If selectors is omitted or empty: returns attribute-only selectors (e.g., `'[attr], [enh-attr]'`)
- If withAttrs is missing or empty: returns empty string
**Throws:**
- Error if template variables have circular references
- Error if template variables reference undefined keys
### Performance Notes
- The function is synchronous and fast
- Resulting queries can be long with many attributes, but CSS engines handle this efficiently
- Queries are deduplicated automatically
- Consider caching the result if calling repeatedly with the same config