-
Notifications
You must be signed in to change notification settings - Fork 27.2k
feat(core): add DomRef API in developer preview #56544
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Add DomRef as a safer and more ergonomic API compared to ElementRef. A DomRef is accessed by calling it, similar to a signal. Additionally, it ties result availability to rendering: it does not allow a reference to be unwrapped until after it has had an opportunity to render.
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| /** | ||
| * @license | ||
| * Copyright Google LLC All Rights Reserved. | ||
| * | ||
| * Use of this source code is governed by an MIT-style license that can be | ||
| * found in the LICENSE file at https://angular.io/license | ||
| */ | ||
|
|
||
| import {RuntimeError, RuntimeErrorCode} from '../errors'; | ||
| import {registerDomRefInitializer} from '../render3/after_render_hooks'; | ||
| import {TNode} from '../render3/interfaces/node'; | ||
| import {LView} from '../render3/interfaces/view'; | ||
| import {getCurrentTNode, getLView} from '../render3/state'; | ||
| import {getNativeByTNode} from '../render3/util/view_utils'; | ||
|
|
||
| /** | ||
| * Symbol used to tell `DomRef`s apart from other functions. | ||
| */ | ||
| export const DOMREF = /* @__PURE__ */ Symbol('DOMREF'); | ||
|
|
||
| /** | ||
| * A value that can be used to obtain a reference to a native DOM element. | ||
| * | ||
| * Prefer to use templating and data-binding provided by Angular instead. | ||
| * This API should be used as a last resort when direct DOM access is unavoidable. | ||
| * | ||
| * While a `DomRef` value may be passed around without restriction, attempting | ||
| * to obtain the native DOM element before the next time the application has | ||
| * rendered will throw an error. To avoid this, you should only unwrap a `DomRef` | ||
| * inside of `afterRender`, `afterNextRender`, or an event handler in response to a | ||
| * user interaction. | ||
| * | ||
| * @developerPreview | ||
| */ | ||
| // Note: This needs to be an interface. If it were a type, we would be | ||
| // unable to export the token class below with the same name, which | ||
| // would make it less ergonomic to use. | ||
| export interface DomRef<T> { | ||
| (): T; | ||
| readonly [DOMREF]: unknown; | ||
| } | ||
|
|
||
| /** | ||
| * Creates an DomRef from the most recent node. | ||
| */ | ||
| function injectDomRef(): DomRef<any> { | ||
| return createDomRef(getCurrentTNode()!, getLView()); | ||
| } | ||
|
|
||
| function invalidDomRefGetter(): never { | ||
| throw new RuntimeError( | ||
| RuntimeErrorCode.DOMREF_NOT_READY, | ||
| ngDevMode && | ||
| 'Attempted to read DomRef before it was ready. Make sure that you are waiting ' + | ||
| 'until the next render before reading.', | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a DomRef for the given node. | ||
| */ | ||
| function createDomRef<T>(tNode: TNode, lView: LView): DomRef<T> { | ||
| let getDomRefImpl: () => T = invalidDomRefGetter; | ||
| const getter = function getDomRef() { | ||
| return getDomRefImpl(); | ||
| } as DomRef<T>; | ||
|
|
||
| const nativeElement = getNativeByTNode(tNode, lView) as T; | ||
| // Note: we don't use `internalAfterNextRender` here, as we want | ||
| // to ensure that the DomRef is only initialized imediately prior | ||
| // to the first user after*Render callback. Using `internalAfterNextRender` | ||
| // would risk exposing the native element too early. | ||
| registerDomRefInitializer(() => { | ||
| getDomRefImpl = () => nativeElement; | ||
| }); | ||
|
|
||
| // We don't currently store anything on our symbol, but we need | ||
| // to provide it so that we can identify the function as a DomRef. | ||
| (getter as any)[DOMREF] = null; | ||
| return getter; | ||
| } | ||
|
|
||
| // This class acts as a DI token for DomRef. | ||
| export class DomRef<T> { | ||
| /** @internal */ | ||
| constructor() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Couldn't we make the constructor private instead of throwing ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we would still want to throw anyway, since you could still do I think we'd really just want to declare export function DomRef<T>(): DomRef<T> {
throw new RuntimeError(
RuntimeErrorCode.DOMREF_CONSTRUCTOR,
ngDevMode && 'DomRef cannot be called or constructed',
);
}But I think this might have some other issues. |
||
| throw new RuntimeError( | ||
| RuntimeErrorCode.DOMREF_CONSTRUCTOR, | ||
| ngDevMode && 'DomRef cannot not be constructed manually.', | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @internal | ||
| * @nocollapse | ||
| */ | ||
| static __NG_ELEMENT_ID__: () => DomRef<any> = injectDomRef; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| /** | ||
| * @license | ||
| * Copyright Google LLC All Rights Reserved. | ||
| * | ||
| * Use of this source code is governed by an MIT-style license that can be | ||
| * found in the LICENSE file at https://angular.io/license | ||
| */ | ||
|
|
||
| import {PLATFORM_BROWSER_ID} from '@angular/common/src/platform_id'; | ||
| import {Component, DomRef, PLATFORM_ID, afterNextRender, inject, signal} from '@angular/core'; | ||
| import {TestBed} from '@angular/core/testing'; | ||
|
|
||
| describe('DomRef', () => { | ||
| beforeEach(() => { | ||
| TestBed.configureTestingModule({ | ||
| providers: [{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID}], | ||
| }); | ||
| }); | ||
|
|
||
| it('should initially throw', () => { | ||
| @Component({ | ||
| template: ``, | ||
| standalone: true, | ||
| }) | ||
| class MyComp { | ||
| elRef = inject(DomRef<HTMLElement>); | ||
|
|
||
| constructor() { | ||
| this.elRef(); | ||
| } | ||
| } | ||
|
|
||
| expect(() => { | ||
| TestBed.createComponent(MyComp); | ||
| }).toThrowError(/Attempted to read DomRef before it was ready/); | ||
| }); | ||
|
|
||
| it('should return the native element in afterRender', async () => { | ||
| @Component({ | ||
| template: ``, | ||
| standalone: true, | ||
| }) | ||
| class MyComp { | ||
| elRef = inject(DomRef<HTMLElement>); | ||
| el: HTMLElement | null = null; | ||
|
|
||
| constructor() { | ||
| afterNextRender(() => { | ||
| this.el = this.elRef(); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| const fixture = TestBed.createComponent(MyComp); | ||
| await fixture.whenStable(); | ||
|
|
||
| expect(fixture.componentInstance.el).not.toBe(null); | ||
| expect(fixture.componentInstance.el).toBe(fixture.nativeElement); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1343,6 +1343,9 @@ | |
| { | ||
| "name": "init_dom2" | ||
| }, | ||
| { | ||
| "name": "init_dom_ref" | ||
| }, | ||
| { | ||
| "name": "init_dom_triggers" | ||
| }, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.