From f521ee7f1b31c7f6dd2c9bb8f9785ea6deeb89d9 Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Tue, 23 Jun 2026 16:36:01 +0200 Subject: [PATCH] refactor(core): cleanup Meta service The service had a rather old implementation. This is mostly a cleanup. --- .../public-api/platform-browser/index.api.md | 1 - packages/platform-browser/src/browser/meta.ts | 103 ++++++++---------- .../test/browser/meta_spec.ts | 6 +- 3 files changed, 52 insertions(+), 58 deletions(-) diff --git a/goldens/public-api/platform-browser/index.api.md b/goldens/public-api/platform-browser/index.api.md index 8d123284bf8d..28ffb31c01e2 100644 --- a/goldens/public-api/platform-browser/index.api.md +++ b/goldens/public-api/platform-browser/index.api.md @@ -123,7 +123,6 @@ export enum HydrationFeatureKind { // @public export class Meta { - constructor(_doc: any); addTag(tag: MetaDefinition, forceCreation?: boolean): HTMLMetaElement | null; addTags(tags: MetaDefinition[], forceCreation?: boolean): HTMLMetaElement[]; getTag(attrSelector: string): HTMLMetaElement | null; diff --git a/packages/platform-browser/src/browser/meta.ts b/packages/platform-browser/src/browser/meta.ts index e2bc1c7a81f9..36e2a8bf1f7b 100644 --- a/packages/platform-browser/src/browser/meta.ts +++ b/packages/platform-browser/src/browser/meta.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ -import {DOCUMENT, ɵDomAdapter as DomAdapter, ɵgetDOM as getDOM} from '@angular/common'; -import {Inject, Injectable} from '@angular/core'; +import {DOCUMENT, ɵgetDOM as getDOM} from '@angular/common'; +import {inject, Service} from '@angular/core'; /** * Represents the attributes of an HTML `` element. The element itself is @@ -55,12 +55,12 @@ export type MetaDefinition = { * * @publicApi */ -@Injectable({providedIn: 'root'}) +@Service() export class Meta { - private _dom: DomAdapter; - constructor(@Inject(DOCUMENT) private _doc: any) { - this._dom = getDOM(); - } + private readonly _doc = inject(DOCUMENT); + private readonly _dom = getDOM(); + private _cachedHead: HTMLHeadElement | undefined; + /** * Retrieves or creates a specific `` tag element in the current HTML document. * In searching for an existing tag, Angular attempts to match the `name` or `property` attribute @@ -85,13 +85,9 @@ export class Meta { * @returns The matching elements if found, or the new elements. */ addTags(tags: MetaDefinition[], forceCreation: boolean = false): HTMLMetaElement[] { - if (!tags) return []; - return tags.reduce((result: HTMLMetaElement[], tag: MetaDefinition) => { - if (tag) { - result.push(this._getOrCreateElement(tag, forceCreation)); - } - return result; - }, []); + return tags + .filter((tag): tag is MetaDefinition => !!tag) + .map((tag) => this._getOrCreateElement(tag, forceCreation)); } /** @@ -102,8 +98,8 @@ export class Meta { */ getTag(attrSelector: string): HTMLMetaElement | null { if (!attrSelector) return null; - const meta = this._doc.querySelector(`meta[${attrSelector}]`); - return meta?.nodeName.toLowerCase() === 'meta' ? meta : null; + const meta = this._doc.querySelector(buildMetaSelector(attrSelector)); + return isMetaTag(meta) ? meta : null; } /** @@ -114,12 +110,8 @@ export class Meta { */ getTags(attrSelector: string): HTMLMetaElement[] { if (!attrSelector) return []; - const list /*NodeList*/ = this._doc.querySelectorAll(`meta[${attrSelector}]`); - return list - ? (([].slice.call(list) as HTMLElement[]).filter( - (elem) => elem.nodeName.toLowerCase() === 'meta', - ) as HTMLMetaElement[]) - : []; + const list = this._doc.querySelectorAll(buildMetaSelector(attrSelector)); + return list ? Array.from(list).filter((elem) => isMetaTag(elem)) : []; } /** @@ -132,11 +124,11 @@ export class Meta { * @return The modified element. */ updateTag(tag: MetaDefinition, selector?: string): HTMLMetaElement | null { - if (!tag) return null; - selector = selector || this._parseSelector(tag); - const meta: HTMLMetaElement = this.getTag(selector)!; + selector ??= parseSelector(tag); + const meta = this.getTag(selector); if (meta) { - return this._setMetaElementAttributes(tag, meta); + setMetaElementAttributes(tag, meta); + return meta; } return this._getOrCreateElement(tag, true); } @@ -165,52 +157,53 @@ export class Meta { forceCreation: boolean = false, ): HTMLMetaElement { if (!forceCreation) { - const selector: string = this._parseSelector(meta); + const selector: string = parseSelector(meta); // It's allowed to have multiple elements with the same name so it's not enough to // just check that element with the same name already present on the page. We also need to // check if element has tag attributes - const elem = this.getTags(selector).filter((elem) => this._containsAttributes(meta, elem))[0]; + const elem = this.getTags(selector).filter((elem) => containsAttributes(meta, elem))[0]; if (elem !== undefined) return elem; } const element: HTMLMetaElement = this._dom.createElement('meta') as HTMLMetaElement; - this._setMetaElementAttributes(meta, element); + setMetaElementAttributes(meta, element); const head = this._doc.getElementsByTagName('head')[0]; head.appendChild(element); return element; } +} - private _setMetaElementAttributes(tag: MetaDefinition, el: HTMLMetaElement): HTMLMetaElement { - Object.keys(tag).forEach((prop: string) => - el.setAttribute(this._getMetaKeyMap(prop), tag[prop]), - ); - return el; - } +function buildMetaSelector(attrSelector: string): string { + return `meta[${attrSelector}]`; +} - private _parseSelector(tag: MetaDefinition): string { - const attr: string = tag.name ? 'name' : 'property'; - return `${attr}=${this._escapeSelectorValue(String(tag[attr]))}`; - } +function setMetaElementAttributes(tag: MetaDefinition, el: HTMLMetaElement) { + Object.keys(tag).forEach((prop: string) => el.setAttribute(getMetaKeyMap(prop), tag[prop])); +} - private _escapeSelectorValue(value: string): string { - // Escape backslashes and double quotes to prevent CSS selector injection. - // This securely confines the value inside an attribute selector. - return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; - } +function parseSelector(tag: MetaDefinition): string { + const attr: string = tag.name ? 'name' : 'property'; + return `${attr}=${escapeSelectorValue(String(tag[attr]))}`; +} - private _containsAttributes(tag: MetaDefinition, elem: HTMLMetaElement): boolean { - return Object.keys(tag).every( - (key: string) => elem.getAttribute(this._getMetaKeyMap(key)) === tag[key], - ); - } +function escapeSelectorValue(value: string): string { + // Escape backslashes and double quotes to prevent CSS selector injection. + // This securely confines the value inside an attribute selector. + return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; +} - private _getMetaKeyMap(prop: string): string { - return META_KEYS_MAP[prop] || prop; - } +function containsAttributes(tag: MetaDefinition, elem: HTMLMetaElement): boolean { + return Object.keys(tag).every((key) => elem.getAttribute(getMetaKeyMap(key)) === tag[key]); +} + +function getMetaKeyMap(prop: string): string { + return META_KEYS_MAP[prop] || prop; +} + +function isMetaTag(tag: HTMLElement | null): tag is HTMLMetaElement { + return tag?.nodeName.toLowerCase() === 'meta'; } /** * Mapping for MetaDefinition properties with their correct meta attribute names */ -const META_KEYS_MAP: {[prop: string]: string} = { - httpEquiv: 'http-equiv', -}; +const META_KEYS_MAP: {[prop: string]: string} = {httpEquiv: 'http-equiv'}; diff --git a/packages/platform-browser/test/browser/meta_spec.ts b/packages/platform-browser/test/browser/meta_spec.ts index 377b59300055..314c9eeb99b1 100644 --- a/packages/platform-browser/test/browser/meta_spec.ts +++ b/packages/platform-browser/test/browser/meta_spec.ts @@ -7,7 +7,7 @@ */ import {ɵgetDOM as getDOM} from '@angular/common'; -import {Injectable} from '@angular/core'; +import {DOCUMENT, Injectable} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {expect} from '@angular/private/testing/matchers'; import {BrowserModule, Meta} from '../../index'; @@ -19,7 +19,8 @@ describe('Meta service', () => { beforeEach(() => { doc = getDOM().createHtmlDocument(); - metaService = new Meta(doc); + TestBed.configureTestingModule({providers: [{provide: DOCUMENT, useValue: doc}]}); + metaService = TestBed.inject(Meta); defaultMeta = getDOM().createElement('meta', doc) as HTMLMetaElement; defaultMeta.setAttribute('property', 'fb:app_id'); defaultMeta.setAttribute('content', '123456789'); @@ -261,6 +262,7 @@ describe('Meta service', () => { } beforeEach(() => { + TestBed.resetTestingModule(); TestBed.configureTestingModule({ imports: [BrowserModule], providers: [DependsOnMeta],