-
Notifications
You must be signed in to change notification settings - Fork 0
#83 new utilities #84
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
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
81dc82b
feat: new utility `onInit` function to replace ngOnInit
mumenthalers c661200
feat(logger): enhance `withCustomLogTransport` to support `useExistin…
mumenthalers 2dace6a
build(release): next version [skip_build]
actions-user aae401f
feat: add `InputsOf` utility type to extract component inputs
mumenthalers 899bdca
feat: add `ApplyPipe`
mumenthalers bf2232b
docs: enhance JSDoc comments and tests
mumenthalers 12106e7
build(release): next version [skip_build]
actions-user File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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,65 @@ | ||
| import { ChangeDetectionStrategy, Component, signal } from '@angular/core' | ||
| import { TestBed } from '@angular/core/testing' | ||
| import { describe, expect, it, vi } from 'vitest' | ||
|
|
||
| import { ApplyPipe } from './apply.pipe' | ||
|
|
||
| describe('ApplyPipe', () => { | ||
| it('calls the provided function with the provided value', () => { | ||
| const pipe = new ApplyPipe() | ||
|
|
||
| const input = 'hello' | ||
| const toUpperCaseFn = vi.fn((arg: string) => arg.toUpperCase()) | ||
|
|
||
| pipe.transform(input, toUpperCaseFn) | ||
| expect(toUpperCaseFn).toHaveBeenCalledWith(input) | ||
| }) | ||
|
|
||
| it('should return the result of the function call', () => { | ||
| const pipe = new ApplyPipe() | ||
|
|
||
| const input = 42 | ||
| const doubleFn = (arg: number) => arg * 2 | ||
| const expected = 84 | ||
|
|
||
| const result = pipe.transform(input, doubleFn) | ||
| expect(result).toEqual(expected) | ||
| }) | ||
|
|
||
| it('works integrated', () => { | ||
| const squareFn = vi.fn((value: number) => value * value) | ||
| const doubleFn = vi.fn((value: number) => 2 * value) | ||
|
|
||
| @Component({ | ||
| selector: 'sc-test-component', | ||
| template: '{{prefix()}}{{ value() | apply: fn() }}', | ||
| changeDetection: ChangeDetectionStrategy.OnPush, | ||
| imports: [ApplyPipe], | ||
| }) | ||
| class TestComponent { | ||
| readonly prefix = signal('Value: ') | ||
| readonly value = signal(4) | ||
| readonly fn = signal(doubleFn) | ||
| } | ||
|
|
||
| const fixture = TestBed.createComponent(TestComponent) | ||
| TestBed.tick() | ||
| const component = fixture.componentInstance | ||
| const el = fixture.nativeElement as HTMLElement | ||
|
|
||
| expect(el.innerHTML).toBe('Value: 8') | ||
|
|
||
| // new fn and new value | ||
| component.value.set(5) | ||
| component.fn.set(squareFn) | ||
| TestBed.tick() | ||
| expect(el.innerHTML).toBe('Value: 25') | ||
|
|
||
| // rerender with the same value | ||
| component.prefix.set('Hello: ') | ||
| TestBed.tick() | ||
| expect(el.innerHTML).toBe('Hello: 25') | ||
| // should have been called once only since pure | ||
| expect(squareFn).toHaveBeenCalledTimes(1) | ||
| }) | ||
| }) | ||
This file contains hidden or 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,15 @@ | ||
| import { Pipe, PipeTransform } from '@angular/core' | ||
|
|
||
| /** | ||
| * generic pipe to call one-param functions from template and make use of angular pure pipe optimization. | ||
| * @example | ||
| * ```angular-html | ||
| * {{ myDate | apply: myDateFormatter }} | ||
| * ``` | ||
| */ | ||
| @Pipe({ name: 'apply' }) | ||
|
mumenthalers marked this conversation as resolved.
|
||
| export class ApplyPipe implements PipeTransform { | ||
| transform<T, R>(value: T, fn: (arg: T) => R): R { | ||
| return fn(value) | ||
| } | ||
| } | ||
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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,156 @@ | ||
| import { Directive, input, model, signal } from '@angular/core' | ||
| import { describe, expectTypeOf, it } from 'vitest' | ||
|
|
||
| import { InputsOf } from './inputs-of.type' | ||
|
|
||
| describe('InputsOf', () => { | ||
| it('extracts InputSignal properties with their unwrapped types', () => { | ||
| @Directive() | ||
| class TestClazz { | ||
| readonly name = input.required<string>() | ||
| readonly age = input<number>(0) | ||
| readonly isActive = signal(true) | ||
| } | ||
|
|
||
| type Result = InputsOf<TestClazz> | ||
|
|
||
| expectTypeOf<Result>().toEqualTypeOf<{ | ||
| readonly name: string | ||
| readonly age: number | ||
| }>() | ||
| }) | ||
|
|
||
| it('extracts ModelSignal properties with their unwrapped types', () => { | ||
| @Directive() | ||
| class TestClazz { | ||
| readonly count = model(0) | ||
| readonly items = model<string[]>([]) | ||
| readonly isVisible = signal(false) | ||
| } | ||
|
|
||
| type Result = InputsOf<TestClazz> | ||
|
|
||
| expectTypeOf<Result>().toEqualTypeOf<{ | ||
| readonly count: number | ||
| readonly items: string[] | ||
| }>() | ||
| }) | ||
|
|
||
| it('excludes regular signals and non-signal properties', () => { | ||
| @Directive() | ||
| class TestClazz { | ||
| readonly name = input<string>('default') | ||
| readonly regularSignal = signal(true) | ||
| readonly plainProperty = 'test' | ||
| readonly method = () => {} | ||
| } | ||
|
|
||
| type Result = InputsOf<TestClazz> | ||
|
|
||
| expectTypeOf<Result>().toEqualTypeOf<{ | ||
| readonly name: string | ||
| }>() | ||
| }) | ||
|
|
||
| it('handles mixed InputSignal and ModelSignal', () => { | ||
| @Directive() | ||
| class TestClazz { | ||
| readonly title = input.required<string>() | ||
| readonly description = input<string>('default') | ||
| readonly status = model<'active' | 'inactive'>('active') | ||
| readonly count = model(0) | ||
| readonly timestamp = signal(Date.now()) | ||
| readonly regular = 'prop' | ||
| } | ||
|
|
||
| type Result = InputsOf<TestClazz> | ||
|
|
||
| expectTypeOf<Result>().toEqualTypeOf<{ | ||
| readonly title: string | ||
| readonly description: string | ||
| readonly status: 'active' | 'inactive' | ||
| readonly count: number | ||
| }>() | ||
| }) | ||
|
|
||
| it('handles complex types in signals', () => { | ||
| interface User { | ||
| id: string | ||
| name: string | ||
| email: string | ||
| } | ||
|
|
||
| @Directive() | ||
| class TestClazz { | ||
| readonly user = input.required<User>() | ||
| readonly roles = input<string[]>([]) | ||
| readonly metadata = model<Record<string, unknown>>({}) | ||
| } | ||
|
|
||
| type Result = InputsOf<TestClazz> | ||
|
|
||
| expectTypeOf<Result>().toEqualTypeOf<{ | ||
| readonly user: User | ||
| readonly roles: string[] | ||
| readonly metadata: Record<string, unknown> | ||
| }>() | ||
| }) | ||
|
|
||
| it('handles empty component (no inputs or models)', () => { | ||
| @Directive() | ||
| class EmptyComponent { | ||
| readonly counter = signal(0) | ||
| readonly value = 42 | ||
| } | ||
|
|
||
| type Result = InputsOf<EmptyComponent> | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-empty-object-type | ||
| expectTypeOf<Result>().toEqualTypeOf<{}>() | ||
| }) | ||
|
|
||
| it('handles generic types', () => { | ||
| @Directive() | ||
| class TestClazz<T> { | ||
| readonly items = input.required<T[]>() | ||
| readonly selectedItem = model<T | null>(null) | ||
| } | ||
|
|
||
| type StringArrayResult = InputsOf<TestClazz<string>> | ||
|
|
||
| expectTypeOf<StringArrayResult>().toEqualTypeOf<{ | ||
| readonly items: string[] | ||
| readonly selectedItem: string | null | ||
| }>() | ||
| }) | ||
|
|
||
| it('handles optional input signals without default value as potentially undefined', () => { | ||
| @Directive() | ||
| class TestClazz { | ||
| readonly required = input.required<string>() | ||
| readonly optional = input<string>() | ||
| } | ||
|
|
||
| type Result = InputsOf<TestClazz> | ||
|
|
||
| expectTypeOf<Result>().toEqualTypeOf<{ | ||
| readonly required: string | ||
| readonly optional: string | undefined | ||
| }>() | ||
| }) | ||
|
|
||
| it('handles nullable types in signals', () => { | ||
| @Directive() | ||
| class TestClazz { | ||
| readonly user = input<{ name: string } | null>(null) | ||
| readonly items = model<string[] | null>(null) | ||
| } | ||
|
|
||
| type Result = InputsOf<TestClazz> | ||
|
|
||
| expectTypeOf<Result>().toEqualTypeOf<{ | ||
| readonly user: { name: string } | null | ||
| readonly items: string[] | null | ||
| }>() | ||
| }) | ||
| }) |
This file contains hidden or 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,26 @@ | ||
| import { InputSignal, ModelSignal } from '@angular/core' | ||
|
|
||
| /** | ||
| * Extracts all properties from a type that are InputSignal or ModelSignal, | ||
| * Caveat: There's no way to differ between required inputs and inputs with default values, as both only expose the non-undefined type. | ||
| * @returns a record where the value is the unwrapped signal type. | ||
| * @example | ||
| * ```ts | ||
| * class MyComponent { | ||
| * readonly name = input.required<string>() | ||
| * readonly age = model(30) | ||
| * readonly isActive = signal(true) | ||
| * } | ||
| * type MyComponentInputs = InputsOf<MyComponent> | ||
| * // MyComponentInputs is equivalent to: { readonly name: string; readonly age: number; } | ||
| * ``` | ||
| */ | ||
| export type InputsOf<T> = { | ||
| readonly [K in keyof T as T[K] extends InputSignal<any> | ModelSignal<any> ? K : never]: T[K] extends InputSignal< | ||
| infer U | ||
| > | ||
| ? U | ||
| : T[K] extends ModelSignal<infer U> | ||
| ? U | ||
| : never | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.