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: add metadata manager provider APIs v2 #926

Merged
merged 13 commits into from
Oct 14, 2024
Merged
34 changes: 34 additions & 0 deletions projects/ngx-meta/api-extractor/ngx-meta.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,19 @@ export const provideNgxMetaCore: (...features: CoreFeatures) => EnvironmentProvi
// @public
export const provideNgxMetaJsonLd: () => Provider[];

// Warning: (ae-incompatible-release-tags) The symbol "provideNgxMetaManager" is marked as @alpha, but its signature references "_ProvideNgxMetaManagerOptions" which is marked as @internal
//
// @alpha
export const provideNgxMetaManager: <T>(jsonPath: string, setterFactory: MetadataSetterFactory<T>, options?: _ProvideNgxMetaManagerOptions) => FactoryProvider;

// @internal (undocumented)
export type _ProvideNgxMetaManagerOptions = Partial<{
d: FactoryProvider['deps'];
g: MetadataResolverOptions['global'];
i: NgxMetaMetadataManager['id'];
o: MetadataResolverOptions['objectMerge'];
}>;

// @public
export const provideNgxMetaMetadataLoader: () => Provider[];

Expand Down Expand Up @@ -589,6 +602,24 @@ export const withContentAttribute: {
(content: string | null | undefined, extras?: NgxMetaElementAttributes): NgxMetaElementAttributes | undefined;
};

// Warning: (ae-incompatible-release-tags) The symbol "withManagerDeps" is marked as @alpha, but its signature references "_ProvideNgxMetaManagerOptions" which is marked as @internal
//
// @alpha
export const withManagerDeps: (...deps: Exclude<FactoryProvider['deps'], undefined>) => _ProvideNgxMetaManagerOptions;

// Warning: (ae-incompatible-release-tags) The symbol "withManagerGlobal" is marked as @alpha, but its signature references "_ProvideNgxMetaManagerOptions" which is marked as @internal
//
// @alpha
export const withManagerGlobal: (global: string) => _ProvideNgxMetaManagerOptions;

// @alpha
export const withManagerJsonPath: (...jsonPath: MetadataResolverOptions['jsonPath']) => string;

// Warning: (ae-incompatible-release-tags) The symbol "withManagerObjectMerging" is marked as @alpha, but its signature references "_ProvideNgxMetaManagerOptions" which is marked as @internal
//
// @alpha
export const withManagerObjectMerging: () => _ProvideNgxMetaManagerOptions;

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

Expand All @@ -598,6 +629,9 @@ export const withNgxMetaBaseUrl: (baseUrl: BaseUrl) => CoreFeature<CoreFeatureKi
// @public
export const withNgxMetaDefaults: (defaults: MetadataValues) => CoreFeature<CoreFeatureKind.Defaults>;

// @alpha
export const withOptions: <T extends object>(...options: ReadonlyArray<T>) => T;

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

Expand Down
6 changes: 1 addition & 5 deletions projects/ngx-meta/src/core/src/managers/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
export {
makeMetadataManagerProviderFromSetterFactory,
MetadataSetterFactory,
MakeMetadataManagerProviderFromSetterFactoryOptions,
} from './make-metadata-manager-provider-from-setter-factory'
export {
_injectMetadataManagers,
NgxMetaMetadataManager,
MetadataSetter,
MetadataResolverOptions,
} from './ngx-meta-metadata-manager'
export * from './provider'
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import { inject } from '@angular/core'
*
* See also:
*
* - {@link https://ngx-meta.dev/guides/manage-your-custom-metadata/ | Manage your custom metadata}
* - {@link https://ngx-meta.dev/guides/manage-your-custom-metadata/ | Manage your custom metadata guide}
*
* - {@link makeMetadataManagerProviderFromSetterFactory} for a helper to create a metadata manager
*
* - {@link provideNgxMetaManager} for an experimental helper to create a metadata manager
*
* @typeParam Value - Value type that can be handled by the setter
*
* @public
Expand Down
13 changes: 13 additions & 0 deletions projects/ngx-meta/src/core/src/managers/provider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export {
provideNgxMetaManager,
withManagerDeps,
withManagerGlobal,
withManagerObjectMerging,
withManagerJsonPath,
_ProvideNgxMetaManagerOptions,
} from './provide-ngx-meta-manager'
export {
makeMetadataManagerProviderFromSetterFactory,
MetadataSetterFactory,
MakeMetadataManagerProviderFromSetterFactoryOptions,
} from './make-metadata-manager-provider-from-setter-factory'
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
MetadataResolverOptions,
MetadataSetter,
NgxMetaMetadataManager,
} from './ngx-meta-metadata-manager'
} from '../ngx-meta-metadata-manager'
import { FactoryProvider } from '@angular/core'

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { injectOneMetadataManager } from '@/ngx-meta/test/inject-one-metadata-manager'
import { FactoryProvider, InjectionToken, Provider } from '@angular/core'
import { TestBed } from '@angular/core/testing'
import { DOCUMENT } from '@angular/common'
import { Router } from '@angular/router'
import { MetadataSetterFactory } from './make-metadata-manager-provider-from-setter-factory'
import { _isDefined, withOptions } from '../../utils'
import {
provideNgxMetaManager,
withManagerDeps,
withManagerGlobal,
withManagerObjectMerging,
} from './provide-ngx-meta-manager'

describe('provide manager', () => {
const jsonPathAsArray = ['dummy-scope', 'dummy-key']
const jsonPath = 'dummy-scope.dummy-key'

it('should provide a manager with the given JSON path', () => {
const provider = makeSut({ jsonPath })

const manager = provideAndInject(provider)

expect(manager.resolverOptions.jsonPath).toEqual(jsonPathAsArray)
})

it('should provide a manager with JSON path as id', () => {
const provider = makeSut({ jsonPath })

const manager = provideAndInject(provider)

expect(manager.id).toEqual(jsonPath)
})

it('should provide a manager with the given factory', () => {
const setter = () => {}
const provider = makeSut({ factory: () => setter })

const manager = provideAndInject(provider)

expect(manager.set).toEqual(setter)
})

describe('when dependencies are given', () => {
const deps = [DOCUMENT, Router] satisfies FactoryProvider['deps']

it('should pass them to the factory function', () => {
const factory = jasmine
.createSpy<MetadataSetterFactory<unknown>>()
.and.returnValue(() => {})
const provider = makeSut({ deps, factory })

provideAndInject(provider)

expect(factory).toHaveBeenCalledWith(
...deps.map((dep) => TestBed.inject(dep as InjectionToken<unknown>)),
)
})
})

describe('when global is given', () => {
const global = 'global'

it('should set it in the manager', () => {
const provider = makeSut({ global })

const manager = provideAndInject(provider)

expect(manager.resolverOptions.global).toEqual(global)
})
})

describe('when object merging is enabled', () => {
const objectMerge = true

it('should set it in the manager', () => {
const provider = makeSut({ objectMerge })

const manager = provideAndInject(provider)

expect(manager.resolverOptions.objectMerge).toBeTrue()
})
})
})

const makeSut = (
opts: {
jsonPath?: string
factory?: MetadataSetterFactory<unknown>
deps?: FactoryProvider['deps']
global?: string
objectMerge?: true
} = {},
) =>
provideNgxMetaManager(
opts.jsonPath ?? 'dummy-scope.dummy-key',
opts.factory ?? (() => () => {}),
withOptions(
...[
opts.deps ? withManagerDeps(...opts.deps) : undefined,
opts.global ? withManagerGlobal(opts.global) : undefined,
opts.objectMerge ? withManagerObjectMerging() : undefined,
].filter(_isDefined),
),
)

const provideAndInject = (provider: Provider) => {
TestBed.configureTestingModule({ providers: [provider] })
return injectOneMetadataManager()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { MetadataSetterFactory } from './make-metadata-manager-provider-from-setter-factory'
import { FactoryProvider } from '@angular/core'
import {
MetadataResolverOptions,
NgxMetaMetadataManager,
} from '../ngx-meta-metadata-manager'

/**
* Creates an {@link NgxMetaMetadataManager} provider to manage some metadata.
*
* Check out {@link https://ngx-meta.dev/guides/manage-your-custom-metadata/ | manage your custom metadata guide} to
* learn how to provide your custom metadata managers.
*
* @remarks
*
* Options can be specified using helper functions. {@link withOptions} can be used to combine more than one.
*
* Available option functions:
*
* - {@link withManagerDeps}
*
* - {@link withManagerGlobal}
*
* - {@link withManagerObjectMerging}
*
* @example
*
* ```typescript
* const CUSTOM_TITLE_PROVIDER = provideNgxMetaManager<string | undefined>(
* 'custom.title',
* (metaElementsService: NgxMetaElementsService) => (value) => {
* metaElementsService.set(
* withNameAttribute('custom:title'),
* withContentAttribute(value),
* )
* },
* withOptions(
* withManagerDeps(NgxMetaElementsService),
* withGlobal('title'),
* ),
* )
* ```
*
* @param jsonPath - Path to access the metadata value this manager needs given a JSON object
* containing metadata values. Path is expressed as the keys to use to access the value
* joined by a "." character.
* You can use {@link withManagerJsonPath} to provide an array of keys instead.
* For more information, checkout {@link MetadataResolverOptions.jsonPath}
* @param setterFactory - Factory function that creates the {@link MetadataSetter} function for the manager (which
* manages the metadata element on the page).
* You can inject dependencies either using {@link withManagerDeps} option, that will be passed
* as arguments to the setter factory function. This way is preferred, as takes fewer bytes of
* your bundle size. However, type safety depends on you.
* Or use {@link https://angular.dev/api/core/inject | Angular's `inject` function} for a more
* type-safe option.
* @param options - Extra options for the metadata manager provider creation. Use one of the helpers listed in this
* method's reference docs to supply one or more of them.
* @alpha
*/
export const provideNgxMetaManager = <T>(
jsonPath: string,
setterFactory: MetadataSetterFactory<T>,
/* istanbul ignore next - quite simple */
options: _ProvideNgxMetaManagerOptions = {},
): FactoryProvider => ({
provide: NgxMetaMetadataManager,
multi: true,
useFactory: (...deps: ReadonlyArray<unknown>) =>
({
id: jsonPath,
set: setterFactory(...deps),
resolverOptions: {
jsonPath: jsonPath.split('.'),
global: options.g,
objectMerge: options.o,
},
}) satisfies NgxMetaMetadataManager<T>,
deps: options.d,
})

/**
* @internal
*/
export type _ProvideNgxMetaManagerOptions = Partial<{
d: FactoryProvider['deps']
g: MetadataResolverOptions['global']
i: NgxMetaMetadataManager['id']
o: MetadataResolverOptions['objectMerge']
}>

/**
* Specifies dependencies to inject to the setter factory function passed to {@link provideNgxMetaManager}
*
* See also:
*
* - {@link https://angular.dev/guide/di/dependency-injection-providers#factory-providers-usefactory:~:text=property%20is%20an%20array%20of%20provider%20tokens | Factory providers' deps}
*
* - {@link https://angular.dev/api/core/FactoryProvider#deps | FactoryProvider#deps}
*
* @param deps - Dependencies to inject. Each argument declares the dependency to inject.
*
* @alpha
*/
export const withManagerDeps = (
...deps: Exclude<FactoryProvider['deps'], undefined>
): _ProvideNgxMetaManagerOptions => ({
d: deps,
})

/**
* Sets the global key to use for a metadata manager created with {@link provideNgxMetaManager}
*
* @param global - See {@link MetadataResolverOptions.global}
*
* @alpha
*/
export const withManagerGlobal = (
global: string,
): _ProvideNgxMetaManagerOptions => ({ g: global })

/**
* Enables object merging for the manager being created with {@link provideNgxMetaManager}
*
* See {@link MetadataResolverOptions.objectMerge} for more information.
*
* @alpha
*/
export const withManagerObjectMerging = (): _ProvideNgxMetaManagerOptions => ({
o: true,
})

/**
* Transforms a JSON Path specified as an array of keys into a string joined by dots.
*
* Useful to use with {@link provideNgxMetaManager} to avoid repeating same keys around.
*
* @param jsonPath - Parts of the JSON Path to join into a string.
*
* @alpha
*/
/* istanbul ignore next - quite simple */
export const withManagerJsonPath = (
...jsonPath: MetadataResolverOptions['jsonPath']
): string => jsonPath.join('.')
1 change: 1 addition & 0 deletions projects/ngx-meta/src/core/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { _isDefined } from './is-defined'
export { _LazyInjectionToken } from './lazy-injection-token'
export { _makeInjectionToken } from './make-injection-token'
export { withOptions } from './with-options'
17 changes: 17 additions & 0 deletions projects/ngx-meta/src/core/src/utils/with-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Helper function to combine multiple options (objects).
*
* In case of specifying same options more than once, the latter one will take precedence.
* Provide them sorted by ascendant priority. Less priority options first. Top priority options last.
*
* Can be used to combine options for:
*
* - {@link provideNgxMetaManager}
*
* @param options - Options to combine.
*
* @alpha
*/
export const withOptions = <T extends object>(
...options: ReadonlyArray<T>
): T => options.reduce<T>((acc, curr) => ({ ...acc, ...curr }), {} as T)