Content-Length: 479814 | pFad | https://github.com/angular/angular/commit/b154fb391142db6139cddad820a9a72838f0f16c

2E feat(core): add support for two-way bindings on dynamically-created c… · angular/angular@b154fb3 · GitHub
Skip to content

Commit b154fb3

Browse files
crisbetopkozlowski-opensource
authored andcommitted
feat(core): add support for two-way bindings on dynamically-created components (#60342)
Builds on the changes from #60137 to add support for two-way bindings on dynamically-created components. Example usage: ```typescript import {createComponent, signal, twoWayBinding} from '@angular/core'; const value = signal(''); createComponent(MyCheckbox, { bindings: [ twoWayBinding('value', value), ], }); ``` In the example above the value of `MyCheckbox` and the `value` signal will be kept in sync. PR Close #60342
1 parent bf2b025 commit b154fb3

File tree

5 files changed

+642
-5
lines changed

5 files changed

+642
-5
lines changed

goldens/public-api/core/index.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,6 +1838,9 @@ export const TRANSLATIONS: InjectionToken<string>;
18381838
// @public
18391839
export const TRANSLATIONS_FORMAT: InjectionToken<string>;
18401840

1841+
// @public
1842+
export function twoWayBinding(publicName: string, value: WritableSignal<unknown>): Binding;
1843+
18411844
// @public
18421845
export const Type: FunctionConstructor;
18431846

packages/core/src/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export {
112112
afterNextRender,
113113
ɵFirstAvailable,
114114
} from './render3/after_render/hooks';
115-
export {inputBinding, outputBinding} from './render3/dynamic_bindings';
115+
export {inputBinding, outputBinding, twoWayBinding} from './render3/dynamic_bindings';
116116
export {ApplicationConfig, mergeApplicationConfig} from './application/application_config';
117117
export {makeStateKey, StateKey, TransferState} from './transfer_state';
118118
export {booleanAttribute, numberAttribute} from './util/coercion';

packages/core/src/render3/component_ref.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,8 @@ function getRootTViewTemplate(
458458
}
459459

460460
function isInputBinding(binding: Binding): boolean {
461-
return binding[BINDING].kind === 'input';
461+
const kind = binding[BINDING].kind;
462+
return kind === 'input' || kind === 'twoWay';
462463
}
463464

464465
/**
@@ -498,7 +499,7 @@ export class ComponentRef<T> extends AbstractComponentRef<T> {
498499
if (this._hasInputBindings && ngDevMode) {
499500
throw new RuntimeError(
500501
RuntimeErrorCode.INVALID_SET_INPUT_CALL,
501-
'Cannot call `setInput` on a component that is using the `inputBinding` function.',
502+
'Cannot call `setInput` on a component that is using the `inputBinding` or `twoWayBinding` functions.',
502503
);
503504
}
504505

packages/core/src/render3/dynamic_bindings.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import {WritableSignal} from '../core_reactivity_export_internal';
910
import {RuntimeError, RuntimeErrorCode} from '../errors';
10-
import {Type} from '../interface/type';
11+
import {Type, Writable} from '../interface/type';
12+
import {assertNotDefined} from '../util/assert';
1113
import {bindingUpdated} from './bindings';
1214
import {listenToDirectiveOutput, wrapListener} from './instructions/listener';
1315
import {setDirectiveInput, storePropertyBindingMetadata} from './instructions/shared';
@@ -174,3 +176,47 @@ export function outputBinding<T>(eventName: string, listener: (event: T) => unkn
174176

175177
return binding;
176178
}
179+
180+
/**
181+
* Creates a two-way binding.
182+
* @param eventName Public name of the two-way compatible input.
183+
* @param value Writable signal from which to get the current value and to which to write new
184+
* values.
185+
*
186+
* ### Usage example
187+
* In this example we create an instance of the `MyCheckbox` component and bind to its `value`
188+
* input using a two-way binding.
189+
*
190+
* ```
191+
* const checkboxValue = signal('');
192+
*
193+
* createComponent(MyCheckbox, {
194+
* bindings: [
195+
* twoWayBinding('value', checkboxValue),
196+
* ],
197+
* });
198+
* ```
199+
*/
200+
export function twoWayBinding(publicName: string, value: WritableSignal<unknown>): Binding {
201+
const input = inputBinding(publicName, value);
202+
const output = outputBinding(publicName + 'Change', (eventValue) => value.set(eventValue));
203+
204+
// We take advantage of inputs only having a `create` block and outputs only having an `update`
205+
// block by passing them through directly instead of creating dedicated functions here. This
206+
// assumption can break down if one of them starts targeting both blocks. These assertions
207+
// are here to help us catch it if something changes in the future.
208+
ngDevMode && assertNotDefined(input.create, 'Unexpected `create` callback in inputBinding');
209+
ngDevMode && assertNotDefined(output.update, 'Unexpected `update` callback in outputBinding');
210+
211+
return {
212+
[BINDING]: {
213+
kind: 'twoWay',
214+
requiredVars: input[BINDING].requiredVars + output[BINDING].requiredVars,
215+
},
216+
set target(target: unknown) {
217+
(input as Writable<Binding>).target = (output as Writable<Binding>).target = target;
218+
},
219+
create: output.create,
220+
update: input.update,
221+
};
222+
}

0 commit comments

Comments
 (0)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: https://github.com/angular/angular/commit/b154fb391142db6139cddad820a9a72838f0f16c

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy