Skip to content

Commit a3f4e72

Browse files
authored
Merge pull request #84 from shiftcode/#83-new-utilities
#83 new utilities
2 parents 5edf681 + 12106e7 commit a3f4e72

14 files changed

Lines changed: 413 additions & 7 deletions

apps/styleguide/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@shiftcode/styleguide",
3-
"version": "15.1.1",
3+
"version": "15.2.0-pr83.1",
44
"private": true,
55
"type": "module",
66
"scripts": {

lerna.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
33
"useNx": false,
44
"packages": ["libs/*", "apps/*"],
5-
"version": "15.1.1",
5+
"version": "15.2.0-pr83.1",
66
"command": {
77
"version": {
88
"allowBranch": "*",

libs/components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@shiftcode/ngx-components",
3-
"version": "15.1.1",
3+
"version": "15.2.0-pr83.1",
44
"repository": "https://github.com/shiftcode/sc-ng-commons-public",
55
"license": "MIT",
66
"author": "shiftcode GmbH <team@shiftcode.ch>",
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { ChangeDetectionStrategy, Component, signal } from '@angular/core'
2+
import { TestBed } from '@angular/core/testing'
3+
import { describe, expect, it, vi } from 'vitest'
4+
5+
import { ApplyPipe } from './apply.pipe'
6+
7+
describe('ApplyPipe', () => {
8+
it('calls the provided function with the provided value', () => {
9+
const pipe = new ApplyPipe()
10+
11+
const input = 'hello'
12+
const toUpperCaseFn = vi.fn((arg: string) => arg.toUpperCase())
13+
14+
pipe.transform(input, toUpperCaseFn)
15+
expect(toUpperCaseFn).toHaveBeenCalledWith(input)
16+
})
17+
18+
it('should return the result of the function call', () => {
19+
const pipe = new ApplyPipe()
20+
21+
const input = 42
22+
const doubleFn = (arg: number) => arg * 2
23+
const expected = 84
24+
25+
const result = pipe.transform(input, doubleFn)
26+
expect(result).toEqual(expected)
27+
})
28+
29+
it('works integrated', () => {
30+
const squareFn = vi.fn((value: number) => value * value)
31+
const doubleFn = vi.fn((value: number) => 2 * value)
32+
33+
@Component({
34+
selector: 'sc-test-component',
35+
template: '{{prefix()}}{{ value() | apply: fn() }}',
36+
changeDetection: ChangeDetectionStrategy.OnPush,
37+
imports: [ApplyPipe],
38+
})
39+
class TestComponent {
40+
readonly prefix = signal('Value: ')
41+
readonly value = signal(4)
42+
readonly fn = signal(doubleFn)
43+
}
44+
45+
const fixture = TestBed.createComponent(TestComponent)
46+
TestBed.tick()
47+
const component = fixture.componentInstance
48+
const el = fixture.nativeElement as HTMLElement
49+
50+
expect(el.innerHTML).toBe('Value: 8')
51+
52+
// new fn and new value
53+
component.value.set(5)
54+
component.fn.set(squareFn)
55+
TestBed.tick()
56+
expect(el.innerHTML).toBe('Value: 25')
57+
58+
// rerender with the same value
59+
component.prefix.set('Hello: ')
60+
TestBed.tick()
61+
expect(el.innerHTML).toBe('Hello: 25')
62+
// should have been called once only since pure
63+
expect(squareFn).toHaveBeenCalledTimes(1)
64+
})
65+
})
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Pipe, PipeTransform } from '@angular/core'
2+
3+
/**
4+
* generic pipe to call one-param functions from template and make use of angular pure pipe optimization.
5+
* @example
6+
* ```angular-html
7+
* {{ myDate | apply: myDateFormatter }}
8+
* ```
9+
*/
10+
@Pipe({ name: 'apply' })
11+
export class ApplyPipe implements PipeTransform {
12+
transform<T, R>(value: T, fn: (arg: T) => R): R {
13+
return fn(value)
14+
}
15+
}

libs/components/src/public-api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// apply
2+
export * from './lib/apply/apply.pipe'
3+
14
// svg
25
export * from './lib/svg/svg.component'
36
export * from './lib/svg/svg-base.directive'

libs/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@shiftcode/ngx-core",
3-
"version": "15.1.1",
3+
"version": "15.2.0-pr83.1",
44
"repository": "https://github.com/shiftcode/sc-ng-commons-public",
55
"license": "MIT",
66
"author": "shiftcode GmbH <team@shiftcode.ch>",

libs/core/src/lib/logger/with-custom-log-transport.function.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,21 @@ describe('withCustomLogTransport', () => {
5252
['test message'],
5353
)
5454
})
55+
56+
test('useExisting reuses the provided singleton instance', () => {
57+
TestBed.configureTestingModule({
58+
providers: [CustomLogTransport, provideLogger(withCustomLogTransport(CustomLogTransport, true))],
59+
})
60+
61+
// Ensure services get instantiated
62+
void TestBed.inject(LoggerService)
63+
64+
const customTransportSingleton = TestBed.inject(CustomLogTransport)
65+
66+
// log transport is provided with `multi` - thus it is an array.
67+
const customTransportFromLogger = TestBed.inject(LogTransport) as unknown as LogTransport[]
68+
69+
expect(customTransportFromLogger[0]).toBeInstanceOf(CustomLogTransport)
70+
expect(customTransportFromLogger[0]).toBe(customTransportSingleton)
71+
})
5572
})

libs/core/src/lib/logger/with-custom-log-transport.function.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,19 @@ import { LoggerFeatureKind } from './logger-feature-kind.enum'
66

77
/**
88
* LoggerFeature to use with {@link provideLogger} that registers a custom LogTransport implementation.
9-
* @param transportClass - The LogTransport implementation class to use
9+
* @param transportClass - The LogTransport implementation class to use.
10+
* @param useExisting - If `true`, the `transportClass` will be registered with `useExisting` instead of `useClass`.
11+
* This requires `transportClass` to already be registered as a provider; otherwise Angular will throw a provider-not-found error at runtime.
1012
*/
11-
export function withCustomLogTransport(transportClass: Type<LogTransport>): LoggerFeature {
13+
export function withCustomLogTransport(transportClass: Type<LogTransport>, useExisting = false): LoggerFeature {
1214
return {
1315
kind: LoggerFeatureKind.TRANSPORT,
14-
providers: [{ provide: LogTransport, useClass: transportClass, multi: true }],
16+
providers: [
17+
{
18+
provide: LogTransport,
19+
multi: true,
20+
...(useExisting ? { useExisting: transportClass } : { useClass: transportClass }),
21+
},
22+
],
1523
}
1624
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { Directive, input, model, signal } from '@angular/core'
2+
import { describe, expectTypeOf, it } from 'vitest'
3+
4+
import { InputsOf } from './inputs-of.type'
5+
6+
describe('InputsOf', () => {
7+
it('extracts InputSignal properties with their unwrapped types', () => {
8+
@Directive()
9+
class TestClazz {
10+
readonly name = input.required<string>()
11+
readonly age = input<number>(0)
12+
readonly isActive = signal(true)
13+
}
14+
15+
type Result = InputsOf<TestClazz>
16+
17+
expectTypeOf<Result>().toEqualTypeOf<{
18+
readonly name: string
19+
readonly age: number
20+
}>()
21+
})
22+
23+
it('extracts ModelSignal properties with their unwrapped types', () => {
24+
@Directive()
25+
class TestClazz {
26+
readonly count = model(0)
27+
readonly items = model<string[]>([])
28+
readonly isVisible = signal(false)
29+
}
30+
31+
type Result = InputsOf<TestClazz>
32+
33+
expectTypeOf<Result>().toEqualTypeOf<{
34+
readonly count: number
35+
readonly items: string[]
36+
}>()
37+
})
38+
39+
it('excludes regular signals and non-signal properties', () => {
40+
@Directive()
41+
class TestClazz {
42+
readonly name = input<string>('default')
43+
readonly regularSignal = signal(true)
44+
readonly plainProperty = 'test'
45+
readonly method = () => {}
46+
}
47+
48+
type Result = InputsOf<TestClazz>
49+
50+
expectTypeOf<Result>().toEqualTypeOf<{
51+
readonly name: string
52+
}>()
53+
})
54+
55+
it('handles mixed InputSignal and ModelSignal', () => {
56+
@Directive()
57+
class TestClazz {
58+
readonly title = input.required<string>()
59+
readonly description = input<string>('default')
60+
readonly status = model<'active' | 'inactive'>('active')
61+
readonly count = model(0)
62+
readonly timestamp = signal(Date.now())
63+
readonly regular = 'prop'
64+
}
65+
66+
type Result = InputsOf<TestClazz>
67+
68+
expectTypeOf<Result>().toEqualTypeOf<{
69+
readonly title: string
70+
readonly description: string
71+
readonly status: 'active' | 'inactive'
72+
readonly count: number
73+
}>()
74+
})
75+
76+
it('handles complex types in signals', () => {
77+
interface User {
78+
id: string
79+
name: string
80+
email: string
81+
}
82+
83+
@Directive()
84+
class TestClazz {
85+
readonly user = input.required<User>()
86+
readonly roles = input<string[]>([])
87+
readonly metadata = model<Record<string, unknown>>({})
88+
}
89+
90+
type Result = InputsOf<TestClazz>
91+
92+
expectTypeOf<Result>().toEqualTypeOf<{
93+
readonly user: User
94+
readonly roles: string[]
95+
readonly metadata: Record<string, unknown>
96+
}>()
97+
})
98+
99+
it('handles empty component (no inputs or models)', () => {
100+
@Directive()
101+
class EmptyComponent {
102+
readonly counter = signal(0)
103+
readonly value = 42
104+
}
105+
106+
type Result = InputsOf<EmptyComponent>
107+
108+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
109+
expectTypeOf<Result>().toEqualTypeOf<{}>()
110+
})
111+
112+
it('handles generic types', () => {
113+
@Directive()
114+
class TestClazz<T> {
115+
readonly items = input.required<T[]>()
116+
readonly selectedItem = model<T | null>(null)
117+
}
118+
119+
type StringArrayResult = InputsOf<TestClazz<string>>
120+
121+
expectTypeOf<StringArrayResult>().toEqualTypeOf<{
122+
readonly items: string[]
123+
readonly selectedItem: string | null
124+
}>()
125+
})
126+
127+
it('handles optional input signals without default value as potentially undefined', () => {
128+
@Directive()
129+
class TestClazz {
130+
readonly required = input.required<string>()
131+
readonly optional = input<string>()
132+
}
133+
134+
type Result = InputsOf<TestClazz>
135+
136+
expectTypeOf<Result>().toEqualTypeOf<{
137+
readonly required: string
138+
readonly optional: string | undefined
139+
}>()
140+
})
141+
142+
it('handles nullable types in signals', () => {
143+
@Directive()
144+
class TestClazz {
145+
readonly user = input<{ name: string } | null>(null)
146+
readonly items = model<string[] | null>(null)
147+
}
148+
149+
type Result = InputsOf<TestClazz>
150+
151+
expectTypeOf<Result>().toEqualTypeOf<{
152+
readonly user: { name: string } | null
153+
readonly items: string[] | null
154+
}>()
155+
})
156+
})

0 commit comments

Comments
 (0)