@@ -66,19 +66,36 @@ export interface ManagerOptionsParts<F extends FullField, O extends FullOperator
6666 readonly structuralOptions : ( ) => Record < string , unknown > ;
6767}
6868
69+ /**
70+ * Every function prop forwarded to the manager through a `live()` closure. Also drives the
71+ * presence flags in {@link ManagerOptionsParts.structuralOptions}, so adding or removing any of
72+ * them reconfigures the manager rather than leaving a stale (or missing) wrapper installed.
73+ */
74+ const forwardedFnProps = [
75+ 'getDefaultField' ,
76+ 'getDefaultOperator' ,
77+ 'getDefaultValue' ,
78+ 'getOperators' ,
79+ 'getValueEditorType' ,
80+ 'getValues' ,
81+ 'getValueSources' ,
82+ 'getMatchModes' ,
83+ 'getParameters' ,
84+ 'getInputType' ,
85+ 'getSubQueryBuilderProps' ,
86+ ] as const ;
87+
88+ type ForwardedFnProp = ( typeof forwardedFnProps ) [ number ] ;
89+
6990/**
7091 * Derives everything the {@link QueryManager} is configured with from props and the merged config.
7192 *
7293 * @param getProps - Reads the current props.
7394 * @param config - The merged `QueryBuilder` config.
74- * @param initialProps - The props as read once, untracked, at initialization. Used only to decide
75- * which function props were supplied, which fixes the manager's precedence rules for the lifetime
76- * of the manager.
7795 */
7896export const createManagerOptions = < F extends FullField , O extends FullOperator > (
7997 getProps : Accessor < QueryBuilderProps < RuleGroupTypeAny , F , O , FullCombinator > > ,
80- config : Accessor < MergedQueryBuilderConfig < F , GetOptionIdentifierType < O > > > ,
81- initialProps : QueryBuilderProps < RuleGroupTypeAny , F , O , FullCombinator >
98+ config : Accessor < MergedQueryBuilderConfig < F , GetOptionIdentifierType < O > > >
8299) : ManagerOptionsParts < F , O > => {
83100 // A plain closure: it returns a primitive, so there is no identity to stabilize.
84101 const maxLevels = ( ) : number =>
@@ -91,16 +108,23 @@ export const createManagerOptions = <F extends FullField, O extends FullOperator
91108
92109 /**
93110 * Forwards a function prop to the manager through a closure, so later changes to the prop take
94- * effect without rebuilding the manager. Returns `undefined` when the prop is absent at
95- * initialization, leaving the manager to apply its own precedence rules instead of treating
96- * the option as configured.
111+ * effect without rebuilding the manager. Returns `undefined` when the prop is absent *now*,
112+ * leaving the manager to apply its own precedence rules instead of treating the option as
113+ * configured.
114+ *
115+ * Presence is read from current props, not `initialProps`: a callback that is later removed
116+ * would otherwise leave a wrapper calling `undefined`, and one later supplied would never
117+ * reach the manager. `forwardedFnProps` puts every presence flag in the structural signature,
118+ * so add/remove transitions reconfigure and this is re-evaluated. The wrapper still re-checks
119+ * at call time, since a swap-to-absent is only visible after that reconfigure lands.
97120 */
98- const live = < A extends unknown [ ] , R > (
99- pick : ( props : QueryBuilderProps < RuleGroupTypeAny , F , O , FullCombinator > ) => unknown
100- ) : ( ( ...args : A ) => R ) | undefined =>
101- typeof pick ( initialProps ) === 'function'
102- ? ( ...args : A ) => ( pick ( getProps ( ) ) as ( ...args : A ) => R ) ( ...args )
103- : undefined ;
121+ const live = < A extends unknown [ ] , R > ( key : ForwardedFnProp ) : ( ( ...args : A ) => R ) | undefined => {
122+ if ( typeof getProps ( ) [ key ] !== 'function' ) return undefined ;
123+ return ( ...args : A ) => {
124+ const fn = getProps ( ) [ key ] as unknown ;
125+ return typeof fn === 'function' ? ( fn as ( ...args : A ) => R ) ( ...args ) : ( undefined as R ) ;
126+ } ;
127+ } ;
104128
105129 /**
106130 * Builds the full option set for the manager. Used both for construction and for every
@@ -137,22 +161,20 @@ export const createManagerOptions = <F extends FullField, O extends FullOperator
137161 history : true ,
138162 validator : p . validator ,
139163 idGenerator : p . idGenerator ,
140- // Forwarded so that changes to these props take effect without a reconfigure.
141- getDefaultField : ( typeof initialProps . getDefaultField === 'function'
142- ? live ( pp => pp . getDefaultField )
143- : p . getDefaultField ) as never ,
144- getDefaultOperator : ( typeof initialProps . getDefaultOperator === 'function'
145- ? live ( pp => pp . getDefaultOperator )
146- : p . getDefaultOperator ) as never ,
147- getDefaultValue : live ( pp => pp . getDefaultValue ) as never ,
148- getOperators : live ( pp => pp . getOperators ) as never ,
149- getValueEditorType : live ( pp => pp . getValueEditorType ) as never ,
150- getValues : live ( pp => pp . getValues ) as never ,
151- getValueSources : live ( pp => pp . getValueSources ) as never ,
152- getMatchModes : live ( pp => pp . getMatchModes ) as never ,
153- getParameters : live ( pp => pp . getParameters ) as never ,
154- getInputType : live ( pp => pp . getInputType ) as never ,
155- getSubQueryBuilderProps : live ( pp => pp . getSubQueryBuilderProps ) as never ,
164+ // Forwarded so that changes to these props take effect without a reconfigure. `live` returns
165+ // `undefined` for a non-function prop, so the two that also accept a plain name fall back
166+ // to the raw value.
167+ getDefaultField : ( live ( 'getDefaultField' ) ?? p . getDefaultField ) as never ,
168+ getDefaultOperator : ( live ( 'getDefaultOperator' ) ?? p . getDefaultOperator ) as never ,
169+ getDefaultValue : live ( 'getDefaultValue' ) as never ,
170+ getOperators : live ( 'getOperators' ) as never ,
171+ getValueEditorType : live ( 'getValueEditorType' ) as never ,
172+ getValues : live ( 'getValues' ) as never ,
173+ getValueSources : live ( 'getValueSources' ) as never ,
174+ getMatchModes : live ( 'getMatchModes' ) as never ,
175+ getParameters : live ( 'getParameters' ) as never ,
176+ getInputType : live ( 'getInputType' ) as never ,
177+ getSubQueryBuilderProps : live ( 'getSubQueryBuilderProps' ) as never ,
156178 } ;
157179 } ;
158180
@@ -184,6 +206,14 @@ export const createManagerOptions = <F extends FullField, O extends FullOperator
184206 maxLevels : maxLevels ( ) ,
185207 disabledPaths : disabledPaths ( ) ,
186208 queryDisabled : p . disabled === true ,
209+ // Presence, not identity: a forwarded callback that appears or disappears changes what the
210+ // manager must be configured with (wrapper vs. `undefined`, i.e. its own precedence rules),
211+ // while a mere identity swap stays invisible to it and is picked up by the live closure. A
212+ // non-function value (`getDefaultField`/`getDefaultOperator` also take a plain name) is
213+ // forwarded as-is, so it is compared by value here instead.
214+ ...Object . fromEntries (
215+ forwardedFnProps . map ( k => [ `fn:${ k } ` , typeof p [ k ] === 'function' ? true : p [ k ] ] )
216+ ) ,
187217 } ;
188218 } ;
189219
0 commit comments