Skip to content

fix(compiler): add missing Baseline DOM properties to the schema - #71470

Open
aminesbdev wants to merge 1 commit into
angular:mainfrom
aminesbdev:fix/compiler-dom-schema-baseline-props
Open

aminesbdev wants to merge 1 commit into
angular:mainfrom
aminesbdev:fix/compiler-dom-schema-baseline-props

Conversation

@aminesbdev

@aminesbdev aminesbdev commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • angular.dev application / infrastructure changes
  • Other... Please describe:

What is the current behavior?

The DOM schema is edited by hand now that the extractor is gone. A few Baseline properties are missing from it, so
binding to them fails at build time:

<button [command]="cmd()" [commandForElement]="dialog">Open</button>
<!-- NG8002: Can't bind to 'command' since it isn't a known property of 'button' -->

Same for [commandForElement], [name] on details, [autocorrect], [webkitdirectory], [fetchPriority] on img,
[rel] on form and [aria-braillelabel]. The only option today is the attr. prefix.

[cite] on q and blockquote has never compiled. The entry for those elements reads 'q,blockquote,cite^[HTMLElement]|',
so cite was added as a third element instead of as their property.

Issue Number: N/A

What is the new behavior?

To decide what to add, I listed the writable properties Chrome exposes on each element in the schema (with
createElementNS for SVG) and compared them with the schema. I then kept the ones that are Baseline according to web-features:

Property Elements Baseline
command, commandForElement button 2025 (invoker commands)
autocorrect all HTML elements 2026
webkitdirectory input 2025
rel SVG a 2025
fetchPriority (+ fetchpriority mapping) img, link, script 2024
name details 2024
ariaBrailleLabel, ariaBrailleRoleDescription (+ aria-* mapping) all elements 2024
crossOrigin SVG image widely available
rel form widely available
cite q, blockquote widely available

I left relList out on form and SVG a. It's a read-only DOMTokenList that forwards to rel, so [rel] is the
binding to use.

I also added the Baseline events missing from [HTMLElement] (animationcancel, beforeinput, beforematch,
beforetoggle, command, compositionstart, compositionupdate, compositionend, contentvisibilityautostatechange,
focusin, focusout, scrollend) so the language service suggests them. The MathML entry already lists some of them.
Outside of completions, the only place that reads these events is the native-event exemption of the opt-in
strictUnclaimedEventNames check. Since it ignores case, (animationCancel) or (focusIn) are now exempt, the same way
(animationEnd) already is.

I left out properties that aren't Baseline yet (closedBy, writingSuggestions, interestForElement, blocking,
loading on media) and role. Like the ARIA attributes, role should probably be rendered as an attribute for SSR,
so it seemed better to handle it separately.

I also left out the declarative shadow DOM properties of template (shadowRootMode, shadowRootDelegatesFocus,
shadowRootClonable, shadowRootSerializable). The browser only attaches a shadow root when the HTML parser sees
shadowrootmode. On a <template> created by the renderer, setting shadowRootMode only sets the attribute (checked in
Chrome 156), so the binding would compile but never create a shadow root.

On ngSrc images, priority stays the way to set the fetch priority. NgOptimizedImage writes fetchpriority itself
in ngOnInit, so it overrides a [fetchPriority] binding on the same image.

On the security side, I didn't add anything to dom_security_schema.ts. None of the new properties takes HTML or
is loaded as a resource. command only acts when the user activates the button, and only on the element in
commandForElement. The built-in commands open, close or toggle a dialog or a popover (popoverTargetAction
already does it for popovers) and custom -- commands only dispatch a command event, so it can't navigate or run
script. rel on form and SVG a changes how the form or the link navigates (noopener, noreferrer, opener), the
same way rel does on a and area, which are already bindable without a security context. crossOrigin on SVG
image sets the CORS mode, like crossOrigin on img, and fetchPriority is only a hint. cite holds a URL, but
browsers neither fetch it nor follow it, and ins/del already expose cite without a security context.

The new assertions in dom_element_schema_registry_spec.ts fail on main, except the two cite element checks. Those
make sure <cite> stays a known element and doesn't get the cite property.

Does this PR introduce a breaking change?

  • Yes
  • No

This only allows bindings that used to fail with NG8002.

