@@ -18,6 +18,7 @@ interface FieldConfig {
1818 defaultValue ?: unknown ;
1919 ref ?: HTMLInputElement ;
2020 type ?: string ;
21+ isCheckboxArray ?: boolean ;
2122}
2223
2324export interface ValidationRules {
@@ -418,42 +419,110 @@ export const form = (config: FormControlConfig & { id?: string } = {}) => {
418419 const isCheckbox = type === 'checkbox' ;
419420 const isFile = type === 'file' ;
420421
421- const defaultValue = this . values [ name ] ?? ( isCheckbox ? ( element ?. checked ?? false ) : '' ) ;
422+ // Check if a checkbox with this name is already registered (indicates multiple checkboxes)
423+ const isCheckboxArray = isCheckbox && this . fields [ name ] ?. type === 'checkbox' ;
424+
425+ let defaultValue : unknown ;
426+
427+ if ( isCheckboxArray ) {
428+ // Ensure array type for checkbox groups
429+ const currentValue = this . values [ name ] ;
430+ defaultValue = Array . isArray ( currentValue ) ? currentValue : [ ] ;
431+ } else if ( isCheckbox ) {
432+ defaultValue = this . values [ name ] ?? element ?. checked ?? false ;
433+ } else {
434+ defaultValue = this . values [ name ] ?? '' ;
435+ }
422436
423437 this . fields [ name ] = {
424438 name,
425439 rules,
426440 defaultValue,
427441 ref : element ,
428442 type,
443+ isCheckboxArray,
429444 } ;
430445
431- this . values [ name ] ??= defaultValue ;
446+ // Force upgrade value to array if a collision is detected
447+ if ( isCheckboxArray && ! Array . isArray ( this . values [ name ] ) ) {
448+ this . values [ name ] = defaultValue ;
449+ } else {
450+ this . values [ name ] ??= defaultValue ;
451+ }
432452
433- const valueExpression = isCheckbox ? '$event.target.checked' : '$event.target.value' ;
453+ const valueExpression = isCheckboxArray
454+ ? '$event.target.value'
455+ : isCheckbox
456+ ? '$event.target.checked'
457+ : '$event.target.value' ;
434458
435459 const bindings : Record < string , unknown > = {
436460 name,
437461 'x-ref' : name ,
438- ':aria-invalid' : `!!errors. ${ name } ` ,
462+ ':aria-invalid' : `!!errors[" ${ name } "] ` ,
439463 ':class' : `{
440- 'tutor-input-error': errors. ${ name } ,
441- 'tutor-input-touched': touchedFields. ${ name } ,
442- 'tutor-input-dirty': dirtyFields. ${ name }
464+ 'tutor-input-error': errors[" ${ name } "] ,
465+ 'tutor-input-touched': touchedFields[" ${ name } "] ,
466+ 'tutor-input-dirty': dirtyFields[" ${ name } "]
443467 }` ,
444468 } ;
445469
446470 if ( ! isFile ) {
447- bindings [ 'x-model' ] = `values.${ name } ` ;
448- bindings [ '@input' ] = `handleFieldInput('${ name } ', ${ valueExpression } )` ;
471+ bindings [ 'x-model' ] = `values["${ name } "]` ;
472+
473+ bindings [ '@input' ] = `handleFieldInput('${ name } ', ${ valueExpression } , $event.target)` ;
474+
449475 bindings [ '@blur' ] = `handleFieldBlur('${ name } ', ${ valueExpression } )` ;
450476 }
451477
452478 return bindings ;
453479 } ,
454480
455- handleFieldInput ( name : string , value : unknown ) : void {
481+ handleCheckboxArrayInput ( name : string , element ?: HTMLInputElement ) : void {
482+ const field = this . fields [ name ] ;
483+ const currentValue = this . values [ name ] as string [ ] ;
484+ const valueArray = Array . isArray ( currentValue ) ? [ ...currentValue ] : [ ] ;
485+
486+ // Use the passed element (from $event.target) or try to get from $refs
487+ const checkbox = element || ( ( this as unknown as AlpineComponent ) . $refs [ name ] as HTMLInputElement ) ;
488+
489+ if ( ! checkbox ) return ;
490+
491+ const checkboxValue = checkbox . value ;
492+ const isChecked = checkbox . checked ;
493+
494+ let newValue : string [ ] ;
495+ if ( isChecked ) {
496+ newValue = valueArray . includes ( checkboxValue ) ? valueArray : [ ...valueArray , checkboxValue ] ;
497+ } else {
498+ newValue = valueArray . filter ( ( v ) => v !== checkboxValue ) ;
499+ }
500+
501+ const defaultArray = Array . isArray ( field . defaultValue ) ? field . defaultValue : [ ] ;
502+ // Sort to compare content regardless of order
503+ const isActuallyChanged = JSON . stringify ( newValue . sort ( ) ) !== JSON . stringify ( defaultArray . sort ( ) ) ;
504+
505+ this . values [ name ] = newValue ;
506+ this . dirtyFields [ name ] = isActuallyChanged ;
507+
508+ const shouldValidate = this . config . mode === 'onChange' || this . touchedFields [ name ] ;
509+
510+ if ( shouldValidate ) {
511+ this . validateField ( name , newValue ) ;
512+ } else {
513+ this . dispatchStateChange ( ) ;
514+ }
515+ } ,
516+
517+ handleFieldInput ( name : string , value : unknown , element ?: HTMLInputElement ) : void {
456518 const field = this . fields [ name ] ;
519+
520+ if ( field ?. isCheckboxArray ) {
521+ this . handleCheckboxArrayInput ( name , element ) ;
522+ return ;
523+ }
524+
525+ // Original logic for non-checkbox-array fields
457526 const isNumber = field ?. rules ?. numberOnly ;
458527 const allowNegative = typeof isNumber === 'object' && isNumber . allowNegative ;
459528 const whole = typeof isNumber === 'object' && isNumber . whole ;
@@ -508,12 +577,22 @@ export const form = (config: FormControlConfig & { id?: string } = {}) => {
508577 if ( shouldTouch ) this . touchedFields [ name ] = true ;
509578 if ( shouldDirty ) {
510579 const field = this . fields [ name ] ;
511- this . dirtyFields [ name ] = String ( value ) !== String ( field ?. defaultValue ?? '' ) ;
580+ // Handle array comparison for checkbox arrays
581+ if ( Array . isArray ( value ) && Array . isArray ( field ?. defaultValue ) ) {
582+ this . dirtyFields [ name ] = JSON . stringify ( value . sort ( ) ) !== JSON . stringify ( field . defaultValue . sort ( ) ) ;
583+ } else {
584+ this . dirtyFields [ name ] = String ( value ) !== String ( field ?. defaultValue ?? '' ) ;
585+ }
512586 }
513587
514588 const fieldElement = this . fields [ name ] ?. ref ;
515589 if ( fieldElement && this . fields [ name ] . type !== 'file' ) {
516- DOMUtils . updateElementValue ( fieldElement , value ) ;
590+ // For checkbox arrays, we need to update all checkboxes with this name
591+ if ( Array . isArray ( value ) && fieldElement . type === 'checkbox' ) {
592+ this . syncCheckboxArray ( name , value as string [ ] ) ;
593+ } else {
594+ DOMUtils . updateElementValue ( fieldElement , value ) ;
595+ }
517596 }
518597
519598 if ( shouldValidate ) {
@@ -814,11 +893,29 @@ export const form = (config: FormControlConfig & { id?: string } = {}) => {
814893 for ( const [ name , value ] of Object . entries ( this . values ) ) {
815894 const fieldRef = this . fields [ name ] ?. ref ;
816895 if ( fieldRef ) {
817- DOMUtils . updateElementValue ( fieldRef , value ) ;
896+ // Handle checkbox arrays specially
897+ if ( Array . isArray ( value ) && fieldRef . type === 'checkbox' ) {
898+ this . syncCheckboxArray ( name , value as string [ ] ) ;
899+ } else {
900+ DOMUtils . updateElementValue ( fieldRef , value ) ;
901+ }
818902 }
819903 }
820904 } ,
821905
906+ syncCheckboxArray ( name : string , values : string [ ] ) : void {
907+ const component = this as unknown as AlpineComponent ;
908+ const formElement = component . $el . closest ( 'form' ) || component . $el . parentElement ;
909+ const checkboxes = formElement ?. querySelectorAll ( `input[type="checkbox"][name="${ name } "]` ) ;
910+
911+ if ( checkboxes ) {
912+ checkboxes . forEach ( ( checkbox ) => {
913+ const input = checkbox as HTMLInputElement ;
914+ input . checked = values . includes ( input . value ) ;
915+ } ) ;
916+ }
917+ } ,
918+
822919 clearAllState ( ) : void {
823920 this . fields = { } ;
824921 this . values = { } ;
0 commit comments