@@ -7,6 +7,8 @@ import { createRef, debounce, invariant } from '@algolia/autocomplete-shared';
77
88import { createAutocompleteDom } from './createAutocompleteDom' ;
99import { createEffectWrapper } from './createEffectWrapper' ;
10+ import { createReactiveWrapper } from './createReactiveWrapper' ;
11+ import { getDefaultOptions } from './getDefaultOptions' ;
1012import { getPanelPositionStyle } from './getPanelPositionStyle' ;
1113import { render } from './render' ;
1214import {
@@ -15,99 +17,85 @@ import {
1517 AutocompletePropGetters ,
1618 AutocompleteState ,
1719} from './types' ;
18- import { getHTMLElement , setProperties } from './utils' ;
20+ import { getHTMLElement , mergeDeep , setProperties } from './utils' ;
1921
20- function defaultRenderer ( { root , sections } ) {
21- for ( const section of sections ) {
22- root . appendChild ( section ) ;
23- }
24- }
22+ export function autocomplete < TItem extends BaseItem > (
23+ options : AutocompleteOptions < TItem >
24+ ) : AutocompleteApi < TItem > {
25+ const { runEffect , cleanupEffects , runEffects } = createEffectWrapper ( ) ;
26+ const { reactive , runReactives } = createReactiveWrapper ( ) ;
2527
26- export function autocomplete < TItem extends BaseItem > ( {
27- container,
28- panelContainer = document . body ,
29- render : renderer = defaultRenderer ,
30- panelPlacement = 'input-wrapper-width' ,
31- classNames = { } ,
32- getEnvironmentProps = ( { props } ) => props ,
33- getFormProps = ( { props } ) => props ,
34- getInputProps = ( { props } ) => props ,
35- getItemProps = ( { props } ) => props ,
36- getLabelProps = ( { props } ) => props ,
37- getListProps = ( { props } ) => props ,
38- getPanelProps = ( { props } ) => props ,
39- getRootProps = ( { props } ) => props ,
40- ...props
41- } : AutocompleteOptions < TItem > ) : AutocompleteApi < TItem > {
42- const { runEffect, cleanupEffects } = createEffectWrapper ( ) ;
28+ const optionsRef = createRef ( options ) ;
4329 const onStateChangeRef = createRef <
44- | ( ( params : {
45- state : AutocompleteState < TItem > ;
46- prevState : AutocompleteState < TItem > ;
47- } ) => void )
48- | undefined
30+ AutocompleteOptions < TItem > [ 'onStateChange' ]
4931 > ( undefined ) ;
50- const autocomplete = createAutocomplete < TItem > ( {
51- ...props ,
52- onStateChange ( options ) {
53- onStateChangeRef . current ?.( options as any ) ;
54- props . onStateChange ?.( options ) ;
55- } ,
56- } ) ;
57- const initialState : AutocompleteState < TItem > = {
32+ const props = reactive ( ( ) => getDefaultOptions ( optionsRef . current ) ) ;
33+ const autocomplete = reactive ( ( ) =>
34+ createAutocomplete < TItem > ( {
35+ ...props . current . core ,
36+ onStateChange ( options ) {
37+ onStateChangeRef . current ?.( options as any ) ;
38+ props . current . core . onStateChange ?.( options as any ) ;
39+ } ,
40+ } )
41+ ) ;
42+ const lastStateRef = createRef < AutocompleteState < TItem > > ( {
5843 collections : [ ] ,
5944 completion : null ,
6045 context : { } ,
6146 isOpen : false ,
6247 query : '' ,
6348 selectedItemId : null ,
6449 status : 'idle' ,
65- ...props . initialState ,
66- } ;
50+ ...props . current . core . initialState ,
51+ } ) ;
6752
6853 const propGetters : AutocompletePropGetters < TItem > = {
69- getEnvironmentProps,
70- getFormProps,
71- getInputProps,
72- getItemProps,
73- getLabelProps,
74- getListProps,
75- getPanelProps,
76- getRootProps,
54+ getEnvironmentProps : props . current . renderer . getEnvironmentProps ,
55+ getFormProps : props . current . renderer . getFormProps ,
56+ getInputProps : props . current . renderer . getInputProps ,
57+ getItemProps : props . current . renderer . getItemProps ,
58+ getLabelProps : props . current . renderer . getLabelProps ,
59+ getListProps : props . current . renderer . getListProps ,
60+ getPanelProps : props . current . renderer . getPanelProps ,
61+ getRootProps : props . current . renderer . getRootProps ,
7762 } ;
7863 const autocompleteScopeApi : AutocompleteScopeApi < TItem > = {
79- setSelectedItemId : autocomplete . setSelectedItemId ,
80- setQuery : autocomplete . setQuery ,
81- setCollections : autocomplete . setCollections ,
82- setIsOpen : autocomplete . setIsOpen ,
83- setStatus : autocomplete . setStatus ,
84- setContext : autocomplete . setContext ,
85- refresh : autocomplete . refresh ,
64+ setSelectedItemId : autocomplete . current . setSelectedItemId ,
65+ setQuery : autocomplete . current . setQuery ,
66+ setCollections : autocomplete . current . setCollections ,
67+ setIsOpen : autocomplete . current . setIsOpen ,
68+ setStatus : autocomplete . current . setStatus ,
69+ setContext : autocomplete . current . setContext ,
70+ refresh : autocomplete . current . refresh ,
8671 } ;
87- const dom = createAutocompleteDom ( {
88- state : initialState ,
89- autocomplete,
90- classNames,
91- propGetters,
92- autocompleteScopeApi,
93- } ) ;
72+
73+ const dom = reactive ( ( ) =>
74+ createAutocompleteDom ( {
75+ state : lastStateRef . current ,
76+ autocomplete : autocomplete . current ,
77+ classNames : props . current . renderer . classNames ,
78+ propGetters,
79+ autocompleteScopeApi,
80+ } )
81+ ) ;
9482
9583 function setPanelPosition ( ) {
96- setProperties ( dom . panel , {
84+ setProperties ( dom . current . panel , {
9785 style : getPanelPositionStyle ( {
98- panelPlacement,
99- container : dom . root ,
100- form : dom . form ,
101- environment : props . environment ,
86+ panelPlacement : props . current . renderer . panelPlacement ,
87+ container : dom . current . root ,
88+ form : dom . current . form ,
89+ environment : props . current . core . environment ,
10290 } ) ,
10391 } ) ;
10492 }
10593
10694 runEffect ( ( ) => {
107- const environmentProps = autocomplete . getEnvironmentProps ( {
108- formElement : dom . form ,
109- panelElement : dom . panel ,
110- inputElement : dom . input ,
95+ const environmentProps = autocomplete . current . getEnvironmentProps ( {
96+ formElement : dom . current . form ,
97+ panelElement : dom . current . panel ,
98+ inputElement : dom . current . input ,
11199 } ) ;
112100
113101 setProperties ( window as any , environmentProps ) ;
@@ -126,45 +114,54 @@ export function autocomplete<TItem extends BaseItem>({
126114 } ) ;
127115
128116 runEffect ( ( ) => {
129- const panelRoot = getHTMLElement ( panelContainer ) ;
130- render ( renderer , {
131- state : initialState ,
132- autocomplete,
117+ const containerElement = getHTMLElement ( props . current . renderer . container ) ;
118+ invariant (
119+ containerElement . tagName !== 'INPUT' ,
120+ 'The `container` option does not support `input` elements. You need to change the container to a `div`.'
121+ ) ;
122+ containerElement . appendChild ( dom . current . root ) ;
123+
124+ return ( ) => {
125+ containerElement . removeChild ( dom . current . root ) ;
126+ } ;
127+ } ) ;
128+
129+ runEffect ( ( ) => {
130+ const panelElement = getHTMLElement ( props . current . renderer . panelContainer ) ;
131+ render ( props . current . renderer . render , {
132+ state : lastStateRef . current ,
133+ autocomplete : autocomplete . current ,
133134 propGetters,
134- dom,
135- classNames,
136- panelRoot,
135+ dom : dom . current ,
136+ classNames : props . current . renderer . classNames ,
137+ panelRoot : panelElement ,
137138 autocompleteScopeApi,
138139 } ) ;
139140
140- return ( ) => { } ;
141+ return ( ) => {
142+ if ( panelElement . contains ( dom . current . panel ) ) {
143+ panelElement . removeChild ( dom . current . panel ) ;
144+ }
145+ } ;
141146 } ) ;
142147
143148 runEffect ( ( ) => {
144- const panelRoot = getHTMLElement ( panelContainer ) ;
145- const unmountRef = createRef < ( ( ) => void ) | undefined > ( undefined ) ;
146- // This batches state changes to limit DOM mutations.
147- // Every time we call a setter in `autocomplete-core` (e.g., in `onInput`),
148- // the core `onStateChange` function is called.
149- // We don't need to be notified of all these state changes to render.
150- // As an example:
151- // - without debouncing: "iphone case" query → 85 renders
152- // - with debouncing: "iphone case" query → 12 renders
153- const debouncedOnStateChange = debounce < {
149+ const debouncedRender = debounce < {
154150 state : AutocompleteState < TItem > ;
155151 } > ( ( { state } ) => {
156- unmountRef . current = render ( renderer , {
152+ lastStateRef . current = state ;
153+ render ( props . current . renderer . render , {
157154 state,
158- autocomplete,
155+ autocomplete : autocomplete . current ,
159156 propGetters,
160- dom,
161- classNames,
162- panelRoot,
157+ dom : dom . current ,
158+ classNames : props . current . renderer . classNames ,
159+ panelRoot : getHTMLElement ( props . current . renderer . panelContainer ) ,
163160 autocompleteScopeApi,
164161 } ) ;
165162 } , 0 ) ;
166163
167- onStateChangeRef . current = ( { prevState , state } ) => {
164+ onStateChangeRef . current = ( { state , prevState } ) => {
168165 // The outer DOM might have changed since the last time the panel was
169166 // positioned. The layout might have shifted vertically for instance.
170167 // It's therefore safer to re-calculate the panel position before opening
@@ -173,48 +170,64 @@ export function autocomplete<TItem extends BaseItem>({
173170 setPanelPosition ( ) ;
174171 }
175172
176- return debouncedOnStateChange ( { state } ) ;
173+ debouncedRender ( { state } ) ;
177174 } ;
178175
179176 return ( ) => {
180- unmountRef . current ?.( ) ;
181177 onStateChangeRef . current = undefined ;
182178 } ;
183179 } ) ;
184180
185- runEffect ( ( ) => {
186- const containerElement = getHTMLElement ( container ) ;
187- invariant (
188- containerElement . tagName !== 'INPUT' ,
189- 'The `container` option does not support `input` elements. You need to change the container to a `div`.'
190- ) ;
191- containerElement . appendChild ( dom . root ) ;
192-
193- return ( ) => {
194- containerElement . removeChild ( dom . root ) ;
195- } ;
196- } ) ;
197-
198181 runEffect ( ( ) => {
199182 const onResize = debounce < Event > ( ( ) => {
200183 setPanelPosition ( ) ;
201- } , 100 ) ;
202-
184+ } , 20 ) ;
203185 window . addEventListener ( 'resize' , onResize ) ;
204186
205187 return ( ) => {
206188 window . removeEventListener ( 'resize' , onResize ) ;
207189 } ;
208190 } ) ;
209191
210- requestAnimationFrame ( ( ) => {
211- setPanelPosition ( ) ;
192+ runEffect ( ( ) => {
193+ requestAnimationFrame ( setPanelPosition ) ;
194+
195+ return ( ) => { } ;
212196 } ) ;
213197
198+ function destroy ( ) {
199+ cleanupEffects ( ) ;
200+ }
201+
202+ function update ( updatedOptions : Partial < AutocompleteOptions < TItem > > = { } ) {
203+ cleanupEffects ( ) ;
204+
205+ optionsRef . current = mergeDeep (
206+ props . current . renderer ,
207+ props . current . core ,
208+ { initialState : lastStateRef . current } ,
209+ updatedOptions
210+ ) ;
211+
212+ runReactives ( ) ;
213+ runEffects ( ) ;
214+
215+ autocomplete . current . refresh ( ) . then ( ( ) => {
216+ render ( props . current . renderer . render , {
217+ state : lastStateRef . current ,
218+ autocomplete : autocomplete . current ,
219+ propGetters,
220+ dom : dom . current ,
221+ classNames : props . current . renderer . classNames ,
222+ panelRoot : getHTMLElement ( props . current . renderer . panelContainer ) ,
223+ autocompleteScopeApi,
224+ } ) ;
225+ } ) ;
226+ }
227+
214228 return {
215229 ...autocompleteScopeApi ,
216- destroy ( ) {
217- cleanupEffects ( ) ;
218- } ,
230+ update,
231+ destroy,
219232 } ;
220233}
0 commit comments