Skip to content

Commit c5b3092

Browse files
authored
Merge pull request #20 from typed-ember/more-robust-component-keyword
Get `{{component}}` working more robustly
2 parents 3331038 + 33e5567 commit c5b3092

File tree

2 files changed

+171
-41
lines changed

2 files changed

+171
-41
lines changed
Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
1-
import { AcceptsBlocks } from '../signature';
1+
import { AcceptsBlocks, AnyBlocks } from '../signature';
22
import { HasSignature } from '../resolution';
33

4-
type SignatureFor<T> = T extends HasSignature<infer Signature> ? Signature : T;
5-
6-
type ArgsFor<T> = SignatureFor<T> extends (args: infer Args) => unknown ? Args : {};
7-
8-
type PositionalFor<T> = SignatureFor<T> extends (
9-
args: never,
10-
...positional: infer Positional
11-
) => unknown
12-
? Positional
13-
: never[];
14-
15-
type BlocksFor<T> = SignatureFor<T> extends (...params: never) => AcceptsBlocks<infer Blocks>
16-
? Blocks
17-
: {};
18-
194
export default interface ComponentKeyword {
20-
<Component, GivenArgs extends keyof ArgsFor<Component>>(
21-
args: { [Arg in GivenArgs]: ArgsFor<Component>[Arg] },
22-
component: Component
5+
// Invoking with a component class
6+
<
7+
Args,
8+
GivenArgs extends Partial<Args>,
9+
Blocks extends AnyBlocks,
10+
ConstructorArgs extends unknown[]
11+
>(
12+
args: GivenArgs,
13+
component: new (...args: ConstructorArgs) => HasSignature<(args: Args) => AcceptsBlocks<Blocks>>
14+
): (
15+
args: Omit<Args, keyof GivenArgs> & Partial<Pick<Args, keyof GivenArgs & keyof Args>>
16+
) => AcceptsBlocks<Blocks>;
17+
18+
// Invoking with the result of another `{{component}}` expression
19+
<Args, GivenArgs extends Partial<Args>, Blocks extends AnyBlocks>(
20+
args: GivenArgs,
21+
component: (args: Args) => AcceptsBlocks<Blocks>
2322
): (
24-
args: Omit<ArgsFor<Component>, GivenArgs> & Partial<Pick<ArgsFor<Component>, GivenArgs>>,
25-
...positional: PositionalFor<Component>
26-
) => AcceptsBlocks<BlocksFor<Component>>;
23+
args: Omit<Args, keyof GivenArgs> & Partial<Pick<Args, keyof GivenArgs & keyof Args>>
24+
) => AcceptsBlocks<Blocks>;
2725
}
Lines changed: 151 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,33 @@
11
import { expectTypeOf } from 'expect-type';
22
import { resolve, invokeBlock } from '@glint/template';
3-
import { AcceptsBlocks } from '@glint/template/-private';
43
import { ComponentKeyword } from '@glint/template/-private/keywords';
4+
import TestComponent from '../test-component';
55

66
const componentKeyword = resolve({} as ComponentKeyword);
77

8-
declare const TestComponent: (args: {
9-
value: string;
10-
}) => AcceptsBlocks<{
11-
default?: [string];
12-
inverse?: [];
13-
}>;
8+
class StringComponent extends TestComponent<{ value: string }, { default?: [string] }> {}
149

15-
const NoopCurriedTestComponent = componentKeyword({}, TestComponent);
16-
const ValueCurriedTestComponent = componentKeyword({ value: 'hello' }, TestComponent);
10+
const NoopCurriedStringComponent = componentKeyword({}, StringComponent);
11+
const ValueCurriedStringComponent = componentKeyword({ value: 'hello' }, StringComponent);
1712

1813
// Invoking the noop-curried component
19-
invokeBlock(resolve(NoopCurriedTestComponent)({ value: 'hello' }), {});
14+
invokeBlock(resolve(NoopCurriedStringComponent)({ value: 'hello' }), {});
2015

2116
// @ts-expect-error: Invoking the curried component but forgetting `value`
22-
resolve(NoopCurriedTestComponent)({});
17+
resolve(NoopCurriedStringComponent)({});
2318

2419
// @ts-expect-error: Invoking the curried component with an invalid value
25-
resolve(NoopCurriedTestComponent)({ value: 123 });
20+
resolve(NoopCurriedStringComponent)({ value: 123 });
2621

2722
// Invoking the noop-curried component with a valid block
28-
invokeBlock(resolve(NoopCurriedTestComponent)({ value: 'hello' }), {
23+
invokeBlock(resolve(NoopCurriedStringComponent)({ value: 'hello' }), {
2924
default(...args) {
3025
expectTypeOf(args).toEqualTypeOf<[string]>();
3126
},
3227
});
3328

3429
// Invoking the noop-curried component with an invalid block
35-
invokeBlock(resolve(NoopCurriedTestComponent)({ value: 'hello' }), {
30+
invokeBlock(resolve(NoopCurriedStringComponent)({ value: 'hello' }), {
3631
default() {
3732
/* nothing */
3833
},
@@ -43,16 +38,153 @@ invokeBlock(resolve(NoopCurriedTestComponent)({ value: 'hello' }), {
4338
});
4439

4540
// Invoking the curried-with-value component with no value
46-
invokeBlock(resolve(ValueCurriedTestComponent)({}), {});
41+
invokeBlock(resolve(ValueCurriedStringComponent)({}), {});
4742

4843
// Invoking the curried-with-value component with a valid value
49-
invokeBlock(resolve(ValueCurriedTestComponent)({ value: 'hi' }), {});
44+
invokeBlock(resolve(ValueCurriedStringComponent)({ value: 'hi' }), {});
5045

5146
// @ts-expect-error: Invoking the curred-with-value component with an invalid value
52-
invokeBlock(resolve(ValueCurriedTestComponent)({ value: 123 }), {});
47+
invokeBlock(resolve(ValueCurriedStringComponent)({ value: 123 }), {});
5348

5449
// @ts-expect-error: Attempting to curry a nonexistent arg
55-
componentKeyword({ foo: true }, TestComponent);
50+
componentKeyword({ foo: true }, StringComponent);
5651

5752
// @ts-expect-error: Attempting to curry an arg with the wrong type
58-
componentKeyword({ value: 123 }, TestComponent);
53+
componentKeyword({ value: 123 }, StringComponent);
54+
55+
class ParametricComponent<T> extends TestComponent<
56+
{ values: Array<T>; optional?: string },
57+
{ default?: [T, number] }
58+
> {}
59+
60+
const NoopCurriedParametricComponent = componentKeyword({}, ParametricComponent);
61+
62+
// The only way to fix a type parameter as part of using the component keyword is to
63+
// say ahead of time the type you're trying to bind it as.
64+
const BoundParametricComponent = ParametricComponent as new () => ParametricComponent<string>;
65+
66+
const RequiredValueCurriedParametricComponent = componentKeyword(
67+
{ values: ['hello'] },
68+
BoundParametricComponent
69+
);
70+
71+
const OptionalValueCurriedParametricComponent = componentKeyword(
72+
{ optional: 'hi' },
73+
ParametricComponent
74+
);
75+
76+
// Invoking the noop-curried component with number values
77+
invokeBlock(resolve(NoopCurriedParametricComponent)({ values: [1, 2, 3] }), {
78+
default(value) {
79+
expectTypeOf(value).toEqualTypeOf<number>();
80+
},
81+
});
82+
83+
// Invoking the noop-curried component with string values
84+
invokeBlock(resolve(NoopCurriedParametricComponent)({ values: ['hello'] }), {
85+
default(value) {
86+
expectTypeOf(value).toEqualTypeOf<string>();
87+
},
88+
});
89+
90+
invokeBlock(
91+
resolve(NoopCurriedParametricComponent)(
92+
// @ts-expect-error: missing required arg `values`
93+
{}
94+
),
95+
{}
96+
);
97+
98+
invokeBlock(
99+
resolve(NoopCurriedParametricComponent)(
100+
// @ts-expect-error: wrong type for `values`
101+
{ values: 'hello' }
102+
),
103+
{}
104+
);
105+
106+
invokeBlock(
107+
resolve(NoopCurriedParametricComponent)({
108+
values: [1, 2, 3],
109+
// @ts-expect-error: extra arg
110+
extra: 'uh oh',
111+
}),
112+
{}
113+
);
114+
115+
// Invoking the curred component with no additional args
116+
invokeBlock(resolve(RequiredValueCurriedParametricComponent)({}), {
117+
default(value) {
118+
expectTypeOf(value).toEqualTypeOf<string>();
119+
},
120+
});
121+
122+
// Invoking the curred component and overriding the given arg
123+
invokeBlock(resolve(RequiredValueCurriedParametricComponent)({ values: ['ok'] }), {
124+
default(value) {
125+
expectTypeOf(value).toEqualTypeOf<string>();
126+
},
127+
});
128+
129+
invokeBlock(
130+
resolve(RequiredValueCurriedParametricComponent)({
131+
// @ts-expect-error: wrong type for arg override
132+
values: [1, 2, 3],
133+
}),
134+
{}
135+
);
136+
137+
invokeBlock(
138+
resolve(RequiredValueCurriedParametricComponent)({
139+
// @ts-expect-error: extra arg
140+
extra: 'bad',
141+
}),
142+
{}
143+
);
144+
145+
// Invoking the curried component, supplying missing required args
146+
invokeBlock(resolve(OptionalValueCurriedParametricComponent)({ values: [1, 2, 3] }), {
147+
default(value) {
148+
expectTypeOf(value).toEqualTypeOf<number>();
149+
},
150+
});
151+
152+
invokeBlock(
153+
resolve(OptionalValueCurriedParametricComponent)(
154+
// @ts-expect-error: missing required arg `values`
155+
{}
156+
),
157+
{}
158+
);
159+
160+
// {{component (component BoundParametricComponent values=(array "hello")) optional="hi"}}
161+
const DoubleCurriedComponent = componentKeyword(
162+
{ optional: 'hi' },
163+
RequiredValueCurriedParametricComponent
164+
);
165+
166+
// Invoking the component with no args
167+
invokeBlock(resolve(DoubleCurriedComponent)({}), {
168+
default(value) {
169+
expectTypeOf(value).toEqualTypeOf<string>();
170+
},
171+
});
172+
173+
// Invoking the component overriding an arg correctly
174+
invokeBlock(resolve(DoubleCurriedComponent)({ values: ['a', 'b'] }), {});
175+
176+
invokeBlock(
177+
resolve(DoubleCurriedComponent)({
178+
// @ts-expect-error: invalid arg override
179+
values: [1, 2, 3],
180+
}),
181+
{}
182+
);
183+
184+
invokeBlock(
185+
resolve(DoubleCurriedComponent)({
186+
// @ts-expect-error: unexpected args
187+
foo: 'bar',
188+
}),
189+
{}
190+
);

0 commit comments

Comments
 (0)