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

chore: stabilize experimental API #521

Merged
merged 4 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# ng-dynamic-component - Changelog

# [10.8.0-next.1](https://github.com/gund/ng-dynamic-component/compare/v10.7.0...v10.8.0-next.1) (2024-12-17)


### Features

* **io:** add support for signal based dynamic components IO ([#520](https://github.com/gund/ng-dynamic-component/issues/520)) ([a8f62d5](https://github.com/gund/ng-dynamic-component/commit/a8f62d5e6123cf5ee2c6867d04a109bc57e120b1))

# [10.7.0](https://github.com/gund/ng-dynamic-component/compare/v10.6.2...v10.7.0) (2023-03-15)


Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ will not work and you will have to import them separately (see their respective
If you still need to use both `<ndc-dynamic>` and dynamic inputs/outputs it is recommended
to keep using `DynamicModule` API.

### Singal based inputs/outputs

**Since v10.8.0**

If you want to dynamically render signal based components - see [`signal-component-io`](projects/ng-dynamic-component/signal-component-io/README.md) package.

### NgComponentOutlet

You can also use [`NgComponentOutlet`](https://angular.io/api/common/NgComponentOutlet)
Expand Down
21 changes: 21 additions & 0 deletions goldens/ng-dynamic-component/api-signal-component-io.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## API Report File for "signal-component-io"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).

```ts

import * as i0 from '@angular/core';

// @public
export class SignalComponentIoModule {
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<SignalComponentIoModule, never>;
// (undocumented)
static ɵinj: i0.ɵɵInjectorDeclaration<SignalComponentIoModule>;
// (undocumented)
static ɵmod: i0.ɵɵNgModuleDeclaration<SignalComponentIoModule, never, never, never>;
}

// (No @packageDocumentation comment for this package)

```
38 changes: 27 additions & 11 deletions goldens/ng-dynamic-component/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { IterableDiffers } from '@angular/core';
import { KeyValueDiffers } from '@angular/core';
import { NgComponentOutlet } from '@angular/common';
import { NgModuleRef } from '@angular/core';
import { Observable } from 'rxjs';
import { OnChanges } from '@angular/core';
import { OnDestroy } from '@angular/core';
import { Renderer2 } from '@angular/core';
Expand All @@ -36,6 +37,21 @@ export interface AttributesMap {
[key: string]: string;
}

// @public (undocumented)
export type ComponentInputKey<T> = keyof T & string;

// @public (undocumented)
export abstract class ComponentIO {
// (undocumented)
abstract getOutput<T, K extends ComponentInputKey<T>>(componentRef: ComponentRef<T>, name: K): Observable<unknown>;
// (undocumented)
abstract setInput<T, K extends ComponentInputKey<T>>(componentRef: ComponentRef<T>, name: K, value: T[K]): void;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<ComponentIO, never>;
// (undocumented)
static ɵprov: i0.ɵɵInjectableDeclaration<ComponentIO>;
}

// @public (undocumented)
export class ComponentOutletInjectorDirective implements DynamicComponentInjector {
constructor(componentOutlet: NgComponentOutlet);
Expand Down Expand Up @@ -123,11 +139,11 @@ export class DynamicAttributesModule {
static ɵfac: i0.ɵɵFactoryDeclaration<DynamicAttributesModule, never>;
// (undocumented)
static ɵinj: i0.ɵɵInjectorDeclaration<DynamicAttributesModule>;
// Warning: (ae-forgotten-export) The symbol "i1_4" needs to be exported by the entry point public-api.d.ts
// Warning: (ae-forgotten-export) The symbol "i1_2" needs to be exported by the entry point public-api.d.ts
// Warning: (ae-forgotten-export) The symbol "i2_2" needs to be exported by the entry point public-api.d.ts
//
// (undocumented)
static ɵmod: i0.ɵɵNgModuleDeclaration<DynamicAttributesModule, never, [typeof i1_4.DynamicAttributesDirective], [typeof i1_4.DynamicAttributesDirective, typeof i2_2.ComponentOutletInjectorModule]>;
static ɵmod: i0.ɵɵNgModuleDeclaration<DynamicAttributesModule, never, [typeof i1_2.DynamicAttributesDirective], [typeof i1_2.DynamicAttributesDirective, typeof i2_2.ComponentOutletInjectorModule]>;
}

// @public (undocumented)
Expand Down Expand Up @@ -206,10 +222,10 @@ export class DynamicDirectivesModule {
static ɵfac: i0.ɵɵFactoryDeclaration<DynamicDirectivesModule, never>;
// (undocumented)
static ɵinj: i0.ɵɵInjectorDeclaration<DynamicDirectivesModule>;
// Warning: (ae-forgotten-export) The symbol "i1_5" needs to be exported by the entry point public-api.d.ts
// Warning: (ae-forgotten-export) The symbol "i1_3" needs to be exported by the entry point public-api.d.ts
//
// (undocumented)
static ɵmod: i0.ɵɵNgModuleDeclaration<DynamicDirectivesModule, never, [typeof i1_5.DynamicDirectivesDirective], [typeof i1_5.DynamicDirectivesDirective, typeof i2_2.ComponentOutletInjectorModule]>;
static ɵmod: i0.ɵɵNgModuleDeclaration<DynamicDirectivesModule, never, [typeof i1_3.DynamicDirectivesDirective], [typeof i1_3.DynamicDirectivesDirective, typeof i2_2.ComponentOutletInjectorModule]>;
}

// @public (undocumented)
Expand All @@ -233,10 +249,10 @@ export class DynamicIoModule {
static ɵfac: i0.ɵɵFactoryDeclaration<DynamicIoModule, never>;
// (undocumented)
static ɵinj: i0.ɵɵInjectorDeclaration<DynamicIoModule>;
// Warning: (ae-forgotten-export) The symbol "i1_3" needs to be exported by the entry point public-api.d.ts
// Warning: (ae-forgotten-export) The symbol "i1_4" needs to be exported by the entry point public-api.d.ts
//
// (undocumented)
static ɵmod: i0.ɵɵNgModuleDeclaration<DynamicIoModule, never, [typeof i1_3.DynamicIoDirective], [typeof i1_3.DynamicIoDirective, typeof i2_2.ComponentOutletInjectorModule]>;
static ɵmod: i0.ɵɵNgModuleDeclaration<DynamicIoModule, never, [typeof i1_4.DynamicIoDirective], [typeof i1_4.DynamicIoDirective, typeof i2_2.ComponentOutletInjectorModule]>;
}

// @public (undocumented)
Expand All @@ -245,11 +261,11 @@ export class DynamicModule {
static ɵfac: i0.ɵɵFactoryDeclaration<DynamicModule, never>;
// (undocumented)
static ɵinj: i0.ɵɵInjectorDeclaration<DynamicModule>;
// Warning: (ae-forgotten-export) The symbol "i1_2" needs to be exported by the entry point public-api.d.ts
// Warning: (ae-forgotten-export) The symbol "i1_5" needs to be exported by the entry point public-api.d.ts
// Warning: (ae-forgotten-export) The symbol "i2_3" needs to be exported by the entry point public-api.d.ts
//
// (undocumented)
static ɵmod: i0.ɵɵNgModuleDeclaration<DynamicModule, never, [typeof i1_2.DynamicIoModule, typeof i2_3.DynamicComponent], [typeof i1_2.DynamicIoModule, typeof i2_3.DynamicComponent]>;
static ɵmod: i0.ɵɵNgModuleDeclaration<DynamicModule, never, [typeof i1_5.DynamicIoModule, typeof i2_3.DynamicComponent], [typeof i1_5.DynamicIoModule, typeof i2_3.DynamicComponent]>;
}

// @public @deprecated (undocumented)
Expand Down Expand Up @@ -292,12 +308,12 @@ export interface IoFactoryServiceOptions {

// @public (undocumented)
export class IoService implements OnDestroy {
constructor(injector: Injector, differs: KeyValueDiffers, cfr: ComponentFactoryResolver, options: IoServiceOptions, compInjector: DynamicComponentInjector, eventArgument: string, cdr: ChangeDetectorRef, eventContextProvider: StaticProvider);
// (undocumented)
constructor(injector: Injector, differs: KeyValueDiffers, cfr: ComponentFactoryResolver, options: IoServiceOptions, compInjector: DynamicComponentInjector, eventArgument: string, cdr: ChangeDetectorRef, eventContextProvider: StaticProvider, componentIO: ComponentIO);
// @internal (undocumented)
ngOnDestroy(): void;
update(inputs?: InputsType | null, outputs?: OutputsType | null): void;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<IoService, [null, null, null, null, null, null, null, { optional: true; }]>;
static ɵfac: i0.ɵɵFactoryDeclaration<IoService, [null, null, null, null, null, null, null, { optional: true; }, null]>;
// (undocumented)
static ɵprov: i0.ɵɵInjectableDeclaration<IoService>;
}
Expand Down
23 changes: 19 additions & 4 deletions projects/ng-dynamic-component/project.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"name": "ng-dynamic-component",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "projects/ng-dynamic-component/src",
Expand All @@ -15,25 +16,39 @@
"executor": "nx:run-commands",
"outputs": ["goldens/ng-dynamic-component", "dist/ng-dynamic-component"],
"options": {
"command": "npx api-extractor run -c projects/ng-dynamic-component/api-extractor.json"
"commands": [
"npx api-extractor run -c projects/ng-dynamic-component/api-extractor.json",
"npx api-extractor run -c projects/ng-dynamic-component/signal-component-io/api-extractor.json"
],
"parallel": true
},
"configurations": {
"local": {
"command": "npx api-extractor run -c projects/ng-dynamic-component/api-extractor.json --local"
"commands": [
"npx api-extractor run -c projects/ng-dynamic-component/api-extractor.json --local",
"npx api-extractor run -c projects/ng-dynamic-component/signal-component-io/api-extractor.json --local"
]
}
},
"dependsOn": ["build-lib", "^build-lib"]
},
"test": {
"executor": "@angular-builders/jest:run",
"options": {}
"options": {},
"configurations": {
"watch": {
"watch": true
}
}
},
"lint": {
"executor": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"projects/ng-dynamic-component/**/*.ts",
"projects/ng-dynamic-component/**/*.html"
"projects/ng-dynamic-component/**/*.html",
"projects/ng-dynamic-component/signal-component-io/**/*.ts",
"projects/ng-dynamic-component/signal-component-io/**/*.html"
]
}
}
Expand Down
28 changes: 28 additions & 0 deletions projects/ng-dynamic-component/signal-component-io/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# ng-dynamic-component/signal-component-io

> Secondary entry point of `ng-dynamic-component`. It can be used by importing from `ng-dynamic-component/signal-component-io`.

This package enables signal based inputs/outputs support for dynamically rendered components.

## Prerequisites

This package requires Angular version which supports signals.
Please refer to (Angular docs)[https://angular.dev/] to see which minimal version is required.

## Usage

**Since v10.8.0**

Import `SignalComponentIoModule` in your application root module or config:

```ts
import { NgModule } from '@angular/core';
import { SignalComponentIoModule } from 'ng-dynamic-component/signal-component-io';

@NgModule({
imports: [SignalComponentIoModule],
})
class AppModule {}
```

Now you can render dynamic components with signal based inputs/outputs!
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"extends": "../api-extractor.json",
"projectFolder": "../../..",
"mainEntryPointFilePath": "<projectFolder>/dist/ng-dynamic-component/signal-component-io/public-api.d.ts",
"apiReport": {
"enabled": true,
"reportFileName": "api-signal-component-io.md"
},
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "",
"alphaTrimmedFilePath": "<projectFolder>/dist/ng-dynamic-component/signal-component-io/public-api-alpha.d.ts",
"betaTrimmedFilePath": "<projectFolder>/dist/ng-dynamic-component/signal-component-io/public-api-beta.d.ts",
"publicTrimmedFilePath": "<projectFolder>/dist/ng-dynamic-component/signal-component-io/public-api.d.ts",
"omitTrimmingComments": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "src/public-api.ts"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "signal-component-io"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { ComponentIO } from 'ng-dynamic-component';
import { SignalComponentIO } from './signal-component-io';

/**
* Enables signal based inputs/outputs support for dynamically rendered components.
* Import once at the root of your application.
* @public
*/
@NgModule({
providers: [{ provide: ComponentIO, useClass: SignalComponentIO }],
})
export class SignalComponentIoModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ComponentRef } from '@angular/core';
// @ts-ignore
import { outputToObservable } from '@angular/core/rxjs-interop';
import { SignalComponentIO } from './signal-component-io';
import { of } from 'rxjs';

jest.mock(
'@angular/core/rxjs-interop',
() => ({ outputToObservable: jest.fn() }),
{ virtual: true },
);

class MockComponentRef<C> {
constructor(public instance: C) {}
setInput = jest.fn();
}

describe('SignalComponentIO', () => {
function setup<C>(instance: C = {} as any) {
const componentIO = new SignalComponentIO();
const mockComponentRef = new MockComponentRef(
instance,
) as MockComponentRef<C> & ComponentRef<Record<string, unknown>>;
const mockOutputToObservable = outputToObservable as jest.Mock;

return { componentIO, mockComponentRef, mockOutputToObservable };
}

describe('setInput()', () => {
it('should call ComponentRef.setInput()', () => {
const { componentIO, mockComponentRef } = setup();

componentIO.setInput(mockComponentRef, 'prop', 'value');

expect(mockComponentRef.setInput).toHaveBeenCalledWith('prop', 'value');
});
});

describe('getOutput()', () => {
it('should return observable output as is', () => {
const output = of('event');
const { componentIO, mockComponentRef } = setup({ output });

componentIO.getOutput(mockComponentRef, 'output');

expect(componentIO.getOutput(mockComponentRef, 'output')).toBe(output);
});

it('should convert signal output to observalbe', () => {
const signal = { subscribe: jest.fn() };
const observable = of('signal');
const { componentIO, mockComponentRef, mockOutputToObservable } = setup({
signal,
});

mockOutputToObservable.mockReturnValue(observable);

expect(componentIO.getOutput(mockComponentRef, 'signal')).toBe(
observable,
);
expect(mockOutputToObservable).toHaveBeenCalledWith(signal);
});

it('should throw if output not an observable/signal', () => {
const output = 'not observable/signal';
const { componentIO, mockComponentRef } = setup({ output });

expect(() =>
componentIO.getOutput(mockComponentRef, 'output'),
).toThrowError('Component output is not an output!');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ComponentRef, Injectable } from '@angular/core';
// @ts-ignore
import { outputToObservable } from '@angular/core/rxjs-interop';
import { ComponentIO, ComponentInputKey } from 'ng-dynamic-component';
import { Observable, isObservable } from 'rxjs';

/** @internal */
@Injectable()
export class SignalComponentIO implements ComponentIO {
setInput<T, K extends ComponentInputKey<T>>(
componentRef: ComponentRef<T>,
name: K,
value: T[K],
): void {
componentRef.setInput(name, value);
}

getOutput<T, K extends ComponentInputKey<T>>(
componentRef: ComponentRef<T>,
name: K,
): Observable<unknown> {
const output = componentRef.instance[name];

if (isObservable(output)) {
return output;
}

if (this.isOutputSignal(output)) {
return outputToObservable(output);
}

throw new Error(`Component ${name} is not an output!`);
}

private isOutputSignal(value: unknown): boolean {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as any)['subscribe'] === 'function'
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib/signal-component-io.module';
Loading
Loading