Add utils/module-loader - a mechanism to resolve modules at runtime.

Exposes a module registration API, that webpack users can use to bundle
"dynamic" modules that are required through some variable.

Falls back to the require API if no module is registered.

Reworked dynamic `require` code to use module-loader:

- navigation: ui/frame.
- XML UI build ui/builder/builder and ui/builder/component-builder.
- module on-demand loads in global functions: globals.
This commit is contained in:
Hristo Deshev
2015-12-14 14:41:16 +02:00
parent 6a37218ace
commit 648e4d216d
4 changed files with 250 additions and 223 deletions

View File

@@ -6,11 +6,36 @@ global.moduleMerge = function (sourceExports: any, destExports: any) {
}
}
type ModuleLoader = () => any;
const modules: Map<string, ModuleLoader> = new Map<string, ModuleLoader>();
global.registerModule = function(name: string, loader: ModuleLoader): void {
modules.set(name, loader);
}
global.moduleExists = function(name: string): boolean {
return modules.has(name);
}
global.loadModule = function(name: string): any {
const loader = modules.get(name);
if (loader) {
return loader();
} else {
return require(name);
}
}
global.registerModule("timer", () => require("timer"));
global.registerModule("ui/dialogs", () => require("ui/dialogs"));
global.registerModule("../xhr/xhr", () => require("../xhr/xhr"));
global.registerModule("fetch", () => require("fetch"));
function registerOnGlobalContext(name, module) {
Object.defineProperty(global, name, {
get: function () {
// We do not need to cache require() call since it is already cached in the runtime.
let m = require(module);
let m = global.loadModule(module);
global.moduleMerge(m, global);
// Redefine the property to make sure the above code is executed only once.

View File

@@ -1,43 +1,41 @@
import view = require("ui/core/view");
import fs = require("file-system");
import xml = require("xml");
import types = require("utils/types");
import componentBuilder = require("ui/builder/component-builder");
import platform = require("platform");
import definition = require("ui/builder");
import page = require("ui/page");
import fileResolverModule = require("file-system/file-name-resolver");
import trace = require("trace");
import debug = require("utils/debug");
import builder = require("ui/builder");
import * as trace from "trace";
import {debug, ScopeError, SourceError, Source} from "utils/debug";
import * as xml from "xml";
import {View, Template} from "ui/core/view";
import {File, path, knownFolders} from "file-system";
import {isString, isFunction, isDefined} from "utils/types";
import {ComponentModule, setPropertyValue, getComponentModule} from "ui/builder/component-builder";
import {platformNames, device} from "platform";
import {LoadOptions} from "ui/builder";
import {Page} from "ui/page";
import {resolveFileName} from "file-system/file-name-resolver";
export function parse(value: string | view.Template, context: any): view.View {
if (types.isString(value)) {
var viewToReturn: view.View;
if (context instanceof view.View) {
export function parse(value: string | Template, context: any): View {
if (isString(value)) {
var viewToReturn: View;
if (context instanceof View) {
context = getExports(context);
}
var componentModule = parseInternal(<string>value, context);
if (componentModule) {
viewToReturn = componentModule.component;
}
return viewToReturn;
} else if (types.isFunction(value)) {
return (<view.Template>value)();
} else if (isFunction(value)) {
return (<Template>value)();
}
}
function parseInternal(value: string, context: any, uri?: string): componentBuilder.ComponentModule {
function parseInternal(value: string, context: any, uri?: string): ComponentModule {
var start: xml2ui.XmlStringParser;
var ui: xml2ui.ComponentParser;
var errorFormat = (debug.debug && uri) ? xml2ui.SourceErrorFormat(uri) : xml2ui.PositionErrorFormat;
var errorFormat = (debug && uri) ? xml2ui.SourceErrorFormat(uri) : xml2ui.PositionErrorFormat;
(start = new xml2ui.XmlStringParser(errorFormat))
.pipe(new xml2ui.PlatformFilter())
.pipe(new xml2ui.XmlStateParser(ui = new xml2ui.ComponentParser(context, errorFormat)));
@@ -47,44 +45,44 @@ function parseInternal(value: string, context: any, uri?: string): componentBuil
return ui.rootComponentModule;
}
function loadCustomComponent(componentPath: string, componentName?: string, attributes?: Object, context?: Object, parentPage?: page.Page): componentBuilder.ComponentModule {
var result: componentBuilder.ComponentModule;
function loadCustomComponent(componentPath: string, componentName?: string, attributes?: Object, context?: Object, parentPage?: Page): ComponentModule {
var result: ComponentModule;
componentPath = componentPath.replace("~/", "");
var fullComponentPathFilePathWithoutExt = componentPath;
if (!fs.File.exists(componentPath) || componentPath === "." || componentPath === "./") {
fullComponentPathFilePathWithoutExt = fs.path.join(fs.knownFolders.currentApp().path, componentPath, componentName);
if (!File.exists(componentPath) || componentPath === "." || componentPath === "./") {
fullComponentPathFilePathWithoutExt = path.join(knownFolders.currentApp().path, componentPath, componentName);
}
var xmlFilePath = fileResolverModule.resolveFileName(fullComponentPathFilePathWithoutExt, "xml");
var xmlFilePath = resolveFileName(fullComponentPathFilePathWithoutExt, "xml");
if (xmlFilePath) {
// Custom components with XML
var jsFilePath = fileResolverModule.resolveFileName(fullComponentPathFilePathWithoutExt, "js");
var jsFilePath = resolveFileName(fullComponentPathFilePathWithoutExt, "js");
var subExports = context;
if (jsFilePath) {
// Custom components with XML and code
subExports = require(jsFilePath)
subExports = global.loadModule(jsFilePath)
}
result = loadInternal(xmlFilePath, subExports);
// Attributes will be transfered to the custom component
if (types.isDefined(result) && types.isDefined(result.component) && types.isDefined(attributes)) {
if (isDefined(result) && isDefined(result.component) && isDefined(attributes)) {
var attr: string;
for (attr in attributes) {
componentBuilder.setPropertyValue(result.component, subExports, context, attr, attributes[attr]);
setPropertyValue(result.component, subExports, context, attr, attributes[attr]);
}
}
} else {
// Custom components without XML
result = componentBuilder.getComponentModule(componentName, componentPath, attributes, context);
result = getComponentModule(componentName, componentPath, attributes, context);
}
// Add component CSS file if exists.
var cssFilePath = fileResolverModule.resolveFileName(fullComponentPathFilePathWithoutExt, "css");
var cssFilePath = resolveFileName(fullComponentPathFilePathWithoutExt, "css");
if (cssFilePath) {
if (parentPage) {
parentPage.addCssFile(cssFilePath);
@@ -96,13 +94,13 @@ function loadCustomComponent(componentPath: string, componentName?: string, attr
return result;
}
export function load(pathOrOptions: string | definition.LoadOptions, context?: any): view.View {
var viewToReturn: view.View;
var componentModule: componentBuilder.ComponentModule;
export function load(pathOrOptions: string | LoadOptions, context?: any): View {
var viewToReturn: View;
var componentModule: ComponentModule;
if (!context) {
if (!types.isString(pathOrOptions)) {
let options = <definition.LoadOptions>pathOrOptions;
if (!isString(pathOrOptions)) {
let options = <LoadOptions>pathOrOptions;
componentModule = loadCustomComponent(options.path, options.name, undefined, options.exports, options.page);
} else {
let path = <string>pathOrOptions;
@@ -120,12 +118,12 @@ export function load(pathOrOptions: string | definition.LoadOptions, context?: a
return viewToReturn;
}
function loadInternal(fileName: string, context?: any): componentBuilder.ComponentModule {
var componentModule: componentBuilder.ComponentModule;
function loadInternal(fileName: string, context?: any): ComponentModule {
var componentModule: ComponentModule;
// Check if the XML file exists.
if (fs.File.exists(fileName)) {
var file = fs.File.fromPath(fileName);
if (File.exists(fileName)) {
var file = File.fromPath(fileName);
var onError = function (error) {
throw new Error("Error loading file " + fileName + " :" + error.message);
}
@@ -141,7 +139,7 @@ function loadInternal(fileName: string, context?: any): componentBuilder.Compone
return componentModule;
}
function getExports(instance: view.View): any {
function getExports(instance: View): any {
var parent = instance.parent;
while (parent && (<any>parent).exports === undefined) {
@@ -152,7 +150,6 @@ function getExports(instance: view.View): any {
}
namespace xml2ui {
/**
* Pipes and filters:
* https://en.wikipedia.org/wiki/Pipeline_(software)
@@ -160,11 +157,11 @@ namespace xml2ui {
interface XmlProducer {
pipe<Next extends XmlConsumer>(next: Next): Next;
}
interface XmlConsumer {
parse(args: xml.ParserEvent);
}
export class XmlProducerBase implements XmlProducer {
private _next: XmlConsumer;
public pipe<Next extends XmlConsumer>(next: Next) {
@@ -175,15 +172,15 @@ namespace xml2ui {
this._next.parse(args);
}
}
export class XmlStringParser extends XmlProducerBase implements XmlProducer {
private error: ErrorFormatter;
constructor(error?: ErrorFormatter) {
super();
this.error = error || PositionErrorFormat;
}
public parse(value: string) {
var xmlParser = new xml.XmlParser((args: xml.ParserEvent) => {
try {
@@ -194,80 +191,80 @@ namespace xml2ui {
}, (e, p) => {
throw this.error(e, p);
}, true);
if (types.isString(value)) {
if (isString(value)) {
value = value.replace(/xmlns=("|')http:\/\/((www)|(schemas))\.nativescript\.org\/tns\.xsd\1/, "");
xmlParser.parse(value);
}
}
}
interface ErrorFormatter {
(e: Error, p: xml.Position): Error;
}
export function PositionErrorFormat(e: Error, p: xml.Position): Error {
return new debug.ScopeError(e, "Parsing XML at " + p.line + ":" + p.column);
return new ScopeError(e, "Parsing XML at " + p.line + ":" + p.column);
}
export function SourceErrorFormat(uri): ErrorFormatter {
return (e: Error, p: xml.Position) => {
var source = new debug.Source(uri, p.line, p.column);
e = new debug.SourceError(e, source, "Building UI from XML.");
var source = new Source(uri, p.line, p.column);
e = new SourceError(e, source, "Building UI from XML.");
return e;
}
}
export class PlatformFilter extends XmlProducerBase implements XmlProducer, XmlConsumer {
private currentPlatformContext: string;
public parse(args: xml.ParserEvent) {
if (args.eventType === xml.ParserEventType.StartElement) {
if (PlatformFilter.isPlatform(args.elementName)) {
if (this.currentPlatformContext) {
throw new Error("Already in '" + this.currentPlatformContext + "' platform context and cannot switch to '" + args.elementName + "' platform! Platform tags cannot be nested.");
}
this.currentPlatformContext = args.elementName;
return;
}
}
if (args.eventType === xml.ParserEventType.EndElement) {
if (PlatformFilter.isPlatform(args.elementName)) {
this.currentPlatformContext = undefined;
return;
}
}
if (this.currentPlatformContext && !PlatformFilter.isCurentPlatform(this.currentPlatformContext)) {
return;
}
this.next(args);
}
private static isPlatform(value: string): boolean {
return value && (value.toLowerCase() === platform.platformNames.android.toLowerCase()
|| value.toLowerCase() === platform.platformNames.ios.toLowerCase());
return value && (value.toLowerCase() === platformNames.android.toLowerCase()
|| value.toLowerCase() === platformNames.ios.toLowerCase());
}
private static isCurentPlatform(value: string): boolean {
return value && value.toLowerCase() === platform.device.os.toLowerCase();
return value && value.toLowerCase() === device.os.toLowerCase();
}
}
export class XmlArgsReplay extends XmlProducerBase implements XmlProducer {
private error: ErrorFormatter;
private args: xml.ParserEvent[];
constructor(args: xml.ParserEvent[], errorFormat: ErrorFormatter) {
super();
this.args = args;
this.error = errorFormat;
}
public replay() {
this.args.forEach((args: xml.ParserEvent) => {
try {
@@ -278,68 +275,68 @@ namespace xml2ui {
});
}
}
interface TemplateProperty {
context?: any;
parent: componentBuilder.ComponentModule;
parent: ComponentModule;
name: string;
elementName: string;
templateItems: Array<string>;
errorFormat: ErrorFormatter;
}
/**
* It is a state pattern
* https://en.wikipedia.org/wiki/State_pattern
*/
export class XmlStateParser implements XmlConsumer {
private state: XmlStateConsumer;
constructor(state: XmlStateConsumer) {
this.state = state;
}
parse(args: xml.ParserEvent) {
this.state = this.state.parse(args);
}
}
interface XmlStateConsumer extends XmlConsumer {
parse(args: xml.ParserEvent): XmlStateConsumer;
}
export class TemplateParser implements XmlStateConsumer {
private _context: any;
private _recordedXmlStream: Array<xml.ParserEvent>;
private _templateProperty: TemplateProperty;
private _nestingLevel: number;
private _state: TemplateParser.State;
private parent: XmlStateConsumer;
constructor(parent: XmlStateConsumer, templateProperty: TemplateProperty) {
this.parent = parent;
this._context = templateProperty.context;
this._recordedXmlStream = new Array<xml.ParserEvent>();
this._templateProperty = templateProperty;
this._nestingLevel = 0;
this._state = TemplateParser.State.EXPECTING_START;
}
public parse(args: xml.ParserEvent): XmlStateConsumer {
if (args.eventType === xml.ParserEventType.StartElement) {
this.parseStartElement(args.prefix, args.namespace, args.elementName, args.attributes);
} else if (args.eventType === xml.ParserEventType.EndElement) {
this.parseEndElement(args.prefix, args.elementName);
}
this._recordedXmlStream.push(args);
return this._state === TemplateParser.State.FINISHED ? this.parent : this;
}
public get elementName(): string {
return this._templateProperty.elementName;
}
@@ -350,46 +347,46 @@ namespace xml2ui {
} else if (this._state === TemplateParser.State.FINISHED) {
throw new Error("Template must have exactly one root element but multiple elements were found.");
}
this._nestingLevel++;
}
private parseEndElement(prefix: string, elementName: string) {
if (this._state === TemplateParser.State.EXPECTING_START) {
throw new Error("Template must have exactly one root element but none was found.");
} else if (this._state === TemplateParser.State.FINISHED) {
throw new Error("No more closing elements expected for this template.");
}
this._nestingLevel--;
if (this._nestingLevel === 0) {
this._state = TemplateParser.State.FINISHED;
this.build();
}
}
private build() {
if (this._templateProperty.name in this._templateProperty.parent.component) {
var context = this._context;
var errorFormat = this._templateProperty.errorFormat;
var template: view.Template = () => {
var template: Template = () => {
var start: xml2ui.XmlArgsReplay;
var ui: xml2ui.ComponentParser;
(start = new xml2ui.XmlArgsReplay(this._recordedXmlStream, errorFormat))
// No platform filter, it has been filtered allready
.pipe(new XmlStateParser(ui = new ComponentParser(context, errorFormat)));
start.replay();
return ui.rootComponentModule.component;
}
this._templateProperty.parent.component[this._templateProperty.name] = template;
}
}
}
export namespace TemplateParser {
export const enum State {
EXPECTING_START,
@@ -397,45 +394,45 @@ namespace xml2ui {
FINISHED
}
}
export class ComponentParser implements XmlStateConsumer {
private static KNOWNCOLLECTIONS = "knownCollections";
private static KNOWNTEMPLATES = "knownTemplates";
public rootComponentModule: componentBuilder.ComponentModule;
public rootComponentModule: ComponentModule;
private context: any;
private currentPage: page.Page;
private parents = new Array<componentBuilder.ComponentModule>();
private currentPage: Page;
private parents = new Array<ComponentModule>();
private complexProperties = new Array<ComponentParser.ComplexProperty>();
private error;
constructor(context: any, errorFormat: ErrorFormatter) {
this.context = context;
this.error = errorFormat;
}
public parse(args: xml.ParserEvent): XmlStateConsumer {
// Get the current parent.
var parent = this.parents[this.parents.length - 1];
var complexProperty = this.complexProperties[this.complexProperties.length - 1];
// Create component instance from every element declaration.
if (args.eventType === xml.ParserEventType.StartElement) {
if (ComponentParser.isComplexProperty(args.elementName)) {
var name = ComponentParser.getComplexPropertyName(args.elementName);
this.complexProperties.push({
parent: parent,
name: name,
items: [],
});
if (ComponentParser.isKnownTemplate(name, parent.exports)) {
return new TemplateParser(this, {
context: (parent ? getExports(parent.component) : null) || this.context, // Passing 'context' won't work if you set "codeFile" on the page
@@ -446,19 +443,19 @@ namespace xml2ui {
errorFormat: this.error
});
}
} else {
var componentModule: componentBuilder.ComponentModule;
var componentModule: ComponentModule;
if (args.prefix && args.namespace) {
// Custom components
componentModule = loadCustomComponent(args.namespace, args.elementName, args.attributes, this.context, this.currentPage);
} else {
// Default components
componentModule = componentBuilder.getComponentModule(args.elementName, args.namespace, args.attributes, this.context);
componentModule = getComponentModule(args.elementName, args.namespace, args.attributes, this.context);
}
if (componentModule) {
if (parent) {
if (complexProperty) {
@@ -470,17 +467,17 @@ namespace xml2ui {
} else if (this.parents.length === 0) {
// Set root component.
this.rootComponentModule = componentModule;
if (this.rootComponentModule && this.rootComponentModule.component instanceof page.Page) {
this.currentPage = <page.Page>this.rootComponentModule.component;
if (this.rootComponentModule && this.rootComponentModule.component instanceof Page) {
this.currentPage = <Page>this.rootComponentModule.component;
}
}
// Add the component instance to the parents scope collection.
this.parents.push(componentModule);
}
}
} else if (args.eventType === xml.ParserEventType.EndElement) {
if (ComponentParser.isComplexProperty(args.elementName)) {
if (complexProperty) {
@@ -492,36 +489,36 @@ namespace xml2ui {
}
// Remove the last complexProperty from the complexProperties collection (move to the previous complexProperty scope).
this.complexProperties.pop();
} else {
// Remove the last parent from the parents collection (move to the previous parent scope).
this.parents.pop();
}
}
return this;
}
private static isComplexProperty(name: string): boolean {
return types.isString(name) && name.indexOf(".") !== -1;
return isString(name) && name.indexOf(".") !== -1;
}
private static getComplexPropertyName(fullName: string): string {
var name: string;
if (types.isString(fullName)) {
if (isString(fullName)) {
var names = fullName.split(".");
name = names[names.length - 1];
}
return name;
}
private static isKnownTemplate(name: string, exports: any): boolean {
return ComponentParser.KNOWNTEMPLATES in exports && exports[ComponentParser.KNOWNTEMPLATES] && name in exports[ComponentParser.KNOWNTEMPLATES];
}
private static addToComplexProperty(parent: componentBuilder.ComponentModule, complexProperty: ComponentParser.ComplexProperty, elementModule: componentBuilder.ComponentModule) {
private static addToComplexProperty(parent: ComponentModule, complexProperty: ComponentParser.ComplexProperty, elementModule: ComponentModule) {
// If property name is known collection we populate array with elements.
var parentComponent = <any>parent.component;
if (ComponentParser.isKnownCollection(complexProperty.name, parent.exports)) {
@@ -533,17 +530,17 @@ namespace xml2ui {
parentComponent[complexProperty.name] = elementModule.component;
}
}
private static isKnownCollection(name: string, context: any): boolean {
return ComponentParser.KNOWNCOLLECTIONS in context && context[ComponentParser.KNOWNCOLLECTIONS] && name in context[ComponentParser.KNOWNCOLLECTIONS];
}
}
export namespace ComponentParser {
export interface ComplexProperty {
parent: componentBuilder.ComponentModule;
parent: ComponentModule;
name: string;
items?: Array<any>;
}
}
}
}

View File

@@ -1,11 +1,11 @@
import view = require("ui/core/view");
import types = require("utils/types");
import definition = require("ui/builder/component-builder");
import fs = require("file-system");
import bindingBuilder = require("./binding-builder");
import platform = require("platform");
import pages = require("ui/page");
import debug = require("utils/debug");
import {isString, isDefined, isFunction} from "utils/types";
import {device} from "platform";
import {Page} from "ui/page";
import {View, isEventOrGesture} from "ui/core/view";
import {ComponentModule} from "ui/builder/component-builder";
import {File, Folder, path, knownFolders} from "file-system";
import {getBindingOptions, bindingConstants} from "./binding-builder";
import {ScopeError} from "utils/debug";
//the imports below are needed for special property registration
import "ui/layouts/dock-layout";
@@ -27,10 +27,10 @@ var MODULES = {
var CODEFILE = "codeFile";
var CSSFILE = "cssFile";
export function getComponentModule(elementName: string, namespace: string, attributes: Object, exports: Object): definition.ComponentModule {
var instance: view.View;
export function getComponentModule(elementName: string, namespace: string, attributes: Object, exports: Object): ComponentModule {
var instance: View;
var instanceModule: Object;
var componentModule: definition.ComponentModule;
var componentModule: ComponentModule;
// Support lower-case-dashed component declaration in the XML (https://github.com/NativeScript/NativeScript/issues/309).
elementName = elementName.split("-").map(s => { return s[0].toUpperCase() + s.substring(1) }).join("");
@@ -41,19 +41,19 @@ export function getComponentModule(elementName: string, namespace: string, attri
elementName.split(/(?=[A-Z])/).join("-").toLowerCase();
try {
if (types.isString(namespace)) {
var pathInsideTNSModules = fs.path.join(fs.knownFolders.currentApp().path, "tns_modules", namespace);
if (isString(namespace)) {
var pathInsideTNSModules = path.join(knownFolders.currentApp().path, "tns_modules", namespace);
if (fs.Folder.exists(pathInsideTNSModules)) {
if (Folder.exists(pathInsideTNSModules)) {
moduleId = pathInsideTNSModules;
} else {
// We expect module at root level in the app.
moduleId = fs.path.join(fs.knownFolders.currentApp().path, namespace);
moduleId = path.join(knownFolders.currentApp().path, namespace);
}
}
// Require module by module id.
instanceModule = require(moduleId);
instanceModule = global.loadModule(moduleId);
// Get the component type from module.
var instanceType = instanceModule[elementName] || Object;
@@ -61,18 +61,18 @@ export function getComponentModule(elementName: string, namespace: string, attri
// Create instance of the component.
instance = new instanceType();
} catch (ex) {
throw new debug.ScopeError(ex, "Module '" + moduleId + "' not found for element '" + (namespace ? namespace + ":" : "") + elementName + "'.");
throw new ScopeError(ex, "Module '" + moduleId + "' not found for element '" + (namespace ? namespace + ":" : "") + elementName + "'.");
}
if (attributes) {
if (attributes[CODEFILE]) {
if (instance instanceof pages.Page) {
if (instance instanceof Page) {
var codeFilePath = attributes[CODEFILE].trim();
if (codeFilePath.indexOf("~/") === 0) {
codeFilePath = fs.path.join(fs.knownFolders.currentApp().path, codeFilePath.replace("~/", ""));
codeFilePath = path.join(knownFolders.currentApp().path, codeFilePath.replace("~/", ""));
}
try {
exports = require(codeFilePath);
exports = global.loadModule(codeFilePath);
(<any>instance).exports = exports;
} catch (ex) {
throw new Error(`Code file with path "${codeFilePath}" cannot be found!`);
@@ -83,13 +83,13 @@ export function getComponentModule(elementName: string, namespace: string, attri
}
if (attributes[CSSFILE]) {
if (instance instanceof pages.Page) {
if (instance instanceof Page) {
var cssFilePath = attributes[CSSFILE].trim();
if (cssFilePath.indexOf("~/") === 0) {
cssFilePath = fs.path.join(fs.knownFolders.currentApp().path, cssFilePath.replace("~/", ""));
cssFilePath = path.join(knownFolders.currentApp().path, cssFilePath.replace("~/", ""));
}
if (fs.File.exists(cssFilePath)) {
(<pages.Page>instance).addCssFile(cssFilePath);
if (File.exists(cssFilePath)) {
(<Page>instance).addCssFile(cssFilePath);
instance[CSSFILE] = true;
} else {
throw new Error(`Css file with path "${cssFilePath}" cannot be found!`);
@@ -107,7 +107,7 @@ export function getComponentModule(elementName: string, namespace: string, attri
if (attr.indexOf(":") !== -1) {
var platformName = attr.split(":")[0].trim();
if (platformName.toLowerCase() === platform.device.os.toLowerCase()) {
if (platformName.toLowerCase() === device.os.toLowerCase()) {
attr = attr.split(":")[1].trim();
} else {
continue;
@@ -121,12 +121,12 @@ export function getComponentModule(elementName: string, namespace: string, attri
var i: number;
for (i = 0; i < properties.length - 1; i++) {
if (types.isDefined(subObj)) {
if (isDefined(subObj)) {
subObj = subObj[properties[i]];
}
}
if (types.isDefined(subObj)) {
if (isDefined(subObj)) {
setPropertyValue(subObj, instanceModule, exports, subPropName, attrValue);
}
} else {
@@ -140,23 +140,23 @@ export function getComponentModule(elementName: string, namespace: string, attri
return componentModule;
}
export function setPropertyValue(instance: view.View, instanceModule: Object, exports: Object, propertyName: string, propertyValue: string) {
export function setPropertyValue(instance: View, instanceModule: Object, exports: Object, propertyName: string, propertyValue: string) {
// Note: instanceModule can be null if we are loading custom compnenet with no code-behind.
if (isBinding(propertyValue) && instance.bind) {
var bindOptions = bindingBuilder.getBindingOptions(propertyName, getBindingExpressionFromAttribute(propertyValue));
var bindOptions = getBindingOptions(propertyName, getBindingExpressionFromAttribute(propertyValue));
instance.bind({
sourceProperty: bindOptions[bindingBuilder.bindingConstants.sourceProperty],
targetProperty: bindOptions[bindingBuilder.bindingConstants.targetProperty],
expression: bindOptions[bindingBuilder.bindingConstants.expression],
twoWay: bindOptions[bindingBuilder.bindingConstants.twoWay]
}, bindOptions[bindingBuilder.bindingConstants.source]);
} else if (view.isEventOrGesture(propertyName, instance)) {
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.
var handler = exports && exports[propertyValue];
// Check if the handler is function and add it to the instance for specified event name.
if (types.isFunction(handler)) {
if (isFunction(handler)) {
instance.on(propertyName, handler);
}
} else {
@@ -194,7 +194,7 @@ function getBindingExpressionFromAttribute(value: string): string {
function isBinding(value: string): boolean {
var isBinding;
if (types.isString(value)) {
if (isString(value)) {
var str = value.trim();
isBinding = str.indexOf("{{") === 0 && str.lastIndexOf("}}") === str.length - 2;
}

View File

@@ -1,23 +1,23 @@
import definition = require("ui/frame");
import view = require("ui/core/view");
import pages = require("ui/page");
import types = require("utils/types");
import trace = require("trace");
import builder = require("ui/builder");
import fs = require("file-system");
import fileResolverModule = require("file-system/file-name-resolver");
import * as definition from "ui/frame";
import {View, CustomLayoutView} from "ui/core/view";
import {Page} from "ui/page";
import {isString, isFunction, isDefined} from "utils/types";
import * as trace from "trace";
import {load as buildModule} from "ui/builder";
import {knownFolders, path} from "file-system";
import {resolveFileName} from "file-system/file-name-resolver";
var frameStack: Array<Frame> = [];
function buildEntryFromArgs(arg: any): definition.NavigationEntry {
var entry: definition.NavigationEntry;
if (arg instanceof pages.Page) {
if (arg instanceof Page) {
throw new Error("Navigating to a Page instance is no longer supported. Please navigate by using either a module name or a page factory function.");
} else if (types.isString(arg)) {
} else if (isString(arg)) {
entry = {
moduleName: arg
};
} else if (types.isFunction(arg)) {
} else if (isFunction(arg)) {
entry = {
create: arg
}
@@ -45,30 +45,35 @@ export function reloadPage(): void {
}
}
export function resolvePageFromEntry(entry: definition.NavigationEntry): pages.Page {
var page: pages.Page;
export function resolvePageFromEntry(entry: definition.NavigationEntry): Page {
var page: Page;
if (entry.create) {
page = entry.create();
if (!(page && page instanceof pages.Page)) {
if (!(page && page instanceof Page)) {
throw new Error("Failed to create Page with entry.create() function.");
}
}
else if (entry.moduleName) {
// Current app full path.
var currentAppPath = fs.knownFolders.currentApp().path;
var currentAppPath = knownFolders.currentApp().path;
//Full path of the module = current app full path + module name.
var moduleNamePath = fs.path.join(currentAppPath, entry.moduleName);
var moduleNamePath = path.join(currentAppPath, entry.moduleName);
var moduleExports;
var moduleExportsResolvedPath = fileResolverModule.resolveFileName(moduleNamePath, "js");
if (moduleExportsResolvedPath) {
trace.write("Loading JS file: " + moduleExportsResolvedPath, trace.categories.Navigation);
// Exclude extension when doing require.
moduleExportsResolvedPath = moduleExportsResolvedPath.substr(0, moduleExportsResolvedPath.length - 3)
moduleExports = require(moduleExportsResolvedPath);
if (global.moduleExists(entry.moduleName)) {
trace.write("Loading pre-registered JS module: " + entry.moduleName, trace.categories.Navigation);
moduleExports = global.loadModule(entry.moduleName);
} else {
var moduleExportsResolvedPath = resolveFileName(moduleNamePath, "js");
if (moduleExportsResolvedPath) {
trace.write("Loading JS file: " + moduleExportsResolvedPath, trace.categories.Navigation);
// Exclude extension when doing require.
moduleExportsResolvedPath = moduleExportsResolvedPath.substr(0, moduleExportsResolvedPath.length - 3)
moduleExports = global.loadModule(moduleExportsResolvedPath);
}
}
if (moduleExports && moduleExports.createPage) {
@@ -79,12 +84,12 @@ export function resolvePageFromEntry(entry: definition.NavigationEntry): pages.P
page = pageFromBuilder(moduleNamePath, moduleExports);
}
if (!(page && page instanceof pages.Page)) {
if (!(page && page instanceof Page)) {
throw new Error("Failed to load Page from entry.moduleName: " + entry.moduleName);
}
// Possible CSS file path. Add it only if CSS not already specified and loaded from cssFile Page attribute in XML.
var cssFileName = fileResolverModule.resolveFileName(moduleNamePath, "css");
var cssFileName = resolveFileName(moduleNamePath, "css");
if (cssFileName && !page["cssFile"]) {
page.addCssFile(cssFileName);
}
@@ -93,19 +98,19 @@ export function resolvePageFromEntry(entry: definition.NavigationEntry): pages.P
return page;
}
function pageFromBuilder(moduleNamePath: string, moduleExports: any): pages.Page {
var page: pages.Page;
var element: view.View;
function pageFromBuilder(moduleNamePath: string, moduleExports: any): Page {
var page: Page;
var element: View;
// Possible XML file path.
var fileName = fileResolverModule.resolveFileName(moduleNamePath, "xml");
var fileName = resolveFileName(moduleNamePath, "xml");
if (fileName) {
trace.write("Loading XML file: " + fileName, trace.categories.Navigation);
// Or check if the file exists in the app modules and load the page from XML.
element = builder.load(fileName, moduleExports);
if (element instanceof pages.Page) {
page = <pages.Page>element;
element = buildModule(fileName, moduleExports);
if (element instanceof Page) {
page = <Page>element;
}
}
@@ -117,7 +122,7 @@ interface NavigationContext {
isBackNavigation: boolean;
}
export class Frame extends view.CustomLayoutView implements definition.Frame {
export class Frame extends CustomLayoutView implements definition.Frame {
public static androidOptionSelectedEvent = "optionSelected";
private _navigationQueue: Array<NavigationContext>;
@@ -192,7 +197,7 @@ export class Frame extends view.CustomLayoutView implements definition.Frame {
}
}
public _processNavigationQueue(page: pages.Page) {
public _processNavigationQueue(page: Page) {
if (this._navigationQueue.length === 0) {
// This could happen when showing recreated page after activity has been destroyed.
return;
@@ -225,12 +230,12 @@ export class Frame extends view.CustomLayoutView implements definition.Frame {
}
var backstackVisibleValue = entry.entry.backstackVisible;
var backstackHidden = types.isDefined(backstackVisibleValue) && !backstackVisibleValue;
var backstackHidden = isDefined(backstackVisibleValue) && !backstackVisibleValue;
return !backstackHidden;
}
public _updateActionBar(page?: pages.Page) {
public _updateActionBar(page?: Page) {
trace.write("calling _updateActionBar on Frame", trace.categories.Navigation);
}
@@ -299,7 +304,7 @@ export class Frame extends view.CustomLayoutView implements definition.Frame {
return this._backStack.slice();
}
get currentPage(): pages.Page {
get currentPage(): Page {
if (this._currentEntry) {
return this._currentEntry.resolvedPage;
}
@@ -342,18 +347,18 @@ export class Frame extends view.CustomLayoutView implements definition.Frame {
return 0;
}
public _eachChildView(callback: (child: view.View) => boolean) {
public _eachChildView(callback: (child: View) => boolean) {
if (this.currentPage) {
callback(this.currentPage);
}
}
public _getIsAnimatedNavigation(entry: definition.NavigationEntry) {
if (entry && types.isDefined(entry.animated)) {
if (entry && isDefined(entry.animated)) {
return entry.animated;
}
if (types.isDefined(this.animated)) {
if (isDefined(this.animated)) {
return this.animated;
}
@@ -368,17 +373,17 @@ export class Frame extends view.CustomLayoutView implements definition.Frame {
return 0;
}
public _getNavBarVisible(page: pages.Page): boolean {
public _getNavBarVisible(page: Page): boolean {
throw new Error();
}
// We don't need to put Page as visual child. Don't call super.
public _addViewToNativeVisualTree(child: view.View): boolean {
public _addViewToNativeVisualTree(child: View): boolean {
return true;
}
// We don't need to put Page as visual child. Don't call super.
public _removeViewFromNativeVisualTree(child: view.View): void {
public _removeViewFromNativeVisualTree(child: View): void {
child._isAddedToNativeVisualTree = false;
}
}