UNPKG

@servicenow/ui-renderer-snabbdom

Version:

Snabbdom renderer for UI Framework on Next Experience

227 lines (196 loc) 7.59 kB
/** * Copyright (c) 2020 ServiceNow, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ import { getConfigItem, configConstants } from '@servicenow/ui-config'; import { createInteractionId, mark, types as metrics } from '@servicenow/ui-metrics'; import { sandbox, locations, camelCase, memoize, allLogTypes as logTypes } from '@servicenow/ui-internal'; import { MODULES, ARIA_PREFIX, ATTR_PREFIX, DATA_PREFIX, HOOK_PREFIX, EVENT_PREFIX, CUSTOM_ELEMENT_ATTRIBUTES, NON_CUSTOM_ELEMENT_ATTRIBUTES, ACRONYM_PROPS } from '../constants'; import log from '../log'; import { INTERACTION_ID } from '../symbols'; const { MEMOIZE_SNABBDOM_PROP_PARTS } = configConstants; const shouldMemoizePropParts = getConfigItem(MEMOIZE_SNABBDOM_PROP_PARTS); const isNil = value => value === null || value === undefined; // TODO: investigate the benifit and drawbacks of memoizing this. This does make it faster but at the cost of memory. const memoizedCamelCase = memoize(camelCase, { profileName: 'snabbdom-camelCase' }); const isCustomElement = tag => tag.indexOf('-') > 0; const memoizedIsCustomElement = memoize(isCustomElement, { profileName: 'snabbdom-isCustomElement' }); function convertToLoadedEvent(value) { const [fn, ...args] = value; return (event, vnode) => { fn.apply(vnode, [...args, event, vnode]); }; } const getPropKeyParts = key => { const index = key.indexOf('-'); const prefix = index > 0 ? key.substring(0, index) : null; const name = prefix === ATTR_PREFIX || DATA_PREFIX || HOOK_PREFIX || EVENT_PREFIX ? key.slice(index + 1) : key; return { index, prefix, name }; }; const memoizedGetPropKeyParts = shouldMemoizePropParts ? memoize(getPropKeyParts, { profileName: 'snabbdom-getPropKeyParts' }) : getPropKeyParts; /** * Move props into the proper module fields so they can properly be handled by modules registered with snabbdom. * @private * @method formatProps * @param tagName element name * @param {object} props Properties to format by prefixes * @returns {{hook: {}, ref: {}, now-aria-ref: {}, on: {}, style: {}, class: {}, attrs: {}, props: {}, dataset: {}}} */ export default function formatProps(tagName, props) { const result = { hook: {}, ref: null, 'now-aria-ref': null, on: {}, style: {}, class: {}, attrs: {}, props: {} }; const isCustomTag = memoizedIsCustomElement(tagName); // this is the fastest way to iterate over an object's keys: // https://jsperf.com/object-keys-vs-for-in-with-closure/3 const keys = Object.keys(props); for (let i = 0; i < keys.length; i++) { const key = keys[i]; const value = props[key]; if (key === 'key') { result.key = value; continue; } const { index: keyIndex, prefix, name: keyName } = memoizedGetPropKeyParts(key); if (keyIndex > 0) { if (prefix === ATTR_PREFIX) { result.attrs[keyName] = isNil(value) ? false : value; continue; } if (prefix === ARIA_PREFIX) { result.attrs[key] = isNil(value) ? false : value; continue; } if (prefix === DATA_PREFIX) { // Perf: Defer defining dataset on the resulting vnode as snabbdom will // otherwise do un-necessary reading from the DOM in the dataset module. if (!result.dataset) result.dataset = {}; result.dataset[keyName] = value; continue; } if (prefix === HOOK_PREFIX && typeof value === 'function') { const name = keyName; result.hook[name] = (vnode, ...rest) => { const host = vnode.elm && vnode.elm.getRootNode().host; if (!host) { try { return value(vnode, ...rest); } catch (e) { log(`An Error occured while executing hook-${name}`, { error: e, level: logTypes.ERROR, origin: 'formatProps' }); return; } } return sandbox((host, vnode, ...rest) => { const interactionId = host[INTERACTION_ID] || createInteractionId(); mark(host, interactionId, metrics.HOOK_START, { name, tagName: vnode.elm.tagName }); value(vnode, ...rest); mark(host, interactionId, metrics.HOOK_END, { name, tagName: vnode.elm.tagName }); }, { args: [host, vnode, ...rest], dispatch: host.helpers, host, location: locations.VIEW, details: { name }, log }); }; continue; } // Backwards compatibility for snabbdom's `loaded event` feature: // // on-click={[function handler() {}, a, b]} // // would automatically bind `a` and `b` as the first args to the handler: // // handler(a, b, event, vnode); // // This was supported in snabbdom@0.7 but removed in snabbdom@2.0 // https://github.com/snabbdom/snabbdom/pull/804 if (prefix === EVENT_PREFIX && Array.isArray(value) && typeof value[0] === 'function' && value.length > 1) { result.on[keyName] = convertToLoadedEvent(value); continue; } if (MODULES.includes(prefix)) { result[prefix][keyName] = value; continue; } } if (MODULES.includes(key)) { result[key] = value; continue; } if (isCustomTag) { if (CUSTOM_ELEMENT_ATTRIBUTES.includes(key)) { result.attrs[key] = isNil(value) ? false : value; if (!result.key && key === 'component-id') result.key = value; } if (process.env.NODE_ENV !== 'production') { const lowerKey = memoizedCamelCase(key).toLowerCase(); if (NON_CUSTOM_ELEMENT_ATTRIBUTES.includes(lowerKey) && !NON_CUSTOM_ELEMENT_ATTRIBUTES.includes(key)) { log(`For tag <${tagName}>: the property '${key}' matches the standard HTML5 attribute '${lowerKey}'. Please use standard HTML attributes & avoid props matching standard HTML attributes`, { level: logTypes.WARN, tagName }); } } } else { if (NON_CUSTOM_ELEMENT_ATTRIBUTES.includes(key)) result.attrs[key] = isNil(value) ? false : value; } if (ACRONYM_PROPS.includes(key)) { result.props[key] = value; continue; } result.props[memoizedCamelCase(key)] = value; } return result; } //# sourceMappingURL=formatProps.js.map