1
- import { InputHTMLAttributes , useCallback , useEffect , useRef , useState } from 'react'
2
- import { Entity } from '@dcl/ecs'
1
+ import { InputHTMLAttributes , useCallback , useEffect , useMemo , useRef , useState } from 'react'
2
+ import { CrdtMessageType , Entity } from '@dcl/ecs'
3
+ import { recursiveCheck as hasDiff } from 'jest-matcher-deep-close-to/lib/recursiveCheck'
3
4
import { getValue , NestedKey , setValue } from '../../lib/logic/get-set-value'
4
5
import { Component } from '../../lib/sdk/components'
5
- import { useComponentValue } from './useComponentValue'
6
+ import { getComponentValue , isLastWriteWinComponent , useComponentValue } from './useComponentValue'
7
+ import { useSdk } from './useSdk'
8
+ import { useChange } from './useChange'
6
9
7
10
type Input = {
8
11
[ key : string ] : boolean | string | string [ ] | any [ ] | Record < string , boolean | string | string [ ] | any [ ] | Input >
@@ -15,6 +18,9 @@ export function isValidNumericInput(input: Input[keyof Input]): boolean {
15
18
if ( typeof input === 'boolean' ) {
16
19
return ! ! input
17
20
}
21
+ if ( typeof input === 'number' ) {
22
+ return ! isNaN ( input )
23
+ }
18
24
return input . length > 0 && ! isNaN ( Number ( input ) )
19
25
}
20
26
@@ -70,10 +76,8 @@ export const useComponentInput = <ComponentValueType extends object, InputType e
70
76
if ( skipSyncRef . current ) return
71
77
if ( validate ( input ) ) {
72
78
const newComponentValue = { ...componentValue , ...fromInputToComponentValue ( input ) }
79
+ if ( isEqual ( newComponentValue ) ) return
73
80
74
- if ( isEqual ( newComponentValue ) ) {
75
- return
76
- }
77
81
setComponentValue ( newComponentValue )
78
82
}
79
83
} , [ input ] )
@@ -115,3 +119,170 @@ export const useComponentInput = <ComponentValueType extends object, InputType e
115
119
116
120
return { getInputProps : getProps , isValid }
117
121
}
122
+
123
+ // Helper function to recursively merge values
124
+ const mergeValues = ( values : any [ ] ) : any => {
125
+ // Base case - if any value is not an object, compare directly
126
+ if ( ! values . every ( ( val ) => val && typeof val === 'object' ) ) {
127
+ return values . every ( ( val ) => val === values [ 0 ] ) ? values [ 0 ] : '--'
128
+ }
129
+
130
+ // Get all keys from all objects
131
+ const allKeys = [ ...new Set ( values . flatMap ( Object . keys ) ) ]
132
+
133
+ // Create result object
134
+ const result : any = { }
135
+
136
+ // For each key, recursively merge values
137
+ for ( const key of allKeys ) {
138
+ const valuesForKey = values . map ( ( obj ) => obj [ key ] )
139
+ result [ key ] = mergeValues ( valuesForKey )
140
+ }
141
+
142
+ return result
143
+ }
144
+
145
+ const mergeComponentValues = < ComponentValueType extends object , InputType extends Input > (
146
+ values : ComponentValueType [ ] ,
147
+ fromComponentValueToInput : ( componentValue : ComponentValueType ) => InputType
148
+ ) : InputType => {
149
+ // Transform all component values to input format
150
+ const inputs = values . map ( fromComponentValueToInput )
151
+
152
+ // Get first input as reference
153
+ const firstInput = inputs [ 0 ]
154
+
155
+ // Create result object with same shape as first input
156
+ const result = { } as InputType
157
+
158
+ // For each key in first input
159
+ for ( const key in firstInput ) {
160
+ const valuesForKey = inputs . map ( ( input ) => input [ key ] )
161
+ result [ key ] = mergeValues ( valuesForKey )
162
+ }
163
+
164
+ return result
165
+ }
166
+
167
+ const getEntityAndComponentValue = < ComponentValueType extends object > (
168
+ entities : Entity [ ] ,
169
+ component : Component < ComponentValueType >
170
+ ) : [ Entity , ComponentValueType ] [ ] => {
171
+ return entities . map ( ( entity ) => [ entity , getComponentValue ( entity , component ) as ComponentValueType ] )
172
+ }
173
+
174
+ export const useMultiComponentInput = < ComponentValueType extends object , InputType extends Input > (
175
+ entities : Entity [ ] ,
176
+ component : Component < ComponentValueType > ,
177
+ fromComponentValueToInput : ( componentValue : ComponentValueType ) => InputType ,
178
+ fromInputToComponentValue : ( input : InputType ) => ComponentValueType ,
179
+ validateInput : ( input : InputType ) => boolean = ( ) => true
180
+ ) => {
181
+ // If there's only one entity, use the single entity version just to be safe for now
182
+ if ( entities . length === 1 ) {
183
+ return useComponentInput (
184
+ entities [ 0 ] ,
185
+ component ,
186
+ fromComponentValueToInput ,
187
+ fromInputToComponentValue ,
188
+ validateInput
189
+ )
190
+ }
191
+ const sdk = useSdk ( )
192
+
193
+ // Get initial merged value from all entities
194
+ const initialEntityValues = getEntityAndComponentValue ( entities , component )
195
+ const initialMergedValue = useMemo (
196
+ ( ) =>
197
+ mergeComponentValues (
198
+ initialEntityValues . map ( ( [ _ , component ] ) => component ) ,
199
+ fromComponentValueToInput
200
+ ) ,
201
+ [ ] // only compute on mount
202
+ )
203
+
204
+ const [ value , setMergeValue ] = useState ( initialMergedValue )
205
+ const [ isValid , setIsValid ] = useState ( true )
206
+ const [ isFocused , setIsFocused ] = useState ( false )
207
+
208
+ // Handle input updates
209
+ const handleUpdate = useCallback (
210
+ ( path : NestedKey < InputType > , getter : ( event : React . ChangeEvent < HTMLInputElement > ) => any = ( e ) => e . target . value ) =>
211
+ ( event : React . ChangeEvent < HTMLInputElement > ) => {
212
+ if ( ! value ) return
213
+
214
+ const newValue = setValue ( value , path , getter ( event ) )
215
+ if ( ! hasDiff ( value , newValue , 2 ) ) return
216
+
217
+ // Only update if component is last-write-win and SDK exists
218
+ if ( ! isLastWriteWinComponent ( component ) || ! sdk ) {
219
+ setMergeValue ( newValue )
220
+ return
221
+ }
222
+
223
+ // Validate and update all entities
224
+ const entityUpdates = getEntityAndComponentValue ( entities , component ) . map ( ( [ entity , componentValue ] ) => {
225
+ const updatedInput = setValue ( fromComponentValueToInput ( componentValue as any ) , path , getter ( event ) )
226
+ const newComponentValue = fromInputToComponentValue ( updatedInput )
227
+ return {
228
+ entity,
229
+ value : newComponentValue ,
230
+ isValid : validateInput ( updatedInput )
231
+ }
232
+ } )
233
+
234
+ const allUpdatesValid = entityUpdates . every ( ( { isValid } ) => isValid )
235
+
236
+ if ( allUpdatesValid ) {
237
+ entityUpdates . forEach ( ( { entity, value } ) => {
238
+ sdk . operations . updateValue ( component , entity , value )
239
+ } )
240
+ void sdk . operations . dispatch ( )
241
+ }
242
+
243
+ setMergeValue ( newValue )
244
+ setIsValid ( allUpdatesValid )
245
+ } ,
246
+ [ value , sdk , component , entities , fromInputToComponentValue , fromComponentValueToInput , validateInput ]
247
+ )
248
+
249
+ // Sync with engine changes
250
+ useChange (
251
+ ( event ) => {
252
+ const isRelevantUpdate =
253
+ entities . includes ( event . entity ) &&
254
+ component . componentId === event . component ?. componentId &&
255
+ event . value &&
256
+ event . operation === CrdtMessageType . PUT_COMPONENT
257
+
258
+ if ( ! isRelevantUpdate ) return
259
+
260
+ const updatedEntityValues = getEntityAndComponentValue ( entities , component )
261
+ const newMergedValue = mergeComponentValues (
262
+ updatedEntityValues . map ( ( [ _ , component ] ) => component ) ,
263
+ fromComponentValueToInput
264
+ )
265
+
266
+ if ( ! hasDiff ( value , newMergedValue , 2 ) || isFocused ) return
267
+
268
+ setMergeValue ( newMergedValue )
269
+ } ,
270
+ [ entities , component , fromComponentValueToInput , value , isFocused ]
271
+ )
272
+
273
+ // Input props getter
274
+ const getInputProps = useCallback (
275
+ (
276
+ path : NestedKey < InputType > ,
277
+ getter ?: ( event : React . ChangeEvent < HTMLInputElement > ) => any
278
+ ) : Pick < InputHTMLAttributes < HTMLElement > , 'value' | 'onChange' | 'onFocus' | 'onBlur' > => ( {
279
+ value : ( getValue ( value , path ) || '' ) . toString ( ) ,
280
+ onChange : handleUpdate ( path , getter ) ,
281
+ onFocus : ( ) => setIsFocused ( true ) ,
282
+ onBlur : ( ) => setIsFocused ( false )
283
+ } ) ,
284
+ [ value , handleUpdate ]
285
+ )
286
+
287
+ return { getInputProps, isValid }
288
+ }
0 commit comments