-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
263b785
commit c2dec12
Showing
4 changed files
with
110 additions
and
104 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
171 changes: 89 additions & 82 deletions
171
packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,144 +1,151 @@ | ||
import { createEvent, dispatchEvents, Event, PluginInjector, Subscriber, Unsubscribe } from 'roqueform'; | ||
import { Field, PluginInjector } from 'roqueform'; | ||
import isDeepEqual from 'fast-deep-equal'; | ||
import { createElementValueAccessor, ElementValueAccessor } from './createElementValueAccessor'; | ||
|
||
const EVENT_CHANGE_OBSERVED_ELEMENTS = 'change:observedElements'; | ||
|
||
/** | ||
* The default value accessor. | ||
*/ | ||
const elementValueAccessor = createElementValueAccessor(); | ||
|
||
export type RefCallback = (element: Element | null) => void; | ||
|
||
/** | ||
* The plugin added to fields by the {@link uncontrolledPlugin}. | ||
*/ | ||
export interface UncontrolledPlugin { | ||
element: Element | null; | ||
|
||
/** | ||
* The array of elements that are used to derive the field value. Update this array by calling {@link observe} method. | ||
* Elements are observed by the {@link !MutationObserver MutationObserver} and deleted from this array when they are | ||
* removed from DOM. | ||
* The array of elements that are used to derive the field value, and which are updated the field value is changed. | ||
* | ||
* @protected | ||
*/ | ||
['observedElements']: Element[]; | ||
['elements']: Element[]; | ||
|
||
/** | ||
* The accessor that reads and writes field value from and to {@link observedElements observed elements}. | ||
* The map from {@link refFor a ref key} to a corresponding element. | ||
* | ||
* @protected | ||
*/ | ||
['elementValueAccessor']: ElementValueAccessor; | ||
['elementsMap']: Map<unknown, Element>; | ||
|
||
/** | ||
* Adds the DOM element to {@link observedElements observed elements}. | ||
* The accessor that reads and writes the field value from and to {@link elements}. | ||
* | ||
* @param element The element to observe. No-op if the element is `null` or not connected to the DOM. | ||
* @protected | ||
*/ | ||
observe(element: Element | null): void; | ||
['elementValueAccessor']: ElementValueAccessor; | ||
|
||
/** | ||
* Subscribes to updates of {@link observedElements observed elements}. | ||
* | ||
* @param eventType The type of the event. | ||
* @param subscriber The subscriber that would be triggered. | ||
* @returns The callback to unsubscribe the subscriber. | ||
* Associates the field with {@link element the DOM element}. | ||
*/ | ||
on(eventType: 'change:observedElements', subscriber: Subscriber<this, Element>): Unsubscribe; | ||
ref(element: Element | null): void; | ||
|
||
/** | ||
* Associates the field with {@link element the DOM element}. This method is usually exposed by plugins that use DOM | ||
* element references. This method is invoked when {@link observedElements the first observed element} is changed. | ||
* | ||
* @protected | ||
* Returns a callback that associates the field with {@link element the DOM element} under | ||
* {@link elementsMap the given key}. | ||
*/ | ||
['ref']?(element: Element | null): void; | ||
refFor(key: unknown): RefCallback; | ||
} | ||
|
||
/** | ||
* Updates field value when the DOM element value is changed and vice versa. | ||
* | ||
* @param accessor The accessor that reads and writes values to and from the DOM elements that | ||
* {@link UncontrolledPlugin.observedElements are observed by the filed}. | ||
*/ | ||
export function uncontrolledPlugin(accessor = elementValueAccessor): PluginInjector<UncontrolledPlugin> { | ||
return field => { | ||
field.observedElements = []; | ||
field.elements = []; | ||
field.elementsMap = new Map(); | ||
field.elementValueAccessor = accessor; | ||
|
||
const mutationObserver = new MutationObserver(mutations => { | ||
const events: Event[] = []; | ||
const { observedElements } = field; | ||
const refs = new Map<unknown, RefCallback>(); | ||
|
||
for (const mutation of mutations) { | ||
for (let i = 0; i < mutation.removedNodes.length; ++i) { | ||
const elementIndex = observedElements.indexOf(mutation.removedNodes.item(i) as Element); | ||
|
||
if (elementIndex === -1) { | ||
continue; | ||
} | ||
|
||
const element = observedElements[elementIndex]; | ||
|
||
element.removeEventListener('input', changeListener); | ||
element.removeEventListener('change', changeListener); | ||
|
||
observedElements.splice(elementIndex, 1); | ||
events.push(createEvent(EVENT_CHANGE_OBSERVED_ELEMENTS, field, element)); | ||
} | ||
} | ||
|
||
if (observedElements.length === 0) { | ||
mutationObserver.disconnect(); | ||
field.ref?.(null); | ||
} else { | ||
field.ref?.(observedElements[0]); | ||
} | ||
|
||
dispatchEvents(events); | ||
}); | ||
let lastValue: unknown; | ||
|
||
const changeListener: EventListener = event => { | ||
let value; | ||
if ( | ||
field.observedElements.indexOf(event.currentTarget as Element) !== -1 && | ||
!isDeepEqual((value = field.elementValueAccessor.get(field.observedElements)), field.value) | ||
field.elements.includes(event.target as Element) && | ||
!isDeepEqual((value = field.elementValueAccessor.get(field.elements)), field.value) | ||
) { | ||
field.setValue(value); | ||
field.setValue((lastValue = value)); | ||
} | ||
}; | ||
|
||
field.on('change:value', () => { | ||
if (field.observedElements.length !== 0) { | ||
field.elementValueAccessor.set(field.observedElements, field.value); | ||
field.on('change:value', event => { | ||
if (field.value !== lastValue && event.target === field && field.elements.length !== 0) { | ||
console.trace('SET_TO_ELEMENT'); | ||
field.elementValueAccessor.set(field.elements, field.value); | ||
} | ||
}); | ||
|
||
field.observe = element => { | ||
const { observedElements } = field; | ||
|
||
if ( | ||
!(element instanceof Element) || | ||
!element.isConnected || | ||
element.parentNode === null || | ||
observedElements.includes(element) | ||
) { | ||
return; | ||
} | ||
const { ref } = field; | ||
|
||
mutationObserver.observe(element.parentNode, { childList: true }); | ||
field.ref = nextElement => { | ||
const prevElement = field.element; | ||
|
||
element.addEventListener('input', changeListener); | ||
element.addEventListener('change', changeListener); | ||
ref?.(nextElement); | ||
|
||
const elementCount = observedElements.push(element); | ||
if (prevElement === nextElement) { | ||
return; | ||
} | ||
|
||
field.elementValueAccessor.set(observedElements, field.value); | ||
if (prevElement !== null) { | ||
field.elements.splice(field.elements.indexOf(prevElement), 1); | ||
prevElement.removeEventListener('input', changeListener); | ||
prevElement.removeEventListener('change', changeListener); | ||
} | ||
|
||
if (elementCount === 1) { | ||
field.ref?.(element); | ||
if (nextElement instanceof Element) { | ||
field.element = nextElement; | ||
field.elements.push(nextElement); | ||
nextElement.addEventListener('input', changeListener); | ||
nextElement.addEventListener('change', changeListener); | ||
} else { | ||
field.element = null; | ||
} | ||
|
||
dispatchEvents([createEvent(EVENT_CHANGE_OBSERVED_ELEMENTS, field, element)]); | ||
field.elementValueAccessor.set(field.elements, field.value); | ||
}; | ||
|
||
field.refFor = key => getOrCreateRefCallback(field, key, refs, changeListener); | ||
}; | ||
} | ||
|
||
function getOrCreateRefCallback( | ||
field: Field<UncontrolledPlugin>, | ||
key: unknown, | ||
refs: Map<unknown, RefCallback>, | ||
changeListener: EventListener | ||
): RefCallback { | ||
let ref = refs.get(key); | ||
if (ref !== undefined) { | ||
return ref; | ||
} | ||
|
||
ref = nextElement => { | ||
const prevElement = field.elementsMap.get(key); | ||
|
||
if (prevElement === nextElement) { | ||
return; | ||
} | ||
if (prevElement !== undefined) { | ||
field.elementsMap.delete(key); | ||
field.elements.splice(field.elements.indexOf(prevElement), 1); | ||
|
||
prevElement.removeEventListener('input', changeListener); | ||
prevElement.removeEventListener('change', changeListener); | ||
} | ||
if (nextElement instanceof Element) { | ||
field.elementsMap.set(key, nextElement); | ||
field.elements.push(nextElement); | ||
|
||
nextElement.addEventListener('input', changeListener); | ||
nextElement.addEventListener('change', changeListener); | ||
} | ||
|
||
field.elementValueAccessor.set(field.elements, field.value); | ||
}; | ||
|
||
refs.set(key, ref); | ||
return ref; | ||
} |