@servicenow/ui-renderer-snabbdom
Version:
Snabbdom renderer for UI Framework on Next Experience
227 lines (196 loc) • 7.59 kB
JavaScript
/**
* 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