@@ -700,6 +700,257 @@ describe('createFormHook', () => {
700700 expect ( button ) . toBeInTheDocument ( )
701701 } )
702702
703+ describe ( 'extendForm' , ( ) => {
704+ it ( 'should include both parent and extended field components' , ( ) => {
705+ function ExtendedTextField ( { label } : { label : string } ) {
706+ const field = useFieldContext < string > ( )
707+ return (
708+ < label >
709+ < div > { label } </ div >
710+ < input
711+ data-testid = "extended-input"
712+ value = { field . state . value }
713+ onChange = { ( e ) => field . handleChange ( e . target . value ) }
714+ />
715+ </ label >
716+ )
717+ }
718+
719+ const { useAppForm : useExtendedForm } = createFormHook ( {
720+ fieldComponents : { TextField } ,
721+ formComponents : { SubscribeButton } ,
722+ fieldContext,
723+ formContext,
724+ } ) . extendForm ( {
725+ fieldComponents : { ExtendedTextField } ,
726+ } )
727+
728+ function Comp ( ) {
729+ const form = useExtendedForm ( {
730+ defaultValues : { firstName : 'John' , lastName : 'Doe' } ,
731+ } )
732+
733+ return (
734+ < >
735+ < form . AppField
736+ name = "firstName"
737+ children = { ( field ) => < field . TextField label = "First Name" /> }
738+ />
739+ < form . AppField
740+ name = "lastName"
741+ children = { ( field ) => (
742+ < field . ExtendedTextField label = "Last Name" />
743+ ) }
744+ />
745+ </ >
746+ )
747+ }
748+
749+ const { getByLabelText, getByTestId } = render ( < Comp /> )
750+ expect ( getByLabelText ( 'First Name' ) ) . toHaveValue ( 'John' )
751+ expect ( getByTestId ( 'extended-input' ) ) . toHaveValue ( 'Doe' )
752+ } )
753+
754+ it ( 'should include both parent and extended form components' , ( ) => {
755+ function ExtendedSubmit ( { label } : { label : string } ) {
756+ const form = useFormContext ( )
757+ return (
758+ < form . Subscribe selector = { ( state ) => state . isSubmitting } >
759+ { ( isSubmitting ) => (
760+ < button
761+ data-testid = "extended-submit"
762+ disabled = { isSubmitting }
763+ type = "button"
764+ >
765+ { label }
766+ </ button >
767+ ) }
768+ </ form . Subscribe >
769+ )
770+ }
771+
772+ const { useAppForm : useExtendedForm } = createFormHook ( {
773+ fieldComponents : { TextField } ,
774+ formComponents : { SubscribeButton } ,
775+ fieldContext,
776+ formContext,
777+ } ) . extendForm ( {
778+ formComponents : { ExtendedSubmit } ,
779+ } )
780+
781+ function Comp ( ) {
782+ const form = useExtendedForm ( {
783+ defaultValues : { firstName : 'John' } ,
784+ } )
785+
786+ return (
787+ < form . AppForm >
788+ < form . SubscribeButton label = "Submit" />
789+ < form . ExtendedSubmit label = "Extended Submit" />
790+ </ form . AppForm >
791+ )
792+ }
793+
794+ const { getByText, getByTestId } = render ( < Comp /> )
795+ expect ( getByText ( 'Submit' ) ) . toBeInTheDocument ( )
796+ expect ( getByTestId ( 'extended-submit' ) ) . toHaveTextContent (
797+ 'Extended Submit' ,
798+ )
799+ } )
800+
801+ it ( 'should support chaining multiple extendForm calls' , ( ) => {
802+ function FieldA ( { label } : { label : string } ) {
803+ const field = useFieldContext < string > ( )
804+ return (
805+ < label >
806+ < div > { label } </ div >
807+ < input
808+ value = { field . state . value }
809+ onChange = { ( e ) => field . handleChange ( e . target . value ) }
810+ />
811+ </ label >
812+ )
813+ }
814+
815+ function FieldB ( { label } : { label : string } ) {
816+ const field = useFieldContext < string > ( )
817+ return (
818+ < label >
819+ < div > { label } </ div >
820+ < input
821+ value = { field . state . value }
822+ onChange = { ( e ) => field . handleChange ( e . target . value ) }
823+ />
824+ </ label >
825+ )
826+ }
827+
828+ const base = createFormHook ( {
829+ fieldComponents : { TextField } ,
830+ formComponents : { } ,
831+ fieldContext,
832+ formContext,
833+ } )
834+
835+ const { useAppForm : useChainedForm } = base
836+ . extendForm ( { fieldComponents : { FieldA } } )
837+ . extendForm ( { fieldComponents : { FieldB } } )
838+
839+ function Comp ( ) {
840+ const form = useChainedForm ( {
841+ defaultValues : { a : 'valueA' , b : 'valueB' , c : 'valueC' } ,
842+ } )
843+
844+ return (
845+ < >
846+ < form . AppField
847+ name = "a"
848+ children = { ( field ) => < field . TextField label = "A" /> }
849+ />
850+ < form . AppField
851+ name = "b"
852+ children = { ( field ) => < field . FieldA label = "B" /> }
853+ />
854+ < form . AppField
855+ name = "c"
856+ children = { ( field ) => < field . FieldB label = "C" /> }
857+ />
858+ </ >
859+ )
860+ }
861+
862+ const { getByLabelText } = render ( < Comp /> )
863+ expect ( getByLabelText ( 'A' ) ) . toHaveValue ( 'valueA' )
864+ expect ( getByLabelText ( 'B' ) ) . toHaveValue ( 'valueB' )
865+ expect ( getByLabelText ( 'C' ) ) . toHaveValue ( 'valueC' )
866+ } )
867+
868+ it ( 'should work with withForm after extendForm' , ( ) => {
869+ function ExtendedTextField ( { label } : { label : string } ) {
870+ const field = useFieldContext < string > ( )
871+ return (
872+ < label >
873+ < div > { label } </ div >
874+ < input
875+ value = { field . state . value }
876+ onChange = { ( e ) => field . handleChange ( e . target . value ) }
877+ />
878+ </ label >
879+ )
880+ }
881+
882+ const { useAppForm : useExtendedForm , withForm : withExtendedForm } =
883+ createFormHook ( {
884+ fieldComponents : { TextField } ,
885+ formComponents : { SubscribeButton } ,
886+ fieldContext,
887+ formContext,
888+ } ) . extendForm ( {
889+ fieldComponents : { ExtendedTextField } ,
890+ } )
891+
892+ const ChildForm = withExtendedForm ( {
893+ defaultValues : { firstName : 'Jane' , lastName : 'Smith' } ,
894+ render : function Render ( { form } ) {
895+ return (
896+ < >
897+ < form . AppField
898+ name = "firstName"
899+ children = { ( field ) => < field . TextField label = "First Name" /> }
900+ />
901+ < form . AppField
902+ name = "lastName"
903+ children = { ( field ) => (
904+ < field . ExtendedTextField label = "Last Name" />
905+ ) }
906+ />
907+ </ >
908+ )
909+ } ,
910+ } )
911+
912+ function Parent ( ) {
913+ const form = useExtendedForm ( {
914+ defaultValues : { firstName : 'Jane' , lastName : 'Smith' } ,
915+ } )
916+ return < ChildForm form = { form } />
917+ }
918+
919+ const { getByLabelText } = render ( < Parent /> )
920+ expect ( getByLabelText ( 'First Name' ) ) . toHaveValue ( 'Jane' )
921+ expect ( getByLabelText ( 'Last Name' ) ) . toHaveValue ( 'Smith' )
922+ } )
923+
924+ it ( 'should return a new extendForm from the extended hook' , ( ) => {
925+ function ExtendedTextField ( { label } : { label : string } ) {
926+ const field = useFieldContext < string > ( )
927+ return (
928+ < label >
929+ < div > { label } </ div >
930+ < input
931+ value = { field . state . value }
932+ onChange = { ( e ) => field . handleChange ( e . target . value ) }
933+ />
934+ </ label >
935+ )
936+ }
937+
938+ const base = createFormHook ( {
939+ fieldComponents : { TextField } ,
940+ formComponents : { SubscribeButton } ,
941+ fieldContext,
942+ formContext,
943+ } )
944+
945+ const extended = base . extendForm ( {
946+ fieldComponents : { ExtendedTextField } ,
947+ } )
948+
949+ // extendForm should itself expose extendForm
950+ expect ( typeof extended . extendForm ) . toBe ( 'function' )
951+ } )
952+ } )
953+
703954 it ( 'should render FieldGroup Subscribe without selector (default identity)' , async ( ) => {
704955 const formOpts = formOptions ( {
705956 defaultValues : {
0 commit comments