Skip to content

Commit

Permalink
feat: introduce meta element APIs v2 (#883)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidlj95 authored Oct 10, 2024
1 parent 65dd7a3 commit 52a3c34
Show file tree
Hide file tree
Showing 11 changed files with 463 additions and 0 deletions.
39 changes: 39 additions & 0 deletions projects/ngx-meta/api-extractor/ngx-meta.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,33 @@ export interface NgxMetaCoreModuleForRootOptions {
defaults?: MetadataValues;
}

// @alpha
export type NgxMetaElementAttributes = Partial<{
charset: string;
content: string;
'http-equiv': string;
id: string;
itemprop: string;
name: string;
property: string;
scheme: string;
url: string;
media: string;
}> & {
[key: string]: string;
};

// @alpha
export type NgxMetaElementNameAttribute = readonly [name: string, value: string];

// @beta
export class NgxMetaElementsService {
constructor(meta: Meta);
// Warning: (ae-incompatible-release-tags) The symbol "set" is marked as @beta, but its signature references "NgxMetaElementNameAttribute" which is marked as @alpha
// Warning: (ae-incompatible-release-tags) The symbol "set" is marked as @beta, but its signature references "NgxMetaElementAttributes" which is marked as @alpha
set(nameAttribute: NgxMetaElementNameAttribute, content: ReadonlyArray<NgxMetaElementAttributes> | NgxMetaElementAttributes | undefined): void;
}

// @public
export class NgxMetaJsonLdModule {
}
Expand Down Expand Up @@ -554,12 +581,24 @@ export type _UrlResolver = (url: URL | string | undefined | null | AngularRouter
// @internal
export const _urlResolver: _LazyInjectionToken<_UrlResolver>;

// @alpha
export const withContentAttribute: {
(content: null | undefined, extras?: NgxMetaElementAttributes): undefined;
(content: string, extras?: NgxMetaElementAttributes): NgxMetaElementAttributes;
};

// @alpha
export const withNameAttribute: (value: string) => readonly ["name", string];

// @public
export const withNgxMetaBaseUrl: (baseUrl: BaseUrl) => CoreFeature<CoreFeatureKind.BaseUrl>;

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

// @alpha
export const withPropertyAttribute: (value: string) => readonly ["property", string];

// (No @packageDocumentation comment for this package)

```
1 change: 1 addition & 0 deletions projects/ngx-meta/src/core/src/meta-elements/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export {
export { NgxMetaMetaService } from './ngx-meta-meta.service'
export { NgxMetaMetaContent } from './ngx-meta-meta-content'
export { NgxMetaMetaDefinition } from './ngx-meta-meta-definition'
export * from './v2'
6 changes: 6 additions & 0 deletions projects/ngx-meta/src/core/src/meta-elements/v2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { NgxMetaElementsService } from './ngx-meta-elements.service'
export { NgxMetaElementNameAttribute } from './ngx-meta-element-name-attribute'
export { NgxMetaElementAttributes } from './ngx-meta-element-attributes'
export { withNameAttribute } from './with-name-attribute'
export { withPropertyAttribute } from './with-property-attribute'
export { withContentAttribute } from './with-content-attribute'
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Models a `<meta>` element HTML's attributes as a key / value JSON object.
*
* Almost equivalent to Angular's {@link https://angular.dev/api/platform-browser/MetaDefinition/ | MetaDefinition}
*
* Only difference is `http-equiv` property. In an Angular's
* {@link https://angular.dev/api/platform-browser/MetaDefinition/ | MetaDefinition}, `httpEquiv` would also be
* accepted. This way there's no need to quote the key property.
* But without `httpEquiv` there's no need to map attribute names. So one bit of code less.
*
* @alpha
*/
export type NgxMetaElementAttributes = Partial<{
charset: string
content: string
'http-equiv': string
id: string
itemprop: string
name: string
property: string
scheme: string
url: string
media: string
}> & {
[key: string]: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* See {@link NgxMetaElementsService.set}
*
* @alpha
*/
export type NgxMetaElementNameAttribute = readonly [name: string, value: string]
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { TestBed } from '@angular/core/testing'
import { NgxMetaElementsService } from './ngx-meta-elements.service'
import { withNameAttribute } from './with-name-attribute'
import { withContentAttribute } from './with-content-attribute'
import { Meta, MetaDefinition } from '@angular/platform-browser'

describe('Meta element service', () => {
const dummyMetaNameAttribute = withNameAttribute('dummy')
const dummyMetaNameAttributeKeyValue = { name: 'dummy' }
const dummyMetaContentAttribute = withContentAttribute('dummy')
const dummyMetaAttributes = {
...dummyMetaNameAttributeKeyValue,
content: 'dummy',
}

const anotherDummyMetaContentAttribute = withContentAttribute('another-dummy')
const anotherDummyMetaAttributes = {
...dummyMetaNameAttributeKeyValue,
content: 'another-dummy',
}

const yetAnotherDummyMetaContentAttribute =
withContentAttribute('yet-another-dummy')
const yetAnotherDummyMetaAttributes = {
...dummyMetaNameAttributeKeyValue,
content: 'yet-another-dummy',
}

const getDummyMetaElements = () =>
TestBed.inject(Meta).getTags('name="dummy"')

afterEach(() => {
getDummyMetaElements().forEach((element) => element.remove())
})

describe('when no elements exist', () => {
describe('when no contents are provided', () => {
const TEST_CASES = [
[[], 'empty array'],
[undefined, 'undefined'],
] as const

TEST_CASES.forEach(([testCase, testCaseName]) => {
describe(`like when ${testCaseName}`, () => {
it('should not create any element', () => {
const sut = makeSut()

sut.set(dummyMetaNameAttribute, testCase)

expect(getDummyMetaElements()).toHaveSize(0)
})
})
})
})

describe('when contents are provided', () => {
describe('when a single content is provided', () => {
it('should create the element', () => {
const sut = makeSut()
sut.set(dummyMetaNameAttribute, dummyMetaContentAttribute)

const elements = getDummyMetaElements()
expect(elements.length).toBe(1)
const element = elements[0]
expect(htmlAttributesToJson(element.attributes)).toEqual(
dummyMetaAttributes,
)
})
})

describe('when multiple contents are provided', () => {
it('should create an element for each one', () => {
const sut = makeSut()
sut.set(dummyMetaNameAttribute, [
dummyMetaContentAttribute,
anotherDummyMetaContentAttribute,
])

const elements = getDummyMetaElements()
expect(
elements.map((e) => e.attributes).map(htmlAttributesToJson),
).toEqual([dummyMetaAttributes, anotherDummyMetaAttributes])
})
})
})
})

describe('when elements exist', () => {
let sut: NgxMetaElementsService

beforeEach(() => {
sut = makeSut()

const dummyNameAttribute = {
[dummyMetaNameAttribute[0]]: dummyMetaNameAttribute[1],
}
TestBed.inject(Meta).addTags([
{
...dummyNameAttribute,
content: 'existing-content-1',
},
{
...dummyNameAttribute,
content: 'existing-content-2',
},
] as MetaDefinition[])
expect(getDummyMetaElements())
.withContext('test setup: two elements should exist')
.toHaveSize(2)
})

describe('when no contents are provided', () => {
const TEST_CASES = [
[[], 'empty array'],
[undefined, 'undefined'],
] as const

TEST_CASES.forEach(([testCase, testCaseName]) => {
describe(`like when ${testCaseName}`, () => {
it('should remove them all', () => {
sut.set(dummyMetaNameAttribute, testCase)

expect(getDummyMetaElements()).toHaveSize(0)
})
})
})
})

describe('when contents are provided', () => {
describe('when a single content is provided', () => {
it('should remove existing elements and create the new one', () => {
sut.set(dummyMetaNameAttribute, dummyMetaContentAttribute)

const elements = getDummyMetaElements()
expect(elements.length).toBe(1)
const element = elements[0]
expect(htmlAttributesToJson(element.attributes)).toEqual(
dummyMetaAttributes,
)
})
})

describe('when multiple contents are provided', () => {
it('should remove existing elements and create new ones', () => {
sut.set(dummyMetaNameAttribute, [
dummyMetaContentAttribute,
anotherDummyMetaContentAttribute,
yetAnotherDummyMetaContentAttribute,
])

expect(
getDummyMetaElements()
.map((e) => e.attributes)
.map(htmlAttributesToJson),
).toEqual([
dummyMetaAttributes,
anotherDummyMetaAttributes,
yetAnotherDummyMetaAttributes,
])
})
})
})
})
})

const makeSut = () => TestBed.inject(NgxMetaElementsService)

export const htmlAttributesToJson = (attributes: NamedNodeMap): object =>
[...Array(attributes.length).keys()]
.map((index) => attributes.item(index))
.map((item) => (item ? { [item.name]: item.value } : {}))
.reduce((acc, curr) => ({ ...acc, ...curr }), {})
Loading

0 comments on commit 52a3c34

Please sign in to comment.