Files
NativeScript/nativescript-core/ui/builder/component-builder/component-builder.ts
Nathan Walker da3c0b973d chore: format
2020-07-11 11:01:40 -07:00

306 lines
7.9 KiB
TypeScript

// Definitions.
import { ComponentModule } from ".";
import { View } from "../../core/view";
// Types.
import { isEventOrGesture } from "../../core/bindable";
import { getBindingOptions, bindingConstants } from "../binding-builder";
import { profile } from "../../../profiling";
import * as debugModule from "../../../utils/debug";
import * as platform from "../../../platform";
import { sanitizeModuleName } from "../module-name-sanitizer";
import { resolveModuleName } from "../../../module-name-resolver";
const UI_PATH = "ui/";
const MODULES = {
TabViewItem: "ui/tab-view",
TabStrip: "ui/tab-navigation-base/tab-strip",
TabStripItem: "ui/tab-navigation-base/tab-strip-item",
TabContentItem: "ui/tab-navigation-base/tab-content-item",
FormattedString: "ui/text-base/formatted-string",
Span: "ui/text-base/span",
ActionItem: "ui/action-bar",
NavigationButton: "ui/action-bar",
SegmentedBarItem: "ui/segmented-bar",
};
const CODE_FILE = "codeFile";
const CSS_FILE = "cssFile";
const IMPORT = "import";
const createComponentInstance = profile(
"createComponentInstance",
(
elementName: string,
namespace: string
): { instance: View; instanceModule: Object } => {
let instance: View;
let instanceModule: Object;
// Get module id.
let resolvedModuleName;
try {
if (typeof namespace === "string") {
resolvedModuleName = resolveModuleName(namespace, "");
instanceModule = global.loadModule(resolvedModuleName, true);
} else {
// load module from @nativescript/core/ui or mapped paths
resolvedModuleName =
MODULES[elementName] ||
UI_PATH +
(elementName.toLowerCase().indexOf("layout") !== -1
? "layouts/"
: "") +
elementName
.split(/(?=[A-Z])/)
.join("-")
.toLowerCase();
// don't register core modules for HMR self-accept
instanceModule = global.loadModule(resolvedModuleName, false);
}
// Get the component type from module.
const instanceType = instanceModule[elementName] || Object;
// Create instance of the component.
instance = new instanceType();
} catch (ex) {
const debug: typeof debugModule = require("../../../utils/debug");
throw new debug.ScopeError(
ex,
"Module '" +
resolvedModuleName +
"' not found for element '" +
(namespace ? namespace + ":" : "") +
elementName +
"'."
);
}
return { instance, instanceModule };
}
);
const getComponentModuleExports = profile(
"getComponentModuleExports",
(instance: View, moduleExports: Object, attributes: Object): Object => {
if (attributes) {
const codeFileAttribute =
attributes[CODE_FILE] || attributes[IMPORT];
if (codeFileAttribute) {
const resolvedCodeFileModule = resolveModuleName(
sanitizeModuleName(codeFileAttribute),
""
);
if (resolvedCodeFileModule) {
moduleExports = global.loadModule(
resolvedCodeFileModule,
true
);
(<any>instance).exports = moduleExports;
} else {
throw new Error(
`Code file with path "${codeFileAttribute}" cannot be found! Looking for webpack module with name "${resolvedCodeFileModule}"`
);
}
}
}
return moduleExports;
}
);
const applyComponentCss = profile(
"applyComponentCss",
(instance: View, moduleName: string, attributes: Object) => {
let cssApplied = false;
if (attributes && attributes[CSS_FILE]) {
let resolvedCssModuleName = resolveModuleName(
sanitizeModuleName(attributes[CSS_FILE]),
"css"
);
if (resolvedCssModuleName) {
instance.addCssFile(resolvedCssModuleName);
cssApplied = true;
} else {
throw new Error(
`Css file with path "${attributes[CSS_FILE]}" cannot be found! Looking for webpack module with name "${resolvedCssModuleName}"`
);
}
}
if (moduleName && !cssApplied) {
let resolvedCssModuleName = resolveModuleName(moduleName, "css");
if (resolvedCssModuleName) {
instance.addCssFile(resolvedCssModuleName);
}
}
}
);
const applyComponentAttributes = profile(
"applyComponentAttributes",
(
instance: View,
instanceModule: Object,
moduleExports: Object,
attributes: Object
) => {
if (instance && instanceModule) {
for (let attr in attributes) {
const attrValue = <string>attributes[attr];
if (attr.indexOf(":") !== -1) {
const platformName = attr.split(":")[0].trim();
if (
platformName.toLowerCase() ===
platform.device.os.toLowerCase()
) {
attr = attr.split(":")[1].trim();
} else {
continue;
}
}
if (attr.indexOf(".") !== -1) {
let subObj = instance;
const properties = attr.split(".");
const subPropName = properties[properties.length - 1];
for (let i = 0; i < properties.length - 1; i++) {
if (subObj !== undefined && subObj !== null) {
subObj = subObj[properties[i]];
}
}
if (subObj !== undefined && subObj !== null) {
setPropertyValue(
subObj,
instanceModule,
moduleExports,
subPropName,
attrValue
);
}
} else {
setPropertyValue(
instance,
instanceModule,
moduleExports,
attr,
attrValue
);
}
}
}
}
);
export function getComponentModule(
elementName: string,
namespace: string,
attributes: Object,
moduleExports: Object,
moduleNamePath?: string,
isRootComponent?: boolean
): ComponentModule {
// Support lower-case-dashed component declaration in the XML (https://github.com/NativeScript/NativeScript/issues/309).
elementName = elementName
.split("-")
.map((s) => s[0].toUpperCase() + s.substring(1))
.join("");
const { instance, instanceModule } = createComponentInstance(
elementName,
namespace,
null
);
moduleExports = getComponentModuleExports(
instance,
<any>moduleExports,
attributes
);
if (isRootComponent) {
applyComponentCss(instance, moduleNamePath, attributes);
}
applyComponentAttributes(instance, instanceModule, moduleExports);
let componentModule;
if (instance && instanceModule) {
componentModule = { component: instance, exports: instanceModule };
}
return componentModule;
}
export function setPropertyValue(
instance: View,
instanceModule: Object,
exports: Object,
propertyName: string,
propertyValue: any
) {
// Note: instanceModule can be null if we are loading custom component with no code-behind.
if (isBinding(propertyValue) && instance.bind) {
const bindOptions = getBindingOptions(
propertyName,
getBindingExpressionFromAttribute(propertyValue)
);
instance.bind(
{
sourceProperty: bindOptions[bindingConstants.sourceProperty],
targetProperty: bindOptions[bindingConstants.targetProperty],
expression: bindOptions[bindingConstants.expression],
twoWay: bindOptions[bindingConstants.twoWay],
},
bindOptions[bindingConstants.source]
);
} else if (isEventOrGesture(propertyName, instance)) {
// Get the event handler from page module exports.
const handler = exports && exports[propertyValue];
// Check if the handler is function and add it to the instance for specified event name.
if (typeof handler === "function") {
instance.on(propertyName, handler);
}
} else if (
isKnownFunction(propertyName, instance) &&
exports &&
typeof exports[propertyValue] === "function"
) {
instance[propertyName] = exports[propertyValue];
} else {
instance[propertyName] = propertyValue;
}
}
function getBindingExpressionFromAttribute(value: string): string {
return value.replace("{{", "").replace("}}", "").trim();
}
function isBinding(value: any): boolean {
let isBinding;
if (typeof value === "string") {
const str = value.trim();
isBinding =
str.indexOf("{{") === 0 && str.lastIndexOf("}}") === str.length - 2;
}
return isBinding;
}
// For example, ListView.itemTemplateSelector
let KNOWN_FUNCTIONS = "knownFunctions";
function isKnownFunction(name: string, instance: View): boolean {
return (
instance.constructor &&
KNOWN_FUNCTIONS in instance.constructor &&
instance.constructor[KNOWN_FUNCTIONS].indexOf(name) !== -1
);
}