Other information

Like any property in the schema, these bindings set the property even in browsers that don't support it yet, and
nothing happens there, the same as with an attribute the browser doesn't know. In JIT, the runtime check tests
propName in element in the running browser and doesn't read this schema, so it behaves the same before and after this change.

One thing to keep in mind with SSR: apart from cite, domino doesn't reflect these properties to attributes (I checked
each one). It doesn't reflect decoding or inert either, and both are already in the schema. Like [popover], these
properties are applied once the app runs in the browser. If the server output needs them, a static attribute or the attr. prefix
still works.

commandForElement takes an element, like popoverTargetElement, so it's meant to be bound to a template reference (#dialog).

@angular-robot angular-robot Bot added the area: compiler Issues related to `ngc`, Angular's template compiler label Oct 20, 2026
@ngbot ngbot Bot added this to the Backlog milestone Oct 20, 2026
@aminesbdev
aminesbdev force-pushed the fix/compiler-dom-schema-baseline-props branch 2 times, most recently from 0f16517 to ffe1b39 Compare October 24, 2026 20:56
JeanMeche

This comment was marked as outdated.

@aminesbdev
aminesbdev force-pushed the fix/compiler-dom-schema-baseline-props branch from ffe1b39 to 8ba00a6 Compare October 25, 2026 13:31
@angular-robot angular-robot Bot added the area: language-service Issues related to Angular's VS Code language service label Oct 25, 2026
@aminesbdev

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review! I pushed an update that covers most of it:

  • fetchPriority on img, link and script, crossOrigin on SVG image and rel on SVG a
  • relList removed from form (and not added on SVG a, for the same reason)
  • the missing events: beforematch, focusin, focusout, the composition events and command
  • the table.has guard in buildAttributeCompletionTable, in a separate fix(language-service) commit with a test. <button> now gets [command] and the other elements get (command)
  • no % on the braille entries, plus the aria-brailleroledescription assertion
  • the commit message says Baseline now, instead of "every browser"

For a few points I wanted to check before changing anything, so I put together a small demo: ngc 22.2.2 with the schema of main, the one of this PR, and this PR with your suggestions applied. I ended up somewhere else on these.

I tried the template shadow DOM properties. <template [shadowRootMode]="'open'"> compiles to ɵɵdomProperty("shadowRootMode", "open"), but the host never gets a shadow root. The browser only attaches it when the HTML parser sees shadowrootmode, and on a template created from script the property just sets the attribute:

const host = document.createElement('div');
const template = document.createElement('template');
template.shadowRootMode = 'open';
host.append(template);
host.shadowRoot; // null

So the binding would compile and do nothing. I listed them in the description with the other exclusions instead.

About NG0303, the JIT check doesn't read this schema, it looks at propName in element in the running browser. I ran the same JIT app with [command] and [commandForElement] against both schemas. In current Chrome there's no NG0303 with either one, even though main's schema doesn't know command. Once I remove command and commandForElement from HTMLButtonElement.prototype to mimic an older browser, both log the same 4 NG0303. So JIT behaves the same before and after, only AOT changes.

On the support window, you're right about autocorrect and command. Most of what this review adds isn't in every v22 browser either, though: fetchPriority needs Firefox 132, rel on SVG a Chrome 136, crossOrigin on SVG image Safari 17.2, beforematch Firefox 139 and Safari 26.2. I see the schema as following what browsers expose, but I'm happy to drop autocorrect if you'd rather wait for that one.

For (focusIn), event names are case-sensitive, so it never fires on a native element. In the demo, (focusin) fired on every focus and (focusIn) never did, so NG8030 was right to report it. Because isKnownEventOfAnyElement ignores case, the check now skips (focusIn) and (compositionEnd). I kept the events since completions need them, I just wanted to flag it in case the case-insensitive lookup is worth another look.

The SVG/MathML inheritance is real for autocorrect, but it's already the case on main: <rect [popover] [inert] [contentEditable] [innerText]> compiles there, while <rect [foo]> fails with NG8002. Changing it would break bindings that compile today, so I'd rather keep it for a separate change.

