-
-
Notifications
You must be signed in to change notification settings - Fork 67
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(io): add support for signal based dynamic components IO
Now you can enable support for signal based components IO if you are using Angular versions that support it.
- Loading branch information
Showing
16 changed files
with
371 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
34 changes: 34 additions & 0 deletions
34
projects/ng-dynamic-component/signal-component-io/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# 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. | ||
|
||
## Warning: Experimental | ||
|
||
This package is still **experimental** and not ready for producation! | ||
APIs may change in the future or be removed completely and never make it to stable release! | ||
Only use it to evaluate the features and provide feedback. | ||
|
||
## Usage | ||
|
||
**Since v10.8.0** | ||
|
||
Import `SignalComponentIoModule` in your application 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! |
5 changes: 5 additions & 0 deletions
5
projects/ng-dynamic-component/signal-component-io/ng-package.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"lib": { | ||
"entryFile": "src/public-api.ts" | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
projects/ng-dynamic-component/signal-component-io/src/lib/signal-component-io.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { NgModule } from '@angular/core'; | ||
import { ComponentIO } from 'ng-dynamic-component'; | ||
import { SignalComponentIO } from './signal-component-io'; | ||
|
||
/** | ||
* @public | ||
* @experimental | ||
*/ | ||
@NgModule({ | ||
providers: [{ provide: ComponentIO, useClass: SignalComponentIO }], | ||
}) | ||
export class SignalComponentIoModule {} |
73 changes: 73 additions & 0 deletions
73
projects/ng-dynamic-component/signal-component-io/src/lib/signal-component-io.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!'); | ||
}); | ||
}); | ||
}); |
45 changes: 45 additions & 0 deletions
45
projects/ng-dynamic-component/signal-component-io/src/lib/signal-component-io.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
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 | ||
* @experimental | ||
*/ | ||
@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' | ||
); | ||
} | ||
} |
1 change: 1 addition & 0 deletions
1
projects/ng-dynamic-component/signal-component-io/src/public-api.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './lib/signal-component-io.module'; |
49 changes: 49 additions & 0 deletions
49
projects/ng-dynamic-component/src/lib/component-io/classic-component-io.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { ComponentRef } from '@angular/core'; | ||
import { of } from 'rxjs'; | ||
import { ClassicComponentIO } from './classic-component-io'; | ||
|
||
class MockComponentRef<C> { | ||
constructor(public instance: C) {} | ||
setInput = jest.fn(); | ||
} | ||
|
||
describe('ClassicComponentIO', () => { | ||
function setup<C>(instance: C = {} as any) { | ||
const componentIO = new ClassicComponentIO(); | ||
const mockComponentRef = new MockComponentRef( | ||
instance, | ||
) as MockComponentRef<C> & ComponentRef<Record<string, unknown>>; | ||
|
||
return { componentIO, mockComponentRef }; | ||
} | ||
|
||
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 throw if output not an observable', () => { | ||
const output = 'not observable'; | ||
const { componentIO, mockComponentRef } = setup({ output }); | ||
|
||
expect(() => | ||
componentIO.getOutput(mockComponentRef, 'output'), | ||
).toThrowError('Component output is not an output!'); | ||
}); | ||
}); | ||
}); |
28 changes: 28 additions & 0 deletions
28
projects/ng-dynamic-component/src/lib/component-io/classic-component-io.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { isObservable, Observable } from 'rxjs'; | ||
import { ComponentInputKey, ComponentIO } from './component-io'; | ||
import { ComponentRef, Injectable } from '@angular/core'; | ||
|
||
/** @internal */ | ||
@Injectable() | ||
export class ClassicComponentIO 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)) { | ||
throw new Error(`Component ${name} is not an output!`); | ||
} | ||
|
||
return output; | ||
} | ||
} |
Oops, something went wrong.