UNPKG

@gechiui/components

Version:
113 lines (96 loc) 3.77 kB
import { createElement } from "@gechiui/element"; /** * External dependencies */ import { debounce, without } from 'lodash'; /** * GeChiUI dependencies */ import { Component } from '@gechiui/element'; import { addAction, applyFilters, removeAction } from '@gechiui/hooks'; import { createHigherOrderComponent } from '@gechiui/compose'; const ANIMATION_FRAME_PERIOD = 16; /** * Creates a higher-order component which adds filtering capability to the * wrapped component. Filters get applied when the original component is about * to be mounted. When a filter is added or removed that matches the hook name, * the wrapped component re-renders. * * @param {string} hookName Hook name exposed to be used by filters. * * @return {Function} Higher-order component factory. */ export default function withFilters(hookName) { return createHigherOrderComponent(OriginalComponent => { const namespace = 'core/with-filters/' + hookName; /** * The component definition with current filters applied. Each instance * reuse this shared reference as an optimization to avoid excessive * calls to `applyFilters` when many instances exist. * * @type {?Component} */ let FilteredComponent; /** * Initializes the FilteredComponent variable once, if not already * assigned. Subsequent calls are effectively a noop. */ function ensureFilteredComponent() { if (FilteredComponent === undefined) { FilteredComponent = applyFilters(hookName, OriginalComponent); } } class FilteredComponentRenderer extends Component { constructor() { super(...arguments); ensureFilteredComponent(); } componentDidMount() { FilteredComponentRenderer.instances.push(this); // If there were previously no mounted instances for components // filtered on this hook, add the hook handler. if (FilteredComponentRenderer.instances.length === 1) { addAction('hookRemoved', namespace, onHooksUpdated); addAction('hookAdded', namespace, onHooksUpdated); } } componentWillUnmount() { FilteredComponentRenderer.instances = without(FilteredComponentRenderer.instances, this); // If this was the last of the mounted components filtered on // this hook, remove the hook handler. if (FilteredComponentRenderer.instances.length === 0) { removeAction('hookRemoved', namespace); removeAction('hookAdded', namespace); } } render() { return createElement(FilteredComponent, this.props); } } FilteredComponentRenderer.instances = []; /** * Updates the FilteredComponent definition, forcing a render for each * mounted instance. This occurs a maximum of once per animation frame. */ const throttledForceUpdate = debounce(() => { // Recreate the filtered component, only after delay so that it's // computed once, even if many filters added. FilteredComponent = applyFilters(hookName, OriginalComponent); // Force each instance to render. FilteredComponentRenderer.instances.forEach(instance => { instance.forceUpdate(); }); }, ANIMATION_FRAME_PERIOD); /** * When a filter is added or removed for the matching hook name, each * mounted instance should re-render with the new filters having been * applied to the original component. * * @param {string} updatedHookName Name of the hook that was updated. */ function onHooksUpdated(updatedHookName) { if (updatedHookName === hookName) { throttledForceUpdate(); } } return FilteredComponentRenderer; }, 'withFilters'); } //# sourceMappingURL=index.js.map