Same for camelCase ARIA in SSR: on main, <div [ariaLabel]="label"> already comes out of the server as an empty <div>, while [aria-label] gets the attribute. The camelCase entry is still needed though. Without ariaBrailleLabel in the schema, [aria-braillelabel] fails with NG8002 too, because the type checker maps the name before calling hasProperty. You're right that _PROP_TO_ATTR is broken. Fixing it changes which DOM completions the language service offers, since it only adds names where the attribute and the property match, so I'd rather send that as its own PR.

Last thing, command only does something when the user activates the button, and only on the element in commandForElement. The built-in commands open, close or toggle a dialog or a popover, which popoverTargetAction already does for popovers, and custom -- commands only dispatch a command event. Agreed it still needs the security sign-off.

@aminesbdev
aminesbdev requested a review from JeanMeche October 25, 2026 13:35
@JeanMeche

Copy link
Copy Markdown
Member

That what's Gemini doing the review for me, let's smooth out the details :

  • let's put shadowRootMode on the side.
  • If it's available we can ship it, so we're good for command, autocorrect etc

Keeping the rest seem reasonable.

@JeanMeche
JeanMeche force-pushed the fix/compiler-dom-schema-baseline-props branch from 8ba00a6 to 7787284 Compare November 10, 2026 20:47
@JeanMeche
JeanMeche requested review from JeanMeche and removed request for crisbeto November 10, 2026 20:54
@JeanMeche
JeanMeche force-pushed the fix/compiler-dom-schema-baseline-props branch 3 times, most recently from 0a7b09c to 8ccd316 Compare November 10, 2026 22:33
Binding to a DOM property that is missing from the schema fails with NG8002,
even when the property is Baseline. This adds the Baseline properties that
were never added to the schema: `command` and `commandForElement` on `button`
(invoker commands), `name` on `details`, `autocorrect`, `webkitdirectory` on
`input`, `fetchPriority` on `img`, `link` and `script`, `rel` on `form` and
SVG `a`, `crossOrigin` on SVG `image`, and the
`aria-braillelabel`/`aria-brailleroledescription` attributes.

The `q`/`blockquote` entry also listed `cite` as an element instead of a
property, so `[cite]` never compiled on those elements.

It also adds attribute-to-property mappings for `fetchpriority` and the
aria-braille attributes (`aria-braillelabel` and `aria-brailleroledescription`).

It also adds the missing events (`animationcancel`, `beforeinput`,
`beforematch`, `beforetoggle`, `command`, the composition events,
`contentvisibilityautostatechange`, `focusin`, `focusout` and `scrollend`)
so the language service can suggest them.
@JeanMeche
JeanMeche force-pushed the fix/compiler-dom-schema-baseline-props branch from 8ccd316 to aad9b08 Compare November 10, 2026 22:56

@alan-agius4 alan-agius4 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AGENT: Thanks for putting this together and for the thorough Baseline breakdown. The schema additions, q/blockquote cite fix, and security posture look good overall, though there are a couple of issues to address before we can merge:

  1. Removing 'fetchpriority' from _ATTR_TO_PROP, since reify.ts and runtime mapPropName don't remap it and [fetchpriority] silently sets a dead expando property at runtime.
  2. Restoring the dropped language-service completion fix (either here or in a prerequisite PR) so *command on [HTMLElement] doesn't clobber [command] completions on <button>.
  3. Restoring !autocorrect so the schema reflects the boolean IDL property.

export const _ATTR_TO_PROP = new Map(
Object.entries({
'class': 'className',
'fetchpriority': 'fetchPriority',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AGENT: Adding 'fetchpriority': 'fetchPriority' here lets <img [fetchpriority]="..."> pass template typechecking (REGISTRY.getMappedPropName), however codegen keeps the raw binding name (mapPropertyName is false in r3_template_transform.ts). Unlike aria-* attributes, entries in _ATTR_TO_PROP also need to exist in DOM_PROPERTY_REMAPPING (packages/compiler/src/template/pipeline/src/phases/reify.ts) and mapPropName (packages/core/src/render3/instructions/shared.ts), neither of which remaps 'fetchpriority'.

As a result, <img [fetchpriority]="'high'"> compiles without NG8002 in AOT and silently writes a dead img.fetchpriority expando property at runtime instead of img.fetchPriority, while logging NG0303 in JIT dev mode.

Other camelCase properties like crossOrigin and referrerPolicy are intentionally omitted here so they are bound as [crossOrigin] or [attr.fetchpriority] without adding extra branches to mapPropName in @angular/core. Let's drop 'fetchpriority' here (and update the commit message accordingly):

Suggested change
'fetchpriority': 'fetchPriority',

'base^[HTMLElement]|href,target',
'body^[HTMLElement]|aLink,background,bgColor,link,*afterprint,*beforeprint,*beforeunload,*blur,*error,*focus,*hashchange,*languagechange,*load,*message,*messageerror,*offline,*online,*pagehide,*pageshow,*popstate,*rejectionhandled,*resize,*scroll,*storage,*unhandledrejection,*unload,text,vLink',
'button^[HTMLElement]|!disabled,formAction,formEnctype,formMethod,!formNoValidate,formTarget,name,popoverTargetAction,%popoverTargetElement,type,value',
'button^[HTMLElement]|command,%commandForElement,!disabled,formAction,formEnctype,formMethod,!formNoValidate,formTarget,name,popoverTargetAction,%popoverTargetElement,type,value',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AGENT: It looks like the language-service commit (22fb98a668) mentioned in your earlier comment was dropped in the latest force push, and it hasn't landed on main yet.

Because *command is still on [HTMLElement] (line 88), buildAttributeCompletionTable (packages/language-service/src/attribute_completions.ts:417-422) inserts 'command' as DomAttribute for <button> and then overwrites it with DomEvent. That prevents <button [com|]> from suggesting [command] and also lets the 12 new events overwrite same named directive @Input() completions.

If you'd prefer to keep the language-service fix in a separate PR, let's get that landed first (ideally keying DomEvent entries distinctly such as (${event}) so neither [command] nor (command) hides the other), or restore the fix into this PR.

/* added manually to avoid breaking changes */
',*message,*mozfullscreenchange,*mozfullscreenerror,*mozpointerlockchange,*mozpointerlockerror,*webglcontextcreationerror,*webglcontextlost,*webglcontextrestored',
'[HTMLElement]^[Element]|accessKey,autocapitalize,!autofocus,contentEditable,dir,!draggable,enterKeyHint,!hidden,!inert,innerText,inputMode,lang,nonce,*abort,*animationend,*animationiteration,*animationstart,*auxclick,*beforexrselect,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*formdata,*gotpointercapture,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*lostpointercapture,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*paste,*pause,*play,*playing,*pointercancel,*pointerdown,*pointerenter,*pointerleave,*pointermove,*pointerout,*pointerover,*pointerrawupdate,*pointerup,*progress,*ratechange,*reset,*resize,*scroll,*securitypolicyviolation,*seeked,*seeking,*select,*selectionchange,*selectstart,*slotchange,*stalled,*submit,*suspend,*timeupdate,*toggle,*transitioncancel,*transitionend,*transitionrun,*transitionstart,*volumechange,*waiting,*webkitanimationend,*webkitanimationiteration,*webkitanimationstart,*webkittransitionend,*wheel,outerText,popover,!spellcheck,%style,#tabIndex,title,!translate,virtualKeyboardPolicy',
'[HTMLElement]^[Element]|accessKey,autocapitalize,autocorrect,!autofocus,contentEditable,dir,!draggable,enterKeyHint,!hidden,!inert,innerText,inputMode,lang,nonce,*abort,*animationcancel,*animationend,*animationiteration,*animationstart,*auxclick,*beforeinput,*beforematch,*beforetoggle,*beforexrselect,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*command,*compositionend,*compositionstart,*compositionupdate,*contentvisibilityautostatechange,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*focusin,*focusout,*formdata,*gotpointercapture,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*lostpointercapture,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*paste,*pause,*play,*playing,*pointercancel,*pointerdown,*pointerenter,*pointerleave,*pointermove,*pointerout,*pointerover,*pointerrawupdate,*pointerup,*progress,*ratechange,*reset,*resize,*scroll,*scrollend,*securitypolicyviolation,*seeked,*seeking,*select,*selectionchange,*selectstart,*slotchange,*stalled,*submit,*suspend,*timeupdate,*toggle,*transitioncancel,*transitionend,*transitionrun,*transitionstart,*volumechange,*waiting,*webkitanimationend,*webkitanimationiteration,*webkitanimationstart,*webkittransitionend,*wheel,outerText,popover,!spellcheck,%style,#tabIndex,title,!translate,virtualKeyboardPolicy',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AGENT: Small catch on autocorrect: even though the HTML attribute takes "on" or "off", SCHEMA models the DOM IDL property type, and HTMLElement.prototype.autocorrect is a boolean in both the HTML spec and TypeScript's lib.dom.d.ts (matching !spellcheck and !translate on this line). Assigning "off" in JS coerces to true, so we should restore the ! prefix here:

Suggested change
'[HTMLElement]^[Element]|accessKey,autocapitalize,autocorrect,!autofocus,contentEditable,dir,!draggable,enterKeyHint,!hidden,!inert,innerText,inputMode,lang,nonce,*abort,*animationcancel,*animationend,*animationiteration,*animationstart,*auxclick,*beforeinput,*beforematch,*beforetoggle,*beforexrselect,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*command,*compositionend,*compositionstart,*compositionupdate,*contentvisibilityautostatechange,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*focusin,*focusout,*formdata,*gotpointercapture,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*lostpointercapture,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*paste,*pause,*play,*playing,*pointercancel,*pointerdown,*pointerenter,*pointerleave,*pointermove,*pointerout,*pointerover,*pointerrawupdate,*pointerup,*progress,*ratechange,*reset,*resize,*scroll,*scrollend,*securitypolicyviolation,*seeked,*seeking,*select,*selectionchange,*selectstart,*slotchange,*stalled,*submit,*suspend,*timeupdate,*toggle,*transitioncancel,*transitionend,*transitionrun,*transitionstart,*volumechange,*waiting,*webkitanimationend,*webkitanimationiteration,*webkitanimationstart,*webkittransitionend,*wheel,outerText,popover,!spellcheck,%style,#tabIndex,title,!translate,virtualKeyboardPolicy',
'[HTMLElement]^[Element]|accessKey,autocapitalize,!autocorrect,!autofocus,contentEditable,dir,!draggable,enterKeyHint,!hidden,!inert,innerText,inputMode,lang,nonce,*abort,*animationcancel,*animationend,*animationiteration,*animationstart,*auxclick,*beforeinput,*beforematch,*beforetoggle,*beforexrselect,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*command,*compositionend,*compositionstart,*compositionupdate,*contentvisibilityautostatechange,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*focusin,*focusout,*formdata,*gotpointercapture,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*lostpointercapture,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*paste,*pause,*play,*playing,*pointercancel,*pointerdown,*pointerenter,*pointerleave,*pointermove,*pointerout,*pointerover,*pointerrawupdate,*pointerup,*progress,*ratechange,*reset,*resize,*scroll,*scrollend,*securitypolicyviolation,*seeked,*seeking,*select,*selectionchange,*selectstart,*slotchange,*stalled,*submit,*suspend,*timeupdate,*toggle,*transitioncancel,*transitionend,*transitionrun,*transitionstart,*volumechange,*waiting,*webkitanimationend,*webkitanimationiteration,*webkitanimationstart,*webkittransitionend,*wheel,outerText,popover,!spellcheck,%style,#tabIndex,title,!translate,virtualKeyboardPolicy',

expect(registry.hasProperty('input', 'popoverTargetAction', [])).toBeTruthy();
expect(registry.hasProperty('input', 'popoverTargetElement', [])).toBeTruthy();
expect(registry.hasProperty('div', 'autocorrect', [])).toBeTruthy();
expect(registry.hasProperty('div', 'ariaBrailleLabel', [])).toBeTruthy();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AGENT: Since *command is also registered on [HTMLElement], let's add a check that command isn't exposed as a bindable property on non button elements:

Suggested change
expect(registry.hasProperty('div', 'ariaBrailleLabel', [])).toBeTruthy();
expect(registry.hasProperty('button', 'command', [])).toBeTruthy();
expect(registry.hasProperty('div', 'command', [])).toBeFalsy();

@pullapprove
pullapprove Bot requested a review from alan-agius4 November 11, 2026 09:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: compiler Issues related to `ngc`, Angular's template compiler area: language-service Issues related to Angular's VS Code language service

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants