|
6 | 6 | * found in the LICENSE file at https://angular.dev/license |
7 | 7 | */ |
8 | 8 |
|
9 | | -import {ApplicationRef, Injector, signal} from '@angular/core'; |
| 9 | +import {ApplicationRef, computed, Injector, linkedSignal, signal} from '@angular/core'; |
10 | 10 | import {TestBed} from '@angular/core/testing'; |
11 | 11 | import * as z from 'zod'; |
12 | 12 | import {form, schema, validateStandardSchema} from '../../../../public_api'; |
@@ -209,4 +209,204 @@ describe('standard schema integration', () => { |
209 | 209 |
|
210 | 210 | expect(f.age().errors()[0].message).toBe('Age must be non-negative'); |
211 | 211 | }); |
| 212 | + |
| 213 | + it('should support reactive schema using computed signal', () => { |
| 214 | + const minLength = signal(2); |
| 215 | + |
| 216 | + const zodSchema = computed(() => |
| 217 | + z.object({ |
| 218 | + first: z.string().min(minLength()), |
| 219 | + last: z.string().min(3), |
| 220 | + }), |
| 221 | + ); |
| 222 | + |
| 223 | + const nameForm = form( |
| 224 | + signal({first: 'A', last: 'B'}), |
| 225 | + (p) => { |
| 226 | + validateStandardSchema(p, () => zodSchema()); |
| 227 | + }, |
| 228 | + {injector: TestBed.inject(Injector)}, |
| 229 | + ); |
| 230 | + |
| 231 | + // Initially, first name should have error (length 1, min 2) |
| 232 | + expect(nameForm.first().errors()).toEqual([ |
| 233 | + jasmine.objectContaining({ |
| 234 | + kind: 'standardSchema', |
| 235 | + issue: jasmine.objectContaining({ |
| 236 | + message: jasmine.stringMatching(/Too small|String must contain at least 2 character/), |
| 237 | + }), |
| 238 | + }), |
| 239 | + ]); |
| 240 | + |
| 241 | + // Change minLength to 1, should remove error |
| 242 | + minLength.set(1); |
| 243 | + expect(nameForm.first().errors()).toEqual([]); |
| 244 | + |
| 245 | + // Change minLength to 3, should add error again |
| 246 | + minLength.set(3); |
| 247 | + expect(nameForm.first().errors()).toEqual([ |
| 248 | + jasmine.objectContaining({ |
| 249 | + kind: 'standardSchema', |
| 250 | + issue: jasmine.objectContaining({ |
| 251 | + message: jasmine.stringMatching(/Too small|String must contain at least 3 character/), |
| 252 | + }), |
| 253 | + }), |
| 254 | + ]); |
| 255 | + }); |
| 256 | + |
| 257 | + it('should support reactive schema using signal', () => { |
| 258 | + const minLength = signal(2); |
| 259 | + |
| 260 | + const nameForm = form( |
| 261 | + signal({first: 'A', last: 'B'}), |
| 262 | + (p) => { |
| 263 | + validateStandardSchema(p, () => |
| 264 | + z.object({ |
| 265 | + first: z.string().min(minLength()), |
| 266 | + last: z.string().min(3), |
| 267 | + }), |
| 268 | + ); |
| 269 | + }, |
| 270 | + {injector: TestBed.inject(Injector)}, |
| 271 | + ); |
| 272 | + |
| 273 | + // Initially, first name should have error (length 1, min 2) |
| 274 | + expect(nameForm.first().errors()).toEqual([ |
| 275 | + jasmine.objectContaining({ |
| 276 | + kind: 'standardSchema', |
| 277 | + issue: jasmine.objectContaining({ |
| 278 | + message: jasmine.stringMatching(/Too small|String must contain at least 2 character/), |
| 279 | + }), |
| 280 | + }), |
| 281 | + ]); |
| 282 | + |
| 283 | + // Change minLength to 1, should remove error |
| 284 | + minLength.set(1); |
| 285 | + expect(nameForm.first().errors()).toEqual([]); |
| 286 | + |
| 287 | + // Change minLength to 5, should add error again |
| 288 | + minLength.set(5); |
| 289 | + expect(nameForm.first().errors()).toEqual([ |
| 290 | + jasmine.objectContaining({ |
| 291 | + kind: 'standardSchema', |
| 292 | + issue: jasmine.objectContaining({ |
| 293 | + message: jasmine.stringMatching(/Too small|String must contain at least 5 character/), |
| 294 | + }), |
| 295 | + }), |
| 296 | + ]); |
| 297 | + }); |
| 298 | + |
| 299 | + it('should support reactive schema using linkedSignal', () => { |
| 300 | + const minFirstLength = signal(2); |
| 301 | + const minLastLength = linkedSignal(() => minFirstLength() + 1); |
| 302 | + |
| 303 | + const nameForm = form( |
| 304 | + signal({first: 'A', last: 'BB'}), |
| 305 | + (p) => { |
| 306 | + validateStandardSchema(p, () => |
| 307 | + z.object({ |
| 308 | + first: z.string().min(minFirstLength()), |
| 309 | + last: z.string().min(minLastLength()), |
| 310 | + }), |
| 311 | + ); |
| 312 | + }, |
| 313 | + {injector: TestBed.inject(Injector)}, |
| 314 | + ); |
| 315 | + |
| 316 | + // Initially, first needs 2 chars, last needs 3 chars (2+1) |
| 317 | + expect(nameForm.first().errors()).toEqual([ |
| 318 | + jasmine.objectContaining({ |
| 319 | + kind: 'standardSchema', |
| 320 | + issue: jasmine.objectContaining({ |
| 321 | + message: jasmine.stringMatching(/Too small|String must contain at least 2 character/), |
| 322 | + }), |
| 323 | + }), |
| 324 | + ]); |
| 325 | + expect(nameForm.last().errors()).toEqual([ |
| 326 | + jasmine.objectContaining({ |
| 327 | + kind: 'standardSchema', |
| 328 | + issue: jasmine.objectContaining({ |
| 329 | + message: jasmine.stringMatching(/Too small|String must contain at least 3 character/), |
| 330 | + }), |
| 331 | + }), |
| 332 | + ]); |
| 333 | + |
| 334 | + // Change minFirstLength to 1, linkedSignal auto-updates to 2 |
| 335 | + minFirstLength.set(1); |
| 336 | + expect(nameForm.first().errors()).toEqual([]); |
| 337 | + expect(nameForm.last().errors()).toEqual([]); |
| 338 | + |
| 339 | + // Change minFirstLength to 4, linkedSignal auto-updates to 5 |
| 340 | + minFirstLength.set(4); |
| 341 | + expect(nameForm.first().errors()).toEqual([ |
| 342 | + jasmine.objectContaining({ |
| 343 | + kind: 'standardSchema', |
| 344 | + issue: jasmine.objectContaining({ |
| 345 | + message: jasmine.stringMatching(/Too small|String must contain at least 4 character/), |
| 346 | + }), |
| 347 | + }), |
| 348 | + ]); |
| 349 | + expect(nameForm.last().errors()).toEqual([ |
| 350 | + jasmine.objectContaining({ |
| 351 | + kind: 'standardSchema', |
| 352 | + issue: jasmine.objectContaining({ |
| 353 | + message: jasmine.stringMatching(/Too small|String must contain at least 5 character/), |
| 354 | + }), |
| 355 | + }), |
| 356 | + ]); |
| 357 | + }); |
| 358 | + |
| 359 | + it('should support reactive schema using LogicFn with field context', () => { |
| 360 | + type FormModel = { |
| 361 | + type: 'email' | 'phone'; |
| 362 | + value: string; |
| 363 | + }; |
| 364 | + |
| 365 | + const model = signal<FormModel>({type: 'email', value: 'invalid'}); |
| 366 | + const nameForm = form( |
| 367 | + model, |
| 368 | + (p) => { |
| 369 | + validateStandardSchema(p, (ctx) => { |
| 370 | + const formValue = ctx.value(); |
| 371 | + if (formValue.type === 'email') { |
| 372 | + return z.object({ |
| 373 | + type: z.literal('email'), |
| 374 | + value: z.email(), |
| 375 | + }); |
| 376 | + } else { |
| 377 | + return z.object({ |
| 378 | + type: z.literal('phone'), |
| 379 | + value: z.string().regex(/^\d{3}-\d{3}-\d{4}$/), |
| 380 | + }); |
| 381 | + } |
| 382 | + }); |
| 383 | + }, |
| 384 | + {injector: TestBed.inject(Injector)}, |
| 385 | + ); |
| 386 | + |
| 387 | + // Initially type is 'email', value should have email error |
| 388 | + expect(nameForm.value().errors()).toEqual([ |
| 389 | + jasmine.objectContaining({ |
| 390 | + kind: 'standardSchema', |
| 391 | + issue: jasmine.objectContaining({ |
| 392 | + message: jasmine.stringMatching(/Invalid email/), |
| 393 | + }), |
| 394 | + }), |
| 395 | + ]); |
| 396 | + |
| 397 | + // Change to phone type, should validate as phone number |
| 398 | + model.set({type: 'phone', value: '123-456-7890'}); |
| 399 | + expect(nameForm.value().errors()).toEqual([]); |
| 400 | + |
| 401 | + // Invalid phone number |
| 402 | + model.set({type: 'phone', value: 'invalid-phone'}); |
| 403 | + expect(nameForm.value().errors()).toEqual([ |
| 404 | + jasmine.objectContaining({ |
| 405 | + kind: 'standardSchema', |
| 406 | + issue: jasmine.objectContaining({ |
| 407 | + message: jasmine.stringMatching(/Invalid/), |
| 408 | + }), |
| 409 | + }), |
| 410 | + ]); |
| 411 | + }); |
212 | 412 | }); |
0 commit comments