Skip to content

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

Open
janechu wants to merge 6 commits intomainfrom
users/janechu/create-attribute-map
Open

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

Conversation

@janechu
Copy link
Copy Markdown
Collaborator

@janechu janechu commented Mar 31, 2026

Pull Request

📖 Description

This PR 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(), the AttributeMap inspects the schema after template processing and creates @attr-style reactive properties for all leaf bindings — i.e. simple template expressions like {{foo}} or <div id="{{bar}}"> that have no nested properties, no array type, and no child element references.

Key behaviours:

  • camelCase → dash-case: property fooBar is registered with attribute name 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 to avoid duplicating accessors
  • 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, bypassing the decorator metadata path (which has already run before the template is processed)

Usage

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

This mirrors the existing ObserverMap integration pattern.

📑 Test Plan

13 Playwright tests were added across two spec files:

  • packages/fast-html/src/components/attribute-map.spec.ts — verifies accessor registration, camelCase→dash-case conversion, event handler exclusion, and FASTElementDefinition lookup updates by navigating to the fixture page
  • 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

All existing tests (666 Playwright + 4 rules) continue to pass.

npm run build -w @microsoft/fast-html   # ✅ passes
npm run test -w @microsoft/fast-html    # ✅ 666 passed, 0 failed

✅ 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

  • Consider adding attributeMap support to the package README / documentation once the API stabilises
  • Investigate whether observedAttributes can be updated after element registration to fully support the DOM attribute → property direction (currently, only property → DOM attribute reflection works for elements registered before the template is processed)

janechu and others added 4 commits March 30, 2026 20:50
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>
@janechu janechu marked this pull request as ready for review March 31, 2026 21:39
janechu and others added 2 commits March 31, 2026 14:45
…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>
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.

1 participant