Skip to content
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

feat: introduce meta element APIs v2 #883

Merged
merged 13 commits into from
Oct 10, 2024
Merged
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