UNPKG

@deck.gl/core

Version:

deck.gl core library

216 lines 10 kB
// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import log from "../utils/log.js"; import { isAsyncIterable } from "../utils/iterable-utils.js"; import { parsePropTypes } from "./prop-types.js"; import { COMPONENT_SYMBOL, PROP_TYPES_SYMBOL, DEPRECATED_PROPS_SYMBOL, ASYNC_ORIGINAL_SYMBOL, ASYNC_RESOLVED_SYMBOL, ASYNC_DEFAULTS_SYMBOL } from "./constants.js"; import Component from "./component.js"; // Create a property object export function createProps(component, propObjects) { // Resolve extension value let extensions; for (let i = propObjects.length - 1; i >= 0; i--) { const props = propObjects[i]; if ('extensions' in props) { // @ts-expect-error TS(2339) extensions not defined extensions = props.extensions; } } // Create a new prop object with empty default props object const propsPrototype = getPropsPrototype(component.constructor, extensions); // The true default props object will be found later const propsInstance = Object.create(propsPrototype); // Props need a back pointer to the owning component propsInstance[COMPONENT_SYMBOL] = component; // The supplied (original) values for those async props that are set to url strings or Promises. // In this case, the actual (i.e. resolved) values are looked up from component.internalState propsInstance[ASYNC_ORIGINAL_SYMBOL] = {}; // Note: the actual (resolved) values for props that are NOT set to urls or Promises. // in this case the values are served directly from this map propsInstance[ASYNC_RESOLVED_SYMBOL] = {}; // "Copy" all sync props for (let i = 0; i < propObjects.length; ++i) { const props = propObjects[i]; // Do not use Object.assign here to avoid Symbols in props overwriting our private fields // This might happen if one of the arguments is another props instance for (const key in props) { propsInstance[key] = props[key]; } } // Props must be immutable Object.freeze(propsInstance); return propsInstance; } const MergedDefaultPropsCacheKey = '_mergedDefaultProps'; // Return precalculated defaultProps and propType objects if available // build them if needed function getPropsPrototype(componentClass, extensions) { // Bail out if we're not looking at a component - for two reasons: // 1. There's no reason for an ancestor of component to have props // 2. If we don't bail out, we'll follow the prototype chain all the way back to the global // function prototype and add _mergedDefaultProps to it, which may break other frameworks // (e.g. the react-three-fiber reconciler) if (!(componentClass instanceof Component.constructor)) return {}; // A string that uniquely identifies the extensions involved let cacheKey = MergedDefaultPropsCacheKey; if (extensions) { for (const extension of extensions) { const ExtensionClass = extension.constructor; if (ExtensionClass) { cacheKey += `:${ExtensionClass.extensionName || ExtensionClass.name}`; } } } const defaultProps = getOwnProperty(componentClass, cacheKey); if (!defaultProps) { return (componentClass[cacheKey] = createPropsPrototypeAndTypes(componentClass, extensions || [])); } return defaultProps; } // Build defaultProps and propType objects by walking component prototype chain function createPropsPrototypeAndTypes(componentClass, extensions) { const parent = componentClass.prototype; if (!parent) { return null; } const parentClass = Object.getPrototypeOf(componentClass); const parentDefaultProps = getPropsPrototype(parentClass); // Parse propTypes from Component.defaultProps const componentDefaultProps = getOwnProperty(componentClass, 'defaultProps') || {}; const componentPropDefs = parsePropTypes(componentDefaultProps); // Merged default props object. Order: parent, self, extensions const defaultProps = Object.assign(Object.create(null), parentDefaultProps, componentPropDefs.defaultProps); // Merged prop type definitions. Order: parent, self, extensions const propTypes = Object.assign(Object.create(null), parentDefaultProps?.[PROP_TYPES_SYMBOL], componentPropDefs.propTypes); // Merged deprecation list. Order: parent, self, extensions const deprecatedProps = Object.assign(Object.create(null), parentDefaultProps?.[DEPRECATED_PROPS_SYMBOL], componentPropDefs.deprecatedProps); for (const extension of extensions) { const extensionDefaultProps = getPropsPrototype(extension.constructor); if (extensionDefaultProps) { Object.assign(defaultProps, extensionDefaultProps); Object.assign(propTypes, extensionDefaultProps[PROP_TYPES_SYMBOL]); Object.assign(deprecatedProps, extensionDefaultProps[DEPRECATED_PROPS_SYMBOL]); } } // Create any necessary property descriptors and create the default prop object // Assign merged default props createPropsPrototype(defaultProps, componentClass); // Add getters/setters for async props addAsyncPropsToPropPrototype(defaultProps, propTypes); // Add setters for deprecated props addDeprecatedPropsToPropPrototype(defaultProps, deprecatedProps); // Store the precalculated props defaultProps[PROP_TYPES_SYMBOL] = propTypes; defaultProps[DEPRECATED_PROPS_SYMBOL] = deprecatedProps; // Backwards compatibility // TODO: remove access of hidden property from the rest of the code base if (extensions.length === 0 && !hasOwnProperty(componentClass, '_propTypes')) { componentClass._propTypes = propTypes; } return defaultProps; } // Builds a pre-merged default props object that component props can inherit from function createPropsPrototype(defaultProps, componentClass) { // Avoid freezing `id` prop const id = getComponentName(componentClass); Object.defineProperties(defaultProps, { // `id` is treated specially because layer might need to override it id: { writable: true, value: id } }); } function addDeprecatedPropsToPropPrototype(defaultProps, deprecatedProps) { for (const propName in deprecatedProps) { /* eslint-disable accessor-pairs */ Object.defineProperty(defaultProps, propName, { enumerable: false, set(newValue) { const nameStr = `${this.id}: ${propName}`; for (const newPropName of deprecatedProps[propName]) { if (!hasOwnProperty(this, newPropName)) { this[newPropName] = newValue; } } log.deprecated(nameStr, deprecatedProps[propName].join('/'))(); } }); /* eslint-enable accessor-pairs */ } } // Create descriptors for overridable props function addAsyncPropsToPropPrototype(defaultProps, propTypes) { const defaultValues = {}; const descriptors = {}; // Move async props into shadow values for (const propName in propTypes) { const propType = propTypes[propName]; const { name, value } = propType; // Note: async is ES7 keyword, can't destructure if (propType.async) { defaultValues[name] = value; descriptors[name] = getDescriptorForAsyncProp(name); } } // Default "resolved" values for async props, returned if value not yet resolved/set. defaultProps[ASYNC_DEFAULTS_SYMBOL] = defaultValues; // Shadowed object, just to make sure "early indexing" into the instance does not fail defaultProps[ASYNC_ORIGINAL_SYMBOL] = {}; Object.defineProperties(defaultProps, descriptors); } // Helper: Configures getter and setter for one async prop function getDescriptorForAsyncProp(name) { return { enumerable: true, // Save the provided value for async props in a special map set(newValue) { if (typeof newValue === 'string' || newValue instanceof Promise || isAsyncIterable(newValue)) { this[ASYNC_ORIGINAL_SYMBOL][name] = newValue; } else { this[ASYNC_RESOLVED_SYMBOL][name] = newValue; } }, // Only the component's state knows the true value of async prop get() { if (this[ASYNC_RESOLVED_SYMBOL]) { // Prop value isn't async, so just return it if (name in this[ASYNC_RESOLVED_SYMBOL]) { const value = this[ASYNC_RESOLVED_SYMBOL][name]; return value || this[ASYNC_DEFAULTS_SYMBOL][name]; } if (name in this[ASYNC_ORIGINAL_SYMBOL]) { // It's an async prop value: look into component state const state = this[COMPONENT_SYMBOL] && this[COMPONENT_SYMBOL].internalState; if (state && state.hasAsyncProp(name)) { return state.getAsyncProp(name) || this[ASYNC_DEFAULTS_SYMBOL][name]; } } } // the prop is not supplied, or // component not yet initialized/matched, return the component's default value for the prop return this[ASYNC_DEFAULTS_SYMBOL][name]; } }; } // HELPER METHODS function hasOwnProperty(object, prop) { return Object.prototype.hasOwnProperty.call(object, prop); } // Constructors have their super class constructors as prototypes function getOwnProperty(object, prop) { return hasOwnProperty(object, prop) && object[prop]; } function getComponentName(componentClass) { const componentName = componentClass.componentName; if (!componentName) { log.warn(`${componentClass.name}.componentName not specified`)(); } return componentName || componentClass.name; } //# sourceMappingURL=create-props.js.map