Skip to content

Commit e0b876a

Browse files
feat(angular): add support to bindings and directives in FlexRender (#6152)
* feat(angular): add support for `bindings` and `directives` in flexRender via createComponent
1 parent 929a948 commit e0b876a

7 files changed

Lines changed: 319 additions & 60 deletions

File tree

packages/angular-table/src/flex-render/flexRenderComponent.ts

Lines changed: 210 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,109 @@
11
import { reflectComponentType } from '@angular/core'
22
import type {
3+
Binding,
34
ComponentMirror,
45
Injector,
56
InputSignal,
67
OutputEmitterRef,
78
Type,
9+
createComponent,
810
} from '@angular/core'
911

10-
interface FlexRenderRequiredInputOptions<
12+
type CreateComponentOptions = Parameters<typeof createComponent>[1]
13+
type CreateComponentBindings = CreateComponentOptions['bindings']
14+
type CreateComponentDirectives = CreateComponentOptions['directives']
15+
16+
interface FlexRenderOptions<
1117
TInputs extends Record<string, any>,
1218
TOutputs extends Record<string, any>,
1319
> {
1420
/**
15-
* Component instance inputs.
16-
* They will be set via [componentRef.setInput API](https://angular.dev/api/core/ComponentRef#setInput)
21+
* Native Angular bindings applied at component creation time via `createComponent`.
22+
* Use this option to set inputs, outputs, or two-way bindings at creation time.
23+
* Shouldn't be used together with {@link FlexRenderOptions#inputs} or {@link FlexRenderOptions#outputs} option.
24+
*
25+
* Binding input/outputs at creation time: {@link https://angular.dev/guide/components/programmatic-rendering#binding-inputs-outputs-and-setting-host-directives-at-creation}
26+
*
27+
* Two-way binding: {@link https://angular.dev/api/core/twoWayBinding}
28+
*
29+
* Output binding: {@link https://angular.dev/api/core/outputBinding}
30+
*
31+
* Input binding: {@link https://angular.dev/api/core/inputBinding}
32+
*
33+
* @example
34+
* ```ts
35+
* import {flexRenderComponent} from '@tanstack/angular-table';
36+
* flexRenderComponent(MyComponent, {
37+
* bindings: [
38+
* // Will update `value` input every time `mySignalValue` changes
39+
* inputBinding('value', mySignalValue),
40+
* // Set myProperty to 1 when the component is created
41+
* inputBinding('myProperty', () => 1),
42+
* // Callback called every time `valueChange` output emit
43+
* outputBinding('valueChange', value => {
44+
* console.log("my value changed to", value)
45+
* }),
46+
* // Two-way binding between `value` input and `valueChange` output
47+
* // Useful while using `model` inputs.
48+
* twoWayBinding('value', mySignal)
49+
* ]
50+
* })
51+
* ```
1752
*/
18-
inputs: TInputs
53+
readonly bindings?: Array<Binding>
1954
/**
20-
* Component instance outputs.
55+
* Directives to apply to the component at creation time.
56+
*
57+
* Binding directives at creation time: {@link https://angular.dev/guide/components/programmatic-rendering#binding-inputs-outputs-and-setting-host-directives-at-creation}
58+
*
59+
* Two-way binding: {@link https://angular.dev/api/core/twoWayBinding}
60+
*
61+
* Output binding: {@link https://angular.dev/api/core/outputBinding}
62+
*
63+
* Input binding: {@link https://angular.dev/api/core/inputBinding}
64+
*
65+
* @example
66+
* ```ts
67+
* import {flexRenderComponent} from '@tanstack/angular-table';
68+
* flexRenderComponent(MyComponent, {
69+
* bindings: [
70+
* // ...
71+
* ],
72+
* directives: [
73+
* DirectiveA,
74+
* {
75+
* type: DirectiveB,
76+
* bindings: [
77+
* inputBinding('value', mySignalValue),
78+
* // ...
79+
* ]
80+
* }
81+
* ]
82+
* })
83+
* ```
2184
*/
22-
outputs?: TOutputs
23-
/**
24-
* Optional {@link Injector} that will be used when rendering the component
25-
*/
26-
injector?: Injector
27-
}
28-
29-
interface FlexRenderOptions<
30-
TInputs extends Record<string, any>,
31-
TOutputs extends Record<string, any>,
32-
> {
85+
readonly directives?: CreateComponentDirectives
3386
/**
3487
* Component instance inputs.
35-
* They will be set via [componentRef.setInput API](https://angular.dev/api/core/ComponentRef#setInput)
88+
*
89+
* These values are assigned after the component has been created using
90+
* [componentRef.setInput API](https://angular.dev/api/core/ComponentRef#setInput).
91+
*
92+
* Shouldn't be used together with {@link FlexRenderOptions#bindings} option
3693
*/
37-
inputs?: TInputs
94+
readonly inputs?: TInputs
3895
/**
3996
* Component instance outputs.
97+
*
98+
* Outputs are wired imperatively after component creation using {@link OutputEmitterRef#subscribe}.
99+
*
100+
* Shouldn't be used together with {@link FlexRenderOptions#bindings} option
40101
*/
41-
outputs?: TOutputs
102+
readonly outputs?: TOutputs
42103
/**
43104
* Optional {@link Injector} that will be used when rendering the component
44105
*/
45-
injector?: Injector
106+
readonly injector?: Injector
46107
}
47108

48109
type Inputs<T> = {
@@ -59,41 +120,145 @@ type Outputs<T> = {
59120
: never
60121
}
61122

62-
type OptionalKeys<T, K = keyof T> = K extends keyof T
63-
? T[K] extends Required<T>[K]
64-
? undefined extends T[K]
65-
? K
66-
: never
67-
: K
68-
: never
69-
70123
/**
71-
* Helper function to create a [@link FlexRenderComponent] instance, with better type-safety.
124+
* Helper function to create a {@link FlexRenderComponent} instance, with better type-safety.
125+
*
126+
* @example
127+
* ```ts
128+
* import {flexRenderComponent} from '@tanstack/angular-table'
129+
* import {inputBinding, outputBinding} from '@angular/core';
72130
*
73-
* - options object must be passed when the given component instance contains at least one required signal input.
74-
* - options/inputs is typed with the given component inputs
75-
* - options/outputs is typed with the given component outputs
131+
* const columns = [
132+
* {
133+
* cell: ({ row }) => {
134+
* return flexRenderComponent(MyComponent, {
135+
* inputs: { value: mySignalValue() },
136+
* outputs: { valueChange: (val) => {} }
137+
* // or using angular native createComponent#binding api
138+
* bindings: [
139+
* inputBinding('value', mySignalValue),
140+
* outputBinding('valueChange', value => {
141+
* console.log("my value changed to", value)
142+
* })
143+
* ]
144+
* })
145+
* },
146+
* },
147+
* ]
148+
* ```
76149
*/
77-
export function flexRenderComponent<
78-
TComponent = any,
79-
TInputs extends Inputs<TComponent> = Inputs<TComponent>,
80-
TOutputs extends Outputs<TComponent> = Outputs<TComponent>,
81-
>(
150+
export function flexRenderComponent<TComponent = any>(
82151
component: Type<TComponent>,
83-
...options: OptionalKeys<TInputs> extends never
84-
? [FlexRenderOptions<TInputs, TOutputs>?]
85-
: [FlexRenderRequiredInputOptions<TInputs, TOutputs>]
152+
options?: FlexRenderOptions<Inputs<TComponent>, Outputs<TComponent>>,
86153
): FlexRenderComponent<TComponent> {
87-
const { inputs, injector, outputs } = options[0] ?? {}
88-
return new FlexRenderComponent(component, inputs, injector, outputs)
154+
const { inputs, injector, outputs, directives, bindings } = options ?? {}
155+
return new FlexRenderComponentInstance(
156+
component,
157+
inputs,
158+
injector,
159+
outputs,
160+
directives,
161+
bindings,
162+
)
163+
}
164+
165+
/**
166+
* Wrapper interface for a component that will be used as content for {@link FlexRenderDirective}.
167+
* Can be created using {@link flexRenderComponent} helper.
168+
*
169+
* @example
170+
*
171+
* ```ts
172+
* import {flexRenderComponent} from '@tanstack/angular-table'
173+
*
174+
* // Usage in cell/header/footer definition
175+
* const columns = [
176+
* {
177+
* cell: ({ row }) => {
178+
* return flexRenderComponent(MyComponent, {
179+
* inputs: { value: mySignalValue() },
180+
* outputs: { valueChange: (val) => {} }
181+
* // or using angular createComponent#bindings api
182+
* bindings: [
183+
* inputBinding('value', mySignalValue),
184+
* outputBinding('valueChange', value => {
185+
* console.log("my value changed to", value)
186+
* })
187+
* ]
188+
* })
189+
* },
190+
* },
191+
* ]
192+
*
193+
* import {input, output} from '@angular/core';
194+
*
195+
* @Component({
196+
* selector: 'my-component',
197+
* })
198+
* class MyComponent {
199+
* readonly value = input(0);
200+
* readonly valueChange = output<number>();
201+
* }
202+
*
203+
* ```
204+
*/
205+
export interface FlexRenderComponent<TComponent = any> {
206+
/**
207+
* The component type
208+
*/
209+
readonly component: Type<TComponent>
210+
/**
211+
* Reflected metadata about the component.
212+
*/
213+
readonly mirror: ComponentMirror<TComponent>
214+
/**
215+
* List of allowed input names.
216+
*/
217+
readonly allowedInputNames: Array<string>
218+
/**
219+
* List of allowed output names.
220+
*/
221+
readonly allowedOutputNames: Array<string>
222+
/**
223+
* Component instance outputs. Subscribed via {@link OutputEmitterRef#subscribe}
224+
*
225+
* @see {@link FlexRenderOptions#outputs}
226+
*/
227+
readonly outputs?: Outputs<TComponent>
228+
/**
229+
* Component instance inputs. Set via [componentRef.setInput API](https://angular.dev/api/core/ComponentRef#setInput))
230+
*
231+
* @see {@link FlexRenderOptions#inputs}
232+
*/
233+
readonly inputs?: Inputs<TComponent>
234+
/**
235+
* Optional {@link Injector} that will be used when rendering the component.
236+
*
237+
* @see {@link FlexRenderOptions#injector}
238+
*/
239+
readonly injector?: Injector
240+
/**
241+
* Bindings to apply to the root component
242+
*
243+
* @see {@link FlexRenderOptions#bindings}
244+
*/
245+
bindings?: CreateComponentBindings
246+
/**
247+
* Directives that should be applied to the component.
248+
*
249+
* @see {FlexRenderOptions#directives}
250+
*/
251+
directives?: CreateComponentDirectives
89252
}
90253

91254
/**
92255
* Wrapper class for a component that will be used as content for {@link FlexRenderDirective}
93256
*
94257
* Prefer {@link flexRenderComponent} helper for better type-safety
95258
*/
96-
export class FlexRenderComponent<TComponent = any> {
259+
export class FlexRenderComponentInstance<
260+
TComponent = any,
261+
> implements FlexRenderComponent<TComponent> {
97262
readonly mirror: ComponentMirror<TComponent>
98263
readonly allowedInputNames: Array<string> = []
99264
readonly allowedOutputNames: Array<string> = []
@@ -103,6 +268,8 @@ export class FlexRenderComponent<TComponent = any> {
103268
readonly inputs?: Inputs<TComponent>,
104269
readonly injector?: Injector,
105270
readonly outputs?: Outputs<TComponent>,
271+
readonly directives?: CreateComponentDirectives,
272+
readonly bindings?: CreateComponentBindings,
106273
) {
107274
const mirror = reflectComponentType(component)
108275
if (!mirror) {

packages/angular-table/src/flex-render/flexRenderComponentFactory.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ export class FlexRenderComponentFactory {
2525
): FlexRenderComponentRef<T> {
2626
const componentRef = this.#viewContainerRef.createComponent(
2727
flexRenderComponent.component,
28-
{ injector: componentInjector },
28+
{
29+
injector: componentInjector,
30+
directives: flexRenderComponent.directives,
31+
bindings: flexRenderComponent.bindings ?? [],
32+
},
2933
)
3034
const view = new FlexRenderComponentRef(
3135
componentRef,

packages/angular-table/src/flex-render/view.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { TemplateRef, Type } from '@angular/core'
2-
import { FlexRenderComponent } from './flexRenderComponent'
2+
import { FlexRenderComponentInstance } from './flexRenderComponent'
3+
import type { FlexRenderComponent } from './flexRenderComponent'
34
import type { FlexRenderContent } from './renderer'
45
import type { EmbeddedViewRef } from '@angular/core'
56
import type { FlexRenderComponentRef } from './flexRenderComponentFactory'
@@ -23,7 +24,7 @@ export function mapToFlexRenderTypedContent(
2324
if (typeof content === 'string' || typeof content === 'number') {
2425
return { kind: 'primitive', content }
2526
}
26-
if (content instanceof FlexRenderComponent) {
27+
if (content instanceof FlexRenderComponentInstance) {
2728
return { kind: 'flexRenderComponent', content }
2829
} else if (content instanceof TemplateRef) {
2930
return { kind: 'templateRef', content }

packages/angular-table/src/flexRender.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ export type {
8080
*/
8181
@Directive({
8282
selector: 'ng-template[flexRender]',
83-
standalone: true,
8483
})
8584
export class FlexRenderDirective<
8685
TFeatures extends TableFeatures,

packages/angular-table/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export * from '@tanstack/table-core'
66
export * from './angularReactivityFeature'
77
export * from './flexRender'
88
export * from './injectTable'
9-
export { flexRenderComponent } from './flex-render/flexRenderComponent'
9+
export * from './flex-render/flexRenderComponent'
1010

1111
export * from './helpers/cell'
1212
export * from './helpers/header'

packages/angular-table/tests/flex-render/flex-render-component.test-d.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,3 @@ test('Infer component inputs', () => {
1313
// Input is optional so we can skip passing the property
1414
flexRenderComponent(Test, { inputs: {} })
1515
})
16-
17-
test('Options are mandatory when given component has required inputs', () => {
18-
class Test {
19-
readonly input1 = input<string>()
20-
readonly requiredInput1 = input.required<string>()
21-
}
22-
23-
// @ts-expect-error Required input
24-
flexRenderComponent(Test)
25-
26-
flexRenderComponent(Test, { inputs: { requiredInput1: 'My value' } })
27-
})

0 commit comments

Comments
 (0)