@muban/muban
Version:
Writing components for server-rendered HTML
144 lines (143 loc) • 6.8 kB
JavaScript
import { isUndefined } from 'isntnt';
import typedObjectEntries from '../type-utils/typedObjectEntries';
function convertToInternalPropInfo(element, propName, propDefinition, refs) {
var _a;
const { type, default: defaultValue, isOptional, validator } = propDefinition;
const getPropInfo = (sourceOption) => {
var _a;
let target;
// resolve refs into elements
if (sourceOption === null || sourceOption === void 0 ? void 0 : sourceOption.target) {
const targetRef = sourceOption.target ? refs[sourceOption.target] : undefined;
if (!targetRef) {
// eslint-disable-next-line no-console
console.error(`Property "${propName}" would like to use target "${sourceOption === null || sourceOption === void 0 ? void 0 : sourceOption.target}", but is not found in available refs.`, Object.keys(refs));
}
else {
// TODO: support collections... but how?
if (targetRef.type === 'element' && targetRef.element) {
target = targetRef.element;
}
if (targetRef.type === 'component' && ((_a = targetRef.component) === null || _a === void 0 ? void 0 : _a.element)) {
target = targetRef.component.element;
}
if (targetRef.type === 'collection' || targetRef.type === 'componentCollection') {
// eslint-disable-next-line no-console
console.warn(`Property "${propName}" would like to use the collection "${sourceOption === null || sourceOption === void 0 ? void 0 : sourceOption.target}" as target, using collections as target is not supported yet, please use a single element/component as target.`);
}
}
}
else {
target = element;
}
return {
name: propName,
type,
default: defaultValue,
isOptional,
validator,
source: {
name: (sourceOption && 'name' in sourceOption && sourceOption.name) || propName,
target,
type: sourceOption === null || sourceOption === void 0 ? void 0 : sourceOption.type,
options: (sourceOption && 'options' in sourceOption && sourceOption.options) || undefined,
},
};
};
return propDefinition.sourceOptions instanceof Array
? (_a = propDefinition.sourceOptions) === null || _a === void 0 ? void 0 : _a.map(getPropInfo)
: [getPropInfo(propDefinition.sourceOptions)];
}
function errorUnlessOptional(propInfo, message, ...logParameters) {
if (!propInfo.isOptional) {
// eslint-disable-next-line no-console
console.error(message, ...logParameters);
throw new Error('Exit');
}
return undefined;
}
export function getValueFromMultipleSources(propInfo, sources) {
for (const info of propInfo) {
try {
return getValueFromSource(info, sources);
}
catch (_a) { }
}
}
export function getValueFromSource(propInfo, sources) {
// if target cannot be found, just return undefined
if (!propInfo.source.target) {
return errorUnlessOptional(propInfo, `Property "${propInfo.name}" is marked as required, but the source target was not found.`);
}
// if source type is explicitly provided
if (propInfo.source.type) {
const source = sources.find((s) => propInfo.source.type === s.sourceName);
if (!(source === null || source === void 0 ? void 0 : source.hasProp(propInfo))) {
return errorUnlessOptional(propInfo, `Property "${propInfo.name}" is not available in source "${propInfo.source.type}".`);
}
return source === null || source === void 0 ? void 0 : source.getProp(propInfo);
}
// otherwise, use the default sources
const defaultSources = sources.filter((s) => ['data', 'json', 'css'].includes(s.sourceName));
// find available sources from those that are registered
const availableSources = defaultSources.filter((s) => (propInfo.source.type === undefined || propInfo.source.type === s.sourceName) &&
s.hasProp(propInfo));
if (availableSources.length === 0) {
return errorUnlessOptional(propInfo, `Property "${propInfo.name}" is marked as required, but not found at target.`, propInfo.source.target);
}
if (propInfo.type === Boolean) {
return availableSources
.map((source) => source.getProp(propInfo))
.reduce((accumulator, value) => {
if (accumulator === true || value === true)
return true;
if (value !== undefined)
return value;
return accumulator;
}, undefined);
}
// if more than 1 sources, pick the first one (except for booleans)
const usedSource = availableSources[0];
if (availableSources.length > 1) {
// eslint-disable-next-line no-console
console.warn(`Property "${propInfo.name}" is defined in more than one property source: ${availableSources
.map((s) => s.sourceName)
.join(', ')}. We'll use the first from the list: "${usedSource.sourceName}"`);
}
return usedSource.getProp(propInfo);
}
export function getComponentProps(props, element, propertySources, refs) {
var _a;
const sources = propertySources.map((s) => s(element));
const p = (_a = typedObjectEntries(props !== null && props !== void 0 ? props : {}).reduce((accumulator, [propName, propType]) => {
const propInfo = convertToInternalPropInfo(element, propName, propType, refs);
// TODO: ignore function prop types for some sources?
let extractedValue;
try {
extractedValue = getValueFromMultipleSources(propInfo, sources);
}
catch (_a) {
return accumulator;
}
// set default value
if (!isUndefined(propType.default) && isUndefined(extractedValue)) {
extractedValue = propType.default;
}
// validate value
if (propType.validator && !isUndefined(extractedValue)) {
if (!propType.validator(extractedValue)) {
// TODO: should we indeed throw errors here, or resolve the value to undefined?
throw new Error(`Validation Error: This prop value ("${extractedValue}") is not valid for:`);
}
}
accumulator[propName] = extractedValue;
return accumulator;
}, {})) !== null && _a !== void 0 ? _a : {};
return p;
}
// const sources = [
// createDataAttributePropertySource(),
// createJsonScriptPropertySource(),
// createReactivePropertySource(),
// ];
// getComponentProps({}, element, sources);