Skip to content

Commit 6709d12

Browse files
committed
feat: introduce meta element APIs v2
1 parent 631a943 commit 6709d12

13 files changed

+436
-0
lines changed

projects/ngx-meta/api-extractor/ngx-meta.api.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,12 @@ export type MetadataSetterFactory<T> = (...deps: Exclude<FactoryProvider['deps']
164164
// @public
165165
export type MetadataValues = object;
166166

167+
// @alpha (undocumented)
168+
export const NGX_META_ELEMENT_SETTER: InjectionToken<NgxMetaElementSetter>;
169+
170+
// @alpha (undocumented)
171+
export const NGX_META_ELEMENTS_SETTER: InjectionToken<NgxMetaElementsSetter>;
172+
167173
// @public
168174
export class NgxMetaCoreModule {
169175
// Warning: (ae-forgotten-export) The symbol "CoreFeatures" needs to be exported by the entry point all-entry-points.d.ts
@@ -177,6 +183,27 @@ export interface NgxMetaCoreModuleForRootOptions {
177183
defaults?: MetadataValues;
178184
}
179185

186+
// @alpha
187+
export type NgxMetaElementAttributes = Partial<{
188+
charset: string;
189+
content: string;
190+
'http-equiv': string;
191+
id: string;
192+
itemprop: string;
193+
name: string;
194+
property: string;
195+
scheme: string;
196+
url: string;
197+
}> & {
198+
[key: string]: string;
199+
};
200+
201+
// @alpha (undocumented)
202+
export type NgxMetaElementSetter = (nameAttribute: readonly [name: string, value: string], content: NgxMetaElementAttributes | undefined) => void;
203+
204+
// @alpha (undocumented)
205+
export type NgxMetaElementsSetter = (nameAttribute: readonly [name: string, value: string], contents: ReadonlyArray<NgxMetaElementAttributes | undefined>) => void;
206+
180207
// @public
181208
export class NgxMetaJsonLdModule {
182209
}
@@ -529,12 +556,21 @@ export const _URL_RESOLVER: InjectionToken<_UrlResolver>;
529556
// @internal (undocumented)
530557
export type _UrlResolver = (url: URL | string | undefined | null | AngularRouterUrl) => string | undefined | null;
531558

559+
// @alpha
560+
export const withContentAttribute: (content: string | null | undefined) => NgxMetaElementAttributes | undefined;
561+
562+
// @alpha
563+
export const withNameAttribute: (value: string) => readonly ["name", string];
564+
532565
// @public
533566
export const withNgxMetaBaseUrl: (baseUrl: BaseUrl) => CoreFeature<CoreFeatureKind.BaseUrl>;
534567

535568
// @public
536569
export const withNgxMetaDefaults: (defaults: MetadataValues) => CoreFeature<CoreFeatureKind.Defaults>;
537570

571+
// @alpha
572+
export const withPropertyAttribute: (value: string) => string[];
573+
538574
// (No @packageDocumentation comment for this package)
539575

540576
```

projects/ngx-meta/src/core/src/meta-elements/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export {
99
export { NgxMetaMetaService } from './ngx-meta-meta.service'
1010
export { NgxMetaMetaContent } from './ngx-meta-meta-content'
1111
export { NgxMetaMetaDefinition } from './ngx-meta-meta-definition'
12+
export * from './v2'
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { TestBed } from '@angular/core/testing'
2+
import { Meta, MetaDefinition } from '@angular/platform-browser'
3+
4+
export const addMetaElements = (tags: ReadonlyArray<MetaDefinition>) =>
5+
TestBed.inject(Meta).addTags(tags as MetaDefinition[])
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { TestBed } from '@angular/core/testing'
2+
import { Meta } from '@angular/platform-browser'
3+
4+
export const clearMetasAfterEach = (attributeSelector: string) => {
5+
afterEach(() => {
6+
TestBed.inject(Meta)
7+
.getTags(attributeSelector)
8+
.forEach((tag) => tag.remove())
9+
})
10+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { TestBed } from '@angular/core/testing'
2+
import { Meta } from '@angular/platform-browser'
3+
4+
export const getMetaElementsBySelector = (attrSelector: string) =>
5+
TestBed.inject(Meta).getTags(attrSelector)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const htmlAttributesToJson = (attributes: NamedNodeMap): object =>
2+
[...Array(attributes.length).keys()]
3+
.map((index) => attributes.item(index))
4+
.map((item) => (item ? { [item.name]: item.value } : {}))
5+
.reduce((acc, curr) => ({ ...acc, ...curr }), {})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export {
2+
NGX_META_ELEMENT_SETTER,
3+
NgxMetaElementSetter,
4+
} from './ngx-meta-element-setter'
5+
export {
6+
NGX_META_ELEMENTS_SETTER,
7+
NgxMetaElementsSetter,
8+
} from './ngx-meta-elements-setter'
9+
export { NgxMetaElementAttributes } from './ngx-meta-element-attributes'
10+
export {
11+
withNameAttribute,
12+
withPropertyAttribute,
13+
withContentAttribute,
14+
} from './meta-elements-helpers'
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { NgxMetaElementAttributes } from './ngx-meta-element-attributes'
2+
3+
/**
4+
* Utility function to specify a `<meta name="{name}">` element.
5+
*
6+
* See {@link NgxMetaElementSetter} for examples.
7+
*
8+
* @param value - Value for the `name` attribute of the `<meta>` element
9+
*
10+
* @alpha
11+
*/
12+
export const withNameAttribute = (value: string) => ['name', value] as const
13+
/**
14+
* Utility function to specify a `<meta property="{property}">` element.
15+
*
16+
* See {@link NgxMetaElementSetter} for examples.
17+
*
18+
* @param value - Value for the `property` attribute of the `<meta>` element
19+
*
20+
* @alpha
21+
*/
22+
export const withPropertyAttribute = (value: string) => ['property', value]
23+
24+
/**
25+
* Utility function to create an {@link NgxMetaElementAttributes} specifying the `content` attribute to the
26+
* given `value`. Unless given `value` is `null` or `undefined`. In that case, `undefined` is returned.
27+
*
28+
* See {@link NgxMetaElementSetter} for examples.
29+
*
30+
* @param content - Value for the `property` attribute of the `<meta>` element
31+
*
32+
* @alpha
33+
*/
34+
export const withContentAttribute = (
35+
content: string | null | undefined,
36+
): NgxMetaElementAttributes | undefined => (content ? { content } : undefined)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// noinspection JSValidateJSDoc
2+
/**
3+
* Models a `<meta>` element HTML's attributes as a key / value map.
4+
*
5+
* Inspired on Angular's {@link https://angular.dev/api/platform-browser/MetaDefinition/ | MetaDefinition}
6+
*
7+
* Only difference is `http-equiv` property.
8+
*
9+
* @alpha
10+
*/
11+
export type NgxMetaElementAttributes = Partial<{
12+
charset: string
13+
content: string
14+
/**
15+
* In an Angular's {@link https://angular.dev/api/platform-browser/MetaDefinition/ | MetaDefinition},
16+
* `httpEquiv` would also be accepted. This way no need to quote the key property.
17+
*
18+
* This way there's no need to map attribute names.
19+
*/
20+
'http-equiv': string
21+
id: string
22+
itemprop: string
23+
name: string
24+
property: string
25+
scheme: string
26+
url: string
27+
}> & {
28+
[key: string]: string
29+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { TestBed } from '@angular/core/testing'
2+
import {
3+
NGX_META_ELEMENT_SETTER,
4+
NgxMetaElementSetter,
5+
} from './ngx-meta-element-setter'
6+
import {
7+
withContentAttribute,
8+
withNameAttribute,
9+
} from './meta-elements-helpers'
10+
import { htmlAttributesToJson } from './__tests__/html-attributes-to-json'
11+
import { getMetaElementsBySelector } from './__tests__/get-meta-elements-by-selector'
12+
import { clearMetasAfterEach } from './__tests__/clear-metas-after-each'
13+
import { addMetaElements } from './__tests__/add-meta-elements'
14+
15+
describe('Meta element setter', () => {
16+
const dummyMetaNameAttribute = withNameAttribute('dummy')
17+
const dummyMetaAttributeSelector = 'name="dummy"'
18+
const dummyMetaContentAttribute = withContentAttribute('dummy')
19+
const dummyMetaAttributes = { name: 'dummy', content: 'dummy' }
20+
21+
clearMetasAfterEach(dummyMetaAttributeSelector)
22+
23+
describe('when no element exists yet', () => {
24+
describe('when no content is provided', () => {
25+
it('should not create the meta element', () => {
26+
const sut = makeSut()
27+
28+
sut(dummyMetaNameAttribute, undefined)
29+
30+
const elements = getMetaElementsBySelector(dummyMetaAttributeSelector)
31+
expect(elements).toHaveSize(0)
32+
})
33+
})
34+
35+
describe('when content is provided', () => {
36+
it('should create the meta element', () => {
37+
const sut = makeSut()
38+
39+
sut(dummyMetaNameAttribute, dummyMetaContentAttribute)
40+
41+
const elements = getMetaElementsBySelector(dummyMetaAttributeSelector)
42+
expect(elements).toHaveSize(1)
43+
const element = elements[0]
44+
expect(htmlAttributesToJson(element.attributes)).toEqual(
45+
dummyMetaAttributes,
46+
)
47+
})
48+
})
49+
})
50+
51+
describe('when an element already exists', () => {
52+
let sut: NgxMetaElementSetter
53+
54+
beforeEach(() => {
55+
sut = makeSut()
56+
addMetaElements([dummyMetaAttributes])
57+
expect(getMetaElementsBySelector(dummyMetaAttributeSelector))
58+
.withContext('test setup: element should exist')
59+
.toHaveSize(1)
60+
})
61+
62+
describe('when no content is provided', () => {
63+
it('should remove the element', () => {
64+
sut(dummyMetaNameAttribute, undefined)
65+
66+
expect(
67+
getMetaElementsBySelector(dummyMetaAttributeSelector),
68+
).toHaveSize(0)
69+
})
70+
})
71+
72+
describe('when content is provided', () => {
73+
const anotherContent = 'another-dummy-content'
74+
75+
it('should update the element', () => {
76+
sut(dummyMetaNameAttribute, withContentAttribute(anotherContent))
77+
78+
const elements = getMetaElementsBySelector(dummyMetaAttributeSelector)
79+
expect(elements).toHaveSize(1)
80+
const element = elements[0]
81+
expect(htmlAttributesToJson(element.attributes)).toEqual({
82+
...dummyMetaAttributes,
83+
content: anotherContent,
84+
})
85+
})
86+
})
87+
})
88+
89+
describe('with content attribute utility function', () => {
90+
const sut = withContentAttribute
91+
92+
describe('when no content is provided', () => {
93+
const TEST_CASES = [null, undefined]
94+
95+
TEST_CASES.forEach((testCase) => {
96+
describe(`like when ${testCase}`, () => {
97+
it('should return nothing', () => {
98+
expect(sut(testCase)).toBeUndefined()
99+
})
100+
})
101+
})
102+
})
103+
104+
describe('when content is provided', () => {
105+
const dummyContent = 'dummy'
106+
107+
it('should return the content value inside the content key', () => {
108+
expect(sut(dummyContent)).toEqual({ content: dummyContent })
109+
})
110+
})
111+
})
112+
})
113+
114+
const makeSut = (): NgxMetaElementSetter => {
115+
TestBed.configureTestingModule({})
116+
return TestBed.inject(NGX_META_ELEMENT_SETTER)
117+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { inject, InjectionToken } from '@angular/core'
2+
import { Meta } from '@angular/platform-browser'
3+
import { NgxMetaElementAttributes } from './ngx-meta-element-attributes'
4+
5+
/**
6+
* @alpha
7+
*/
8+
export const NGX_META_ELEMENT_SETTER = new InjectionToken<NgxMetaElementSetter>(
9+
ngDevMode ? 'NgxMeta Meta elements setter' : 'NgxMetaMES',
10+
{
11+
factory: () => {
12+
const meta = inject(Meta)
13+
return (nameAttribute, content) => {
14+
const [nameAttributeName, nameAttributeValue] = nameAttribute
15+
const attrSelector = `${nameAttributeName}="${nameAttributeValue}"`
16+
if (!content) {
17+
meta.removeTag(attrSelector)
18+
return
19+
}
20+
meta.updateTag(
21+
{ [nameAttributeName]: nameAttributeValue, ...content },
22+
attrSelector,
23+
)
24+
}
25+
},
26+
},
27+
)
28+
29+
/**
30+
* @alpha
31+
*/
32+
export type NgxMetaElementSetter = (
33+
nameAttribute: readonly [name: string, value: string],
34+
content: NgxMetaElementAttributes | undefined,
35+
) => void

0 commit comments

Comments
 (0)