Skip to content

Commit 551c579

Browse files
crisbetothePunderWoman
authored andcommitted
refactor(core): address PR feedback (angular#54252)
Addresses the feedback from angular#54252. PR Close angular#54252
1 parent a17f6cb commit 551c579

12 files changed

Lines changed: 63 additions & 30 deletions

File tree

packages/compiler-cli/src/ngtsc/annotations/directive/src/model_function.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export function tryParseSignalModelMapping(
3434
const bindingPropertyName = options?.alias ?? classPropertyName;
3535

3636
return {
37+
call: model.call,
3738
input: {
3839
isSignal: true,
3940
transform: null,

packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,7 +1127,8 @@ function parseOutputFields(
11271127
`Using @Output with a model input is not allowed.`);
11281128
}
11291129

1130-
const queryNode = decoratorOutput?.decorator.node ?? initializerOutput?.call;
1130+
const queryNode =
1131+
decoratorOutput?.decorator.node ?? initializerOutput?.call ?? modelMapping?.call;
11311132
if (queryNode !== undefined && member.isStatic) {
11321133
throw new FatalDiagnosticError(
11331134
ErrorCode.INCORRECTLY_DECLARED_ON_STATIC_MEMBER, queryNode,
@@ -1148,7 +1149,8 @@ function parseOutputFields(
11481149

11491150
// Validate that initializer-based outputs are not accidentally declared
11501151
// in the `outputs` class metadata.
1151-
if (initializerOutput !== null && outputsFromMeta.hasOwnProperty(member.name)) {
1152+
if (((initializerOutput !== null || modelMapping !== null) &&
1153+
outputsFromMeta.hasOwnProperty(member.name))) {
11521154
throw new FatalDiagnosticError(
11531155
ErrorCode.INITIALIZER_API_DECORATOR_METADATA_COLLISION, member.node ?? clazz,
11541156
`Output "${member.name}" is unexpectedly declared in @${classDecorator.name} as well.`);

packages/compiler-cli/src/ngtsc/metadata/src/api.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,11 @@ export type InputMapping = InputOrOutput&{
152152

153153
/** Metadata for a model mapping. */
154154
export interface ModelMapping {
155-
/** Information about the input declared by the model. */
156-
input: InputOrOutput&{
157-
/** Whether the input declared by the model is required. */
158-
required: boolean;
155+
/** Node defining the model mapping. */
156+
call: ts.CallExpression;
159157

160-
/** Model inputs can't have transforms. Keeping it here for consistency. */
161-
transform: null;
162-
};
158+
/** Information about the input declared by the model. */
159+
input: InputMapping;
163160

164161
/** Information about the output implicitly declared by the model. */
165162
output: InputOrOutput;

packages/compiler-cli/src/ngtsc/typecheck/test/output_function_diagnostics.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,24 @@ runInEachFileSystem(() => {
3030
`TestComponent.html(1, 24): Property 'x' does not exist on type 'void'.`,
3131
],
3232
},
33+
{
34+
id: 'two way data binding, invalid',
35+
inputs: {'value': {type: 'InputSignal<string>', isSignal: true}},
36+
outputs: {'valueChange': {type: 'OutputEmitter<string>'}},
37+
template: `<div dir [(value)]="bla">`,
38+
component: `bla = true;`,
39+
expected: [
40+
`TestComponent.html(1, 12): Type 'boolean' is not assignable to type 'string | WritableSignal<string>'.`,
41+
],
42+
},
43+
{
44+
id: 'two way data binding, valid',
45+
inputs: {'value': {type: 'InputSignal<string>', isSignal: true}},
46+
outputs: {'valueChange': {type: 'OutputEmitter<string>'}},
47+
template: `<div dir [(value)]="bla">`,
48+
component: `bla: string = ''`,
49+
expected: [],
50+
},
3351
{
3452
id: 'complex output object',
3553
outputs: {'evt': {type: 'OutputEmitter<{works: boolean}>'}},

packages/compiler-cli/test/initializer_api_transforms_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {MockAotContext, MockCompilerHost} from './mocks';
1616
const TEST_FILE_INPUT = '/test.ts';
1717
const TEST_FILE_OUTPUT = `/test.js`;
1818

19-
describe('signal inputs metadata transform', () => {
19+
describe('initializer API metadata transform', () => {
2020
let host: MockCompilerHost;
2121
let context: MockAotContext;
2222

packages/compiler-cli/test/ngtsc/authoring_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ runInEachFileSystem(() => {
342342
'{ "value": "valueChange"; }, never, never, false, never>;');
343343
});
344344

345-
it('should declare an input/output pair for a field initialized to an alised model()', () => {
345+
it('should declare an input/output pair for a field initialized to an aliased model()', () => {
346346
env.write('test.ts', `
347347
import {Directive, model} from '@angular/core';
348348

packages/core/src/authoring/model/model_signal.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export interface ModelSignal<T> extends WritableSignal<T> {
4242
[ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]: T;
4343

4444
/**
45-
* Subscribes to changes in the model's value.
45+
* Subscribes to changes in the model's value. Used by listener instructions at runtime.
4646
* @internal
4747
*/
4848
subscribe(callback: (value: T) => void): {unsubscribe: () => void};
@@ -56,6 +56,7 @@ export interface ModelSignal<T> extends WritableSignal<T> {
5656
* @param options Additional options for the model.
5757
*/
5858
export function createModelSignal<T>(initialValue: T): ModelSignal<T> {
59+
const subscriptions: ((value: T) => void)[] = [];
5960
const node: ModelSignalNode<T> = Object.create(MODEL_SIGNAL_NODE);
6061

6162
node.value = initialValue;
@@ -73,8 +74,6 @@ export function createModelSignal<T>(initialValue: T): ModelSignal<T> {
7374
}
7475

7576
function notifySubscribers(value: T): void {
76-
const subscriptions = node.subscriptions;
77-
7877
for (let i = 0; i < subscriptions.length; i++) {
7978
subscriptions[i](value);
8079
}
@@ -95,15 +94,15 @@ export function createModelSignal<T>(initialValue: T): ModelSignal<T> {
9594
};
9695

9796
getter.subscribe = (callback: (value: T) => void) => {
98-
node.subscriptions.push(callback);
97+
subscriptions.push(callback);
9998

10099
// TODO(crisbeto): figure out if we can get rid of the object literal.
101100
return {
102101
unsubscribe: () => {
103-
const index = node.subscriptions.indexOf(callback);
102+
const index = subscriptions.indexOf(callback);
104103

105104
if (index > -1) {
106-
node.subscriptions.splice(index, 1);
105+
subscriptions.splice(index, 1);
107106
}
108107
}
109108
};

packages/core/src/authoring/model/model_signal_node.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ export const REQUIRED_UNSET_VALUE = /* @__PURE__ */ Symbol('ModelSignalNode#UNSE
1515
* signals by adding the ability to track subscriptions and to be required.
1616
*/
1717
export interface ModelSignalNode<T> extends SignalNode<T|typeof REQUIRED_UNSET_VALUE> {
18-
/** Currently-active subscriptions on the model. */
19-
subscriptions: ((value: T) => void)[];
20-
2118
/** Used by the runtime to write a value to the signal input. */
2219
applyValueToInputSignal: (node: ModelSignalNode<T>, value: T) => void;
2320
}
@@ -28,7 +25,6 @@ export interface ModelSignalNode<T> extends SignalNode<T|typeof REQUIRED_UNSET_V
2825
export const MODEL_SIGNAL_NODE: ModelSignalNode<unknown> = /* @__PURE__ */ (() => {
2926
return {
3027
...SIGNAL_NODE,
31-
subscriptions: [],
3228

3329
// TODO(crisbeto): figure out how to avoid this.
3430
// Maybe set an input flag that the value is writeable?

packages/core/test/acceptance/authoring/model_inputs_spec.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ describe('model inputs', () => {
3636

3737
// Changing the value from within the directive.
3838
host.dir.value.set(2);
39-
fixture.detectChanges();
4039
expect(host.value()).toBe(2);
4140
expect(host.dir.value()).toBe(2);
4241

@@ -73,7 +72,6 @@ describe('model inputs', () => {
7372

7473
// Changing the value from within the directive.
7574
host.dir.value.set(2);
76-
fixture.detectChanges();
7775
expect(host.value).toBe(2);
7876
expect(host.dir.value()).toBe(2);
7977

@@ -456,7 +454,7 @@ describe('model inputs', () => {
456454
expect(host.dir.value()).toBe(3);
457455
});
458456

459-
it('should allow a two-way-bound signal to be bound in the template', () => {
457+
it('should reflect changes to a two-way-bound signal in the DOM', () => {
460458
@Directive({
461459
selector: '[dir]',
462460
standalone: true,
@@ -501,9 +499,9 @@ describe('model inputs', () => {
501499
class Dir implements OnChanges {
502500
value = model(0);
503501

504-
ngOnChanges(allCahnges: SimpleChanges): void {
505-
if (allCahnges['value']) {
506-
changes.push(allCahnges['value']);
502+
ngOnChanges(allChanges: SimpleChanges): void {
503+
if (allChanges['value']) {
504+
changes.push(allChanges['value']);
507505
}
508506
}
509507
}

packages/core/test/authoring/model_input_spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,29 @@ describe('model signal', () => {
8989
expect(values).toEqual([1, 2]);
9090
});
9191

92+
it('should not share subscriptions between models', () => {
93+
let emitCount = 0;
94+
const signal1 = model(0) as SubscribableSignal<number>;
95+
const signal2 = model(0) as SubscribableSignal<number>;
96+
const callback = () => emitCount++;
97+
const subscription1 = signal1.subscribe(callback);
98+
const subscription2 = signal2.subscribe(callback);
99+
100+
signal1.set(1);
101+
expect(emitCount).toBe(1);
102+
103+
signal2.set(1);
104+
expect(emitCount).toBe(2);
105+
106+
subscription1.unsubscribe();
107+
signal2.set(2);
108+
expect(emitCount).toBe(3);
109+
110+
subscription2.unsubscribe();
111+
signal2.set(3);
112+
expect(emitCount).toBe(3);
113+
});
114+
92115
it('should throw if there is no value for required model', () => {
93116
const signal = model.required();
94117

0 commit comments

Comments
 (0)