Skip to content

feat(fast-html): add AttributeMap class for automatic @attr definitions #7354

Merged
janechu merged 8 commits intomainfrom
users/janechu/create-attribute-map
Apr 14, 2026
Merged

feat(fast-html): add AttributeMap class for automatic @attr definitions #7354
janechu merged 8 commits intomainfrom
users/janechu/create-attribute-map

Conversation

@janechu
Copy link
Copy Markdown
Collaborator

@janechu janechu commented Mar 31, 2026

Pull Request

📖 Description

Adds an AttributeMap class to @microsoft/fast-html that automatically defines @attr properties on a custom element's class prototype based on the JSON schema generated by TemplateElement.

When attributeMap: "all" is configured for an element via TemplateElement.options(), AttributeMap inspects the schema after template processing and creates reactive properties for all leaf bindings — simple template expressions like {{foo}} or id="{{foo-bar}}" that have no nested properties, no array type, and no child element references.

Key behaviours:

  • No normalization: the binding key as written in the template is used as-is for both the attribute name and the property name (e.g. {{foo-bar}} → attribute foo-bar, property foo-bar)
  • Lowercase required: because HTML attributes are case-insensitive, binding keys should use lowercase names (optionally dash-separated)
  • Bracket notation: properties containing dashes must be accessed via bracket notation (e.g. element["foo-bar"])
  • Skips non-leaf properties: paths like {{user.name}} result in user having sub-properties in the schema and are excluded
  • Skips existing accessors: properties already decorated with @attr or @observable are left untouched
  • Updates FASTElementDefinition: attributeLookup and propertyLookup are patched so attributeChangedCallback correctly delegates to the new AttributeDefinition
  • No decorator syntax required: uses Observable.defineProperty with an AttributeDefinition instance directly

Usage

TemplateElement.options({
    "my-element": {
        attributeMap: "all",
    },
});
<f-template name="my-element">
    <template>
        <p>{{greeting}}</p>
        <p>{{first-name}}</p>
    </template>
</f-template>

This registers greeting (attribute greeting, property greeting) and first-name (attribute first-name, property first-name) as @attr properties, enabling setAttribute("first-name", "Jane") to trigger a template re-render automatically.

This mirrors the existing ObserverMap integration pattern.

📑 Test Plan

Tests were added across two spec files:

  • packages/fast-html/src/components/attribute-map.spec.ts — verifies accessor registration, no-normalization identity mapping, event handler exclusion, and FASTElementDefinition lookup updates
  • packages/fast-html/test/fixtures/attribute-map/attribute-map.spec.ts — end-to-end tests verifying template re-rendering when properties are set via JavaScript or setAttribute

The attribute-map fixture follows the standard fixture pattern (entry.html, templates.html, state.json) and its index.html is generated by npm run build:fixtures with SSR-rendered shadow DOM.

All existing tests continue to pass.

✅ Checklist

General

  • I have included a change request file using $ npm run change
  • I have added tests for my changes.
  • I have tested my changes.
  • I have updated the project documentation to reflect my changes.
  • I have read the CONTRIBUTING documentation and followed the standards for this project.

⏭ Next Steps

  • Investigate whether observedAttributes can be updated after element registration to fully support the DOM attribute → property direction for elements registered before the template is processed

@janechu janechu marked this pull request as ready for review March 31, 2026 21:39
@janechu janechu marked this pull request as draft April 6, 2026 20:59
@janechu janechu force-pushed the users/janechu/create-attribute-map branch from 1e71068 to 28fd2c1 Compare April 14, 2026 19:22
@janechu janechu changed the title feat(fast-html): add AttributeMap class for automatic @attr definitions feat(fast-html): add AttributeMap class for automatic @attr definitions (no normalization) Apr 14, 2026
@janechu janechu marked this pull request as ready for review April 14, 2026 19:29
@janechu janechu changed the title feat(fast-html): add AttributeMap class for automatic @attr definitions (no normalization) feat(fast-html): add AttributeMap class for automatic @attr definitions Apr 14, 2026
@janechu janechu marked this pull request as draft April 14, 2026 19:45
@janechu janechu force-pushed the users/janechu/create-attribute-map branch from 28fd2c1 to c31bc76 Compare April 14, 2026 19:53
@janechu janechu marked this pull request as ready for review April 14, 2026 19:55
@janechu janechu requested review from Copilot April 14, 2026 19:56
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds an AttributeMap feature to @microsoft/fast-html to automatically define @attr-style reactive properties for leaf template bindings, enabled via TemplateElement.options({ ..., attributeMap: "all" }).

Changes:

  • Introduces AttributeMap + attributeMap element option and wires it into TemplateElement processing.
  • Adds a new fixture and Playwright coverage validating attribute/property identity mapping and re-render behavior via setAttribute.
  • Updates package docs and fixture build script to document and include the new capability.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/fast-html/src/components/template.ts Adds attributeMap option plumbing and invokes AttributeMap.defineProperties() during template processing.
packages/fast-html/src/components/index.ts Exports AttributeMap and AttributeMapOption from the public components entrypoint.
packages/fast-html/src/components/attribute-map.ts Implements schema-driven detection of leaf bindings and defines them as @attr accessors.
packages/fast-html/src/components/attribute-map.spec.ts Adds Playwright tests validating generated accessors, exclusions, reflection, and lookup patch behavior.
packages/fast-html/scripts/build-fixtures.js Registers the new attribute-map fixture in the fixture build list.
packages/fast-html/test/fixtures/attribute-map/entry.html Adds the fixture entry page for E2E tests.
packages/fast-html/test/fixtures/attribute-map/index.html Adds the generated SSR fixture output HTML for the new fixture.
packages/fast-html/test/fixtures/attribute-map/main.ts Defines test custom elements and configures TemplateElement.options({ attributeMap: "all" }).
packages/fast-html/test/fixtures/attribute-map/state.json Initial fixture state for leaf bindings (foo, foo-bar).
packages/fast-html/test/fixtures/attribute-map/templates.html Fixture templates exercising leaf bindings and click handlers.
packages/fast-html/test/fixtures/attribute-map/attribute-map.spec.ts Adds fixture-based Playwright E2E tests (DOM behavior + reflection).
packages/fast-html/README.md Documents the new attributeMap option, behavior, and usage.
packages/fast-html/DESIGN.md Adds design/behavior notes explaining AttributeMap and “leaf binding” rules.
change/@microsoft-fast-html-90d4b1e4-b272-4691-9b04-e6290ebc8b2f.json Adds the change file for release notes/versioning.

janechu and others added 8 commits April 14, 2026 14:12
AttributeMap inspects the JSON schema generated by TemplateElement for a
custom element and defines @attr properties on the class prototype for
all leaf-level bindings (simple {{foo}} and attribute {{bar}} paths that
have no nested properties, no type, and no anyOf).

- Add AttributeMap class in packages/fast-html/src/components/attribute-map.ts
  - Reads root properties from the Schema
  - Skips properties with nested 'properties', 'type', or 'anyOf' (not leaves)
  - Skips properties that already have an @attr or @observable accessor
  - Converts camelCase property names to dash-case (fooBar -> foo-bar)
  - Creates AttributeDefinition instances via Observable.defineProperty
  - Updates FASTElementDefinition.attributeLookup and propertyLookup
- Integrate AttributeMap into TemplateElement (template.ts / index.ts)
  - Add AttributeMapOption constant and type
  - Add attributeMap option to ElementOptions interface
  - TemplateElement.options() stores attributeMap option
  - connectedCallback instantiates AttributeMap when attributeMap === 'all'
  - defineProperties() called after schema is fully populated
- Add tests in attribute-map.spec.ts (browser E2E tests)
- Add fixture in test/fixtures/attribute-map/

Usage:
  TemplateElement.options({
    'my-element': { attributeMap: 'all' },
  });

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…emplate updates

Update AttributeMap.defineProperties() to also push each newly created
attribute name to the existing observedAttributes array on the class.

For all f-template-registered elements, registry.define() (which causes
the browser to cache observedAttributes) is called AFTER defineProperties()
runs, because composeAsync() waits for definition.template to be set
before resolving. This creates a reliable window to mutate the array so
the browser observes the dynamically-added attributes.

Update tests to use element.setAttribute() inside page.evaluate instead
of button clicks, testing both directions:
- setAttribute() → attributeChangedCallback() → property → template re-render
- property assignment → attribute reflection via DOM

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…om attribute-map fixture

Tests no longer need window.Observable or window.__FAST__ to verify
AttributeMap behaviour. Use Object.getOwnPropertyDescriptor to check
that accessor get/set was added to the prototype, and verify attribute
lookup via setAttribute behaviour instead of inspecting internal
registry state.

Also update the beachball change type to prerelease.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…by AttributeMap

Add an AttributeMapWithExistingAttrElement fixture element with a
pre-defined @attr foo property (default value 'original'). After
f-template processes with attributeMap: 'all', tests confirm that:
- the @attr default value is preserved (accessor was not re-defined)
- setAttribute() still routes through the original @attr definition

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Attribute names are no longer converted from camelCase to dash-case.
The binding key as written in the template is used as-is for both the
attribute name and the property name (e.g. {{foo-bar}} → attribute
foo-bar, property foo-bar).

Because HTML attributes are case-insensitive, binding keys should use
lowercase names (optionally dash-separated). Properties containing
dashes must be accessed via bracket notation (element["foo-bar"]).

- Remove camelCaseToDashCase method from AttributeMap
- Update fixture template to use {{foo-bar}} instead of {{fooBar}}
- Update all tests to use bracket notation for dash-case properties
- Add AttributeMap documentation to DESIGN.md and README.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ixture

Split the hand-written index.html into entry.html, templates.html, and
state.json to match the pattern used by all other fixtures. Add
attribute-map to the build-fixtures.js list so index.html is generated
by npm run build:fixtures with SSR-rendered shadow DOM.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove async from test.describe callbacks in both spec files
  (Playwright requires synchronous describe callbacks)
- Convert existingAccessors to a Set for O(1) lookup instead of O(N)
- Update definition.attributes array alongside attributeLookup and
  propertyLookup to keep the definition's canonical attribute list
  in sync
- Add .define() call to attributeMap README usage snippet
- Add comment explaining why observedAttributes mutation via push
  is safe (FAST uses a concrete array, and registry.define() runs
  after this method)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu janechu force-pushed the users/janechu/create-attribute-map branch from 1dd91d2 to 64b8760 Compare April 14, 2026 21:13
@janechu janechu merged commit 27f65b9 into main Apr 14, 2026
14 checks passed
@janechu janechu deleted the users/janechu/create-attribute-map branch April 14, 2026 22:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants