@@ -16,6 +16,7 @@ import {
1616 provideZonelessChangeDetection ,
1717 signal ,
1818 viewChild ,
19+ viewChildren ,
1920} from '@angular/core' ;
2021import { TestBed } from '@angular/core/testing' ;
2122import {
@@ -54,6 +55,252 @@ describe('control directive', () => {
5455 } ) ;
5556 } ) ;
5657
58+ describe ( 'properties' , ( ) => {
59+ describe ( 'disabled' , ( ) => {
60+ it ( 'native control' , ( ) => {
61+ @Component ( {
62+ imports : [ Control ] ,
63+ template : `<input [control]="f">` ,
64+ } )
65+ class TestCmp {
66+ readonly disabled = signal ( false ) ;
67+ readonly f = form ( signal ( false ) , ( p ) => {
68+ disabled ( p , this . disabled ) ;
69+ } ) ;
70+ }
71+
72+ const fixture = act ( ( ) => TestBed . createComponent ( TestCmp ) ) ;
73+ const input = fixture . nativeElement . firstChild ;
74+ expect ( input . disabled ) . toBe ( false ) ;
75+
76+ act ( ( ) => fixture . componentInstance . disabled . set ( true ) ) ;
77+ expect ( input . disabled ) . toBe ( true ) ;
78+ } ) ;
79+
80+ it ( 'custom control' , ( ) => {
81+ @Component ( { selector : 'custom-control' , template : `` } )
82+ class CustomControl {
83+ readonly value = model ( false ) ;
84+ readonly disabled = input ( false ) ;
85+ }
86+
87+ @Component ( {
88+ imports : [ Control , CustomControl ] ,
89+ template : `<custom-control [control]="f" />` ,
90+ } )
91+ class TestCmp {
92+ readonly disabled = signal ( false ) ;
93+ readonly f = form ( signal ( false ) , ( p ) => {
94+ disabled ( p , this . disabled ) ;
95+ } ) ;
96+ readonly customControl = viewChild . required ( CustomControl ) ;
97+ }
98+
99+ const fixture = act ( ( ) => TestBed . createComponent ( TestCmp ) ) ;
100+ const component = fixture . componentInstance ;
101+ expect ( component . customControl ( ) . disabled ( ) ) . toBe ( false ) ;
102+
103+ act ( ( ) => component . disabled . set ( true ) ) ;
104+ expect ( component . customControl ( ) . disabled ( ) ) . toBe ( true ) ;
105+ } ) ;
106+ } ) ;
107+
108+ describe ( 'name' , ( ) => {
109+ it ( 'native control' , ( ) => {
110+ @Component ( {
111+ imports : [ Control ] ,
112+ template : `
113+ @for (item of f; track item) {
114+ <input #control [control]="item">
115+ <span>{{item().value()}}</span>
116+ }
117+ ` ,
118+ } )
119+ class TestCmp {
120+ readonly f = form ( signal ( [ 'a' , 'b' ] ) , { name : 'root' } ) ;
121+ readonly controls = viewChildren < ElementRef < HTMLInputElement > > ( 'control' ) ;
122+ }
123+
124+ const fixture = act ( ( ) => TestBed . createComponent ( TestCmp ) ) ;
125+ const component = fixture . componentInstance ;
126+ const controlA = component . controls ( ) [ 0 ] . nativeElement ;
127+ const controlB = component . controls ( ) [ 1 ] . nativeElement ;
128+ expect ( controlA . value ) . toBe ( 'a' ) ;
129+ expect ( controlB . value ) . toBe ( 'b' ) ;
130+ expect ( controlA . name ) . toBe ( 'root.0' ) ;
131+ expect ( controlB . name ) . toBe ( 'root.1' ) ;
132+ expect ( fixture . nativeElement . innerText ) . toBe ( 'ab' ) ;
133+
134+ act ( ( ) => component . f ( ) . value . update ( ( items ) => [ items [ 1 ] , items [ 0 ] ] ) ) ;
135+
136+ // @for should not recreate views when swapped.
137+ expect ( controlA . isConnected ) . toBeTrue ( ) ;
138+ expect ( controlB . isConnected ) . toBeTrue ( ) ;
139+
140+ pending ( 'TODO: https://github.com/angular/angular/issues/63882' ) ;
141+
142+ // Controls should retain their value.
143+ expect ( controlA . value ) . toBe ( 'a' ) ;
144+ expect ( controlB . value ) . toBe ( 'b' ) ;
145+
146+ // Controls should have new names to reflect their new position.
147+ expect ( controlA . name ) . toBe ( 'root.1' ) ;
148+ expect ( controlB . name ) . toBe ( 'root.0' ) ;
149+
150+ // DOM order of controls should be swapped.
151+ expect ( fixture . nativeElement . innerText ) . toBe ( 'ba' ) ;
152+ } ) ;
153+
154+ it ( 'custom control' , ( ) => {
155+ @Component ( { selector : 'custom-control' , template : `{{value()}}` } )
156+ class CustomControl {
157+ readonly value = model ( '' ) ;
158+ readonly name = input ( '' ) ;
159+ }
160+
161+ @Component ( {
162+ imports : [ Control , CustomControl ] ,
163+ template : `
164+ @for (item of f; track item) {
165+ <custom-control [control]="item" />
166+ }
167+ ` ,
168+ } )
169+ class TestCmp {
170+ readonly f = form ( signal ( [ 'a' , 'b' ] ) , { name : 'root' } ) ;
171+ readonly controls = viewChildren ( CustomControl ) ;
172+ }
173+
174+ const fixture = act ( ( ) => TestBed . createComponent ( TestCmp ) ) ;
175+ const component = fixture . componentInstance ;
176+ const controlA = component . controls ( ) [ 0 ] ;
177+ const controlB = component . controls ( ) [ 1 ] ;
178+ expect ( controlA . value ( ) ) . toBe ( 'a' ) ;
179+ expect ( controlB . value ( ) ) . toBe ( 'b' ) ;
180+ expect ( controlA . name ( ) ) . toBe ( 'root.0' ) ;
181+ expect ( controlB . name ( ) ) . toBe ( 'root.1' ) ;
182+ expect ( fixture . nativeElement . innerText ) . toBe ( 'ab' ) ;
183+
184+ act ( ( ) => component . f ( ) . value . update ( ( items ) => [ items [ 1 ] , items [ 0 ] ] ) ) ;
185+
186+ // @for should not recreate views when swapped.
187+ expect ( component . controls ( ) ) . toContain ( controlA ) ;
188+ expect ( component . controls ( ) ) . toContain ( controlB ) ;
189+
190+ pending ( 'TODO: https://github.com/angular/angular/issues/63882' ) ;
191+
192+ // Controls should retain their values.
193+ expect ( controlA . value ( ) ) . toBe ( 'a' ) ;
194+ expect ( controlB . value ( ) ) . toBe ( 'b' ) ;
195+
196+ // Controls should have new names to reflect their new position.
197+ expect ( controlA . name ( ) ) . toBe ( 'root.1' ) ;
198+ expect ( controlB . name ( ) ) . toBe ( 'root.0' ) ;
199+
200+ // DOM order of controls should be swapped.
201+ expect ( fixture . nativeElement . innerText ) . toBe ( 'ba' ) ;
202+ } ) ;
203+ } ) ;
204+
205+ describe ( 'readonly' , ( ) => {
206+ it ( 'native control' , ( ) => {
207+ @Component ( {
208+ imports : [ Control ] ,
209+ template : `<input [control]="f">` ,
210+ } )
211+ class TestCmp {
212+ readonly readonly = signal ( true ) ;
213+ readonly f = form ( signal ( '' ) , ( p ) => {
214+ readonly ( p , this . readonly ) ;
215+ } ) ;
216+ }
217+
218+ const fixture = act ( ( ) => TestBed . createComponent ( TestCmp ) ) ;
219+ const element = fixture . nativeElement . firstChild as HTMLInputElement ;
220+ expect ( element . readOnly ) . toBe ( true ) ;
221+
222+ act ( ( ) => fixture . componentInstance . readonly . set ( false ) ) ;
223+ expect ( element . readOnly ) . toBe ( false ) ;
224+ } ) ;
225+
226+ it ( 'custom control' , ( ) => {
227+ @Component ( { selector : 'custom-control' , template : `` } )
228+ class CustomControl {
229+ readonly value = model ( '' ) ;
230+ readonly readonly = input ( false ) ;
231+ }
232+
233+ @Component ( {
234+ imports : [ Control , CustomControl ] ,
235+ template : `<custom-control [control]="f" />` ,
236+ } )
237+ class TestCmp {
238+ readonly readonly = signal ( false ) ;
239+ readonly f = form ( signal ( '' ) , ( p ) => {
240+ readonly ( p , this . readonly ) ;
241+ } ) ;
242+ readonly child = viewChild . required ( CustomControl ) ;
243+ }
244+
245+ const fixture = act ( ( ) => TestBed . createComponent ( TestCmp ) ) ;
246+ const component = fixture . componentInstance ;
247+ expect ( component . child ( ) . readonly ( ) ) . toBe ( false ) ;
248+
249+ act ( ( ) => component . readonly . set ( true ) ) ;
250+ expect ( component . child ( ) . readonly ( ) ) . toBe ( true ) ;
251+ } ) ;
252+ } ) ;
253+
254+ describe ( 'required' , ( ) => {
255+ it ( 'native control' , ( ) => {
256+ @Component ( {
257+ imports : [ Control ] ,
258+ template : `<input [control]="f">` ,
259+ } )
260+ class TestCmp {
261+ readonly required = signal ( false ) ;
262+ readonly f = form ( signal ( '' ) , ( p ) => {
263+ required ( p , { when : this . required } ) ;
264+ } ) ;
265+ }
266+
267+ const fixture = act ( ( ) => TestBed . createComponent ( TestCmp ) ) ;
268+ const element = fixture . nativeElement . firstChild ;
269+ expect ( element . required ) . toBe ( false ) ;
270+
271+ act ( ( ) => fixture . componentInstance . required . set ( true ) ) ;
272+ expect ( element . required ) . toBe ( true ) ;
273+ } ) ;
274+
275+ it ( 'custom control' , ( ) => {
276+ @Component ( { selector : 'custom-control' , template : `` } )
277+ class CustomControl {
278+ readonly value = model ( '' ) ;
279+ readonly required = input ( false ) ;
280+ }
281+
282+ @Component ( {
283+ imports : [ Control , CustomControl ] ,
284+ template : `<custom-control [control]="f" />` ,
285+ } )
286+ class TestCmp {
287+ readonly required = signal ( false ) ;
288+ readonly f = form ( signal ( '' ) , ( p ) => {
289+ required ( p , { when : this . required } ) ;
290+ } ) ;
291+ readonly customControl = viewChild . required ( CustomControl ) ;
292+ }
293+
294+ const fixture = act ( ( ) => TestBed . createComponent ( TestCmp ) ) ;
295+ const component = fixture . componentInstance ;
296+ expect ( component . customControl ( ) . required ( ) ) . toBe ( false ) ;
297+
298+ act ( ( ) => component . required . set ( true ) ) ;
299+ expect ( component . customControl ( ) . required ( ) ) . toBe ( true ) ;
300+ } ) ;
301+ } ) ;
302+ } ) ;
303+
57304 it ( 'synchronizes a basic form with a custom control' , ( ) => {
58305 @Component ( {
59306 imports : [ Control ] ,
0 commit comments