diff --git a/e2e/renderer/app/action-bar/action-bar-dynamic-items.component.ts b/e2e/renderer/app/action-bar/action-bar-dynamic-items.component.ts
new file mode 100644
index 000000000..0614e0798
--- /dev/null
+++ b/e2e/renderer/app/action-bar/action-bar-dynamic-items.component.ts
@@ -0,0 +1,27 @@
+import { Component } from "@angular/core";
+
+@Component({
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+})
+export class ActionBarDynamicItemsComponent {
+ public showNavigationButton = true;
+ public show1 = true;
+ public show2 = true;
+}
+
diff --git a/e2e/renderer/app/action-bar/action-bar-extension.component.ts b/e2e/renderer/app/action-bar/action-bar-extension.component.ts
new file mode 100644
index 000000000..7d3b9b8e8
--- /dev/null
+++ b/e2e/renderer/app/action-bar/action-bar-extension.component.ts
@@ -0,0 +1,16 @@
+import { Component } from "@angular/core";
+
+@Component({
+ template: `
+
+
+
+
+
+
+
+ `
+})
+export class ActionBarExtensionComponent {
+ public show = true;
+}
diff --git a/e2e/renderer/app/app-routing.module.ts b/e2e/renderer/app/app-routing.module.ts
index 576923862..a975d5be8 100644
--- a/e2e/renderer/app/app-routing.module.ts
+++ b/e2e/renderer/app/app-routing.module.ts
@@ -1,6 +1,9 @@
import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core";
import { NativeScriptRouterModule } from "nativescript-angular/router";
+import { ActionBarDynamicItemsComponent } from "./action-bar/action-bar-dynamic-items.component";
+import { ActionBarExtensionComponent } from "./action-bar/action-bar-extension.component";
+
import { ListComponent } from "./list.component";
import { NgForComponent } from "./ngfor.component";
import { NgForOfComponent } from "./ngforof.component";
@@ -17,6 +20,14 @@ export const routes = [
redirectTo: "/list",
pathMatch: "full"
},
+ {
+ path: "action-bar-dynamic",
+ component: ActionBarDynamicItemsComponent,
+ },
+ {
+ path: "action-bar-extension",
+ component: ActionBarExtensionComponent,
+ },
{
path: "list",
component: ListComponent,
@@ -56,6 +67,9 @@ export const routes = [
];
export const navigatableComponents = [
+ ActionBarDynamicItemsComponent,
+ ActionBarExtensionComponent,
+
ListComponent,
NgForComponent,
NgForOfComponent,
diff --git a/e2e/renderer/app/content-view.component.ts b/e2e/renderer/app/content-view.component.ts
index 6ec92a1ba..a2aeb50ba 100644
--- a/e2e/renderer/app/content-view.component.ts
+++ b/e2e/renderer/app/content-view.component.ts
@@ -1,7 +1,6 @@
import { Component } from "@angular/core";
@Component({
- selector: "my-app",
template: `
diff --git a/e2e/renderer/app/list.component.ts b/e2e/renderer/app/list.component.ts
index ec4ca839c..ddbaa8b95 100644
--- a/e2e/renderer/app/list.component.ts
+++ b/e2e/renderer/app/list.component.ts
@@ -3,6 +3,8 @@ import { Component } from "@angular/core";
@Component({
template: `
+
+
diff --git a/e2e/renderer/e2e/action-bar.e2e-spec.ts b/e2e/renderer/e2e/action-bar.e2e-spec.ts
new file mode 100644
index 000000000..20f63d24b
--- /dev/null
+++ b/e2e/renderer/e2e/action-bar.e2e-spec.ts
@@ -0,0 +1,163 @@
+import {
+ AppiumDriver,
+ createDriver,
+ SearchOptions,
+ elementHelper,
+} from "nativescript-dev-appium";
+
+import { isOnTheLeft } from "./helpers/location";
+import { DriverWrapper, ExtendedUIElement } from "./helpers/appium-elements";
+
+describe("Action Bar scenario", () => {
+ let driver: AppiumDriver;
+ let driverWrapper: DriverWrapper;
+
+ describe("dynamically add/remove ActionItems", async () => {
+ let firstActionItem: ExtendedUIElement;
+ let secondActionItem: ExtendedUIElement;
+ let toggleFirstButton: ExtendedUIElement;
+ let toggleSecondButton: ExtendedUIElement;
+
+ before(async () => {
+ driver = await createDriver();
+ driverWrapper = new DriverWrapper(driver);
+ });
+
+ after(async () => {
+ await driver.quit();
+ console.log("Driver quits!");
+ });
+
+ it("should navigate to page", async () => {
+ const navigationButton =
+ await driverWrapper.findElementByText("ActionBar dynamic", SearchOptions.exact);
+ await navigationButton.click();
+
+ const actionBar =
+ await driverWrapper.findElementByText("Action Bar Dynamic Items", SearchOptions.exact);
+ });
+
+ it("should find elements", async () => {
+ firstActionItem = await driverWrapper.findElementByText("one");
+ secondActionItem = await driverWrapper.findElementByText("two");
+
+ toggleFirstButton = await driverWrapper.findElementByText("toggle 1");
+ toggleSecondButton = await driverWrapper.findElementByText("toggle 2");
+ });
+
+ it("should initially render the action items in the correct order", async () => {
+ await checkOrderIsCorrect();
+ });
+
+ it("should detach first element when its condition is false", done => {
+ (async () => {
+ await toggleFirst();
+
+ try {
+ await driverWrapper.findElementByText("one", SearchOptions.exact);
+ } catch (e) {
+ done();
+ }
+ })();
+ });
+
+ it("should attach first element in the correct position", async () => {
+ await toggleFirst();
+ await checkOrderIsCorrect();
+ });
+
+ it("should detach second element when its condition is false", done => {
+ (async () => {
+ await toggleSecond();
+
+ try {
+ await driverWrapper.findElementByText("two", SearchOptions.exact);
+ } catch (e) {
+ done();
+ }
+ })();
+ });
+
+ it("should attach second element in the correct position", async () => {
+ await toggleSecond();
+ await checkOrderIsCorrect();
+ });
+
+ it("should detach and then reattach both at correct places", async () => {
+ await toggleFirst();
+ await toggleSecond();
+
+ await toggleFirst();
+ await toggleSecond();
+
+ await checkOrderIsCorrect();
+ });
+
+ const checkOrderIsCorrect = async () => {
+ await isOnTheLeft(firstActionItem, secondActionItem);
+ };
+
+ const toggleFirst = async () => {
+ toggleFirstButton = await toggleFirstButton.refetch();
+ await toggleFirstButton.click();
+ };
+
+ const toggleSecond = async () => {
+ toggleSecondButton = await toggleSecondButton.refetch();
+ await toggleSecondButton.click();
+ };
+
+ });
+
+ describe("Action Bar extension with dynamic ActionItem", async () => {
+ let toggleButton: ExtendedUIElement;
+ let conditional: ExtendedUIElement;
+
+ before(async () => {
+ driver = await createDriver();
+ driverWrapper = new DriverWrapper(driver);
+ });
+
+ after(async () => {
+ await driver.quit();
+ console.log("Driver quits!");
+ });
+
+ it("should navigate to page", async () => {
+ const navigationButton =
+ await driverWrapper.findElementByText("ActionBarExtension", SearchOptions.exact);
+ await navigationButton.click();
+ });
+
+ it("should find elements", async () => {
+ toggleButton = await driverWrapper.findElementByText("toggle");
+ conditional = await driverWrapper.findElementByText("conditional");
+ });
+
+ it("should detach conditional action item when its condition is false", done => {
+ (async () => {
+ await toggle();
+
+ try {
+ await driverWrapper.findElementByText("conditional", SearchOptions.exact);
+ } catch (e) {
+ done();
+ }
+ })();
+ });
+
+ it("should reattach conditional action item at correct place", async () => {
+ await toggle();
+ await checkOrderIsCorrect();
+ });
+
+ const checkOrderIsCorrect = async () => {
+ await isOnTheLeft(toggleButton, conditional);
+ };
+
+ const toggle = async () => {
+ toggleButton = await toggleButton.refetch();
+ await toggleButton.click();
+ };
+ });
+});
diff --git a/e2e/renderer/e2e/helpers/appium-elements.ts b/e2e/renderer/e2e/helpers/appium-elements.ts
index 26a020a4d..203808169 100644
--- a/e2e/renderer/e2e/helpers/appium-elements.ts
+++ b/e2e/renderer/e2e/helpers/appium-elements.ts
@@ -38,4 +38,25 @@ export class DriverWrapper {
return result;
}
+
+ @refetchable()
+ async findElementByXPath(...args: any[]): Promise {
+ const result = await (this.driver).findElementByXPath(...args);
+
+ return result;
+ }
+
+ @refetchable()
+ async findElementsByXPath(...args: any[]): Promise {
+ const result = await (this.driver).findElementsByXPath(...args);
+
+ return result || [];
+ }
+
+ @refetchable()
+ async findElementsByClassName(...args: any[]): Promise {
+ const result = await (this.driver).findElementsByClassName(...args);
+
+ return result || [];
+ }
}
diff --git a/e2e/renderer/e2e/helpers/location.ts b/e2e/renderer/e2e/helpers/location.ts
index 189dfbbcc..df2a7cc67 100644
--- a/e2e/renderer/e2e/helpers/location.ts
+++ b/e2e/renderer/e2e/helpers/location.ts
@@ -11,3 +11,14 @@ export const isAbove = async (first: ExtendedUIElement, second: ExtendedUIElemen
assert.isTrue(firstY < secondY);
}
+
+export const isOnTheLeft = async (first: ExtendedUIElement, second: ExtendedUIElement) => {
+ first = await first.refetch();
+ second = await second.refetch();
+
+ const { x: firstX } = await first.location();
+ const { x: secondX } = await second.location();
+
+ assert.isTrue(firstX < secondX);
+}
+
diff --git a/nativescript-angular/directives/action-bar.ts b/nativescript-angular/directives/action-bar.ts
index 5158a1532..1b9053dd2 100644
--- a/nativescript-angular/directives/action-bar.ts
+++ b/nativescript-angular/directives/action-bar.ts
@@ -1,54 +1,87 @@
import { Directive, Component, ElementRef, Optional, OnDestroy } from "@angular/core";
-import { ActionItem, ActionBar, NavigationButton } from "tns-core-modules/ui/action-bar";
+import {
+ ActionBar,
+ ActionItem,
+ ActionItems,
+ NavigationButton,
+} from "tns-core-modules/ui/action-bar";
import { Page } from "tns-core-modules/ui/page";
-import { View } from "tns-core-modules/ui/core/view";
import { isBlank } from "../lang-facade";
import {
- InvisibleNode,
NgView,
ViewClassMeta,
+ ViewExtensions,
+ isInvisibleNode,
+ isView,
registerElement,
} from "../element-registry";
+export function isActionItem(view: any): view is ActionItem {
+ return view instanceof ActionItem;
+}
+
+export function isNavigationButton(view: any): view is NavigationButton {
+ return view instanceof NavigationButton;
+}
+
+type NgActionBar = (ActionBar & ViewExtensions);
+
const actionBarMeta: ViewClassMeta = {
skipAddToDom: true,
- insertChild: (parent: NgView, child: NgView) => {
- const bar = (parent);
- const childView = child;
-
- if (child instanceof InvisibleNode) {
+ insertChild: (parent: NgActionBar, child: NgView, next: any) => {
+ if (isInvisibleNode(child)) {
return;
- } else if (child instanceof NavigationButton) {
- bar.navigationButton = childView;
- childView.parent = bar;
- } else if (child instanceof ActionItem) {
- bar.actionItems.addItem(childView);
- childView.parent = bar;
- } else if (child instanceof View) {
- bar.titleView = childView;
+ } else if (isNavigationButton(child)) {
+ parent.navigationButton = child;
+ child.templateParent = parent;
+ } else if (isActionItem(child)) {
+ addActionItem(parent, child, next);
+ child.templateParent = parent;
+ } else if (isView(child)) {
+ parent.titleView = child;
}
},
- removeChild: (parent: NgView, child: NgView) => {
- const bar = (parent);
- const childView = child;
-
- if (child instanceof InvisibleNode) {
+ removeChild: (parent: NgActionBar, child: NgView) => {
+ if (isInvisibleNode(child)) {
return;
- } else if (child instanceof NavigationButton) {
- if (bar.navigationButton === childView) {
- bar.navigationButton = null;
+ } else if (isNavigationButton(child)) {
+ if (parent.navigationButton === child) {
+ parent.navigationButton = null;
}
- childView.parent = null;
- } else if (child instanceof ActionItem) {
- bar.actionItems.removeItem(childView);
- childView.parent = null;
- } else if (child instanceof View && bar.titleView && bar.titleView === childView) {
- bar.titleView = null;
+
+ child.templateParent = null;
+ } else if (isActionItem(child)) {
+ parent.actionItems.removeItem(child);
+ child.templateParent = null;
+ } else if (isView(child) && parent.titleView && parent.titleView === child) {
+ parent.titleView = null;
}
},
};
+const addActionItem = (bar: NgActionBar, item: ActionItem, next: ActionItem) => {
+ if (next) {
+ insertActionItemBefore(bar, item, next);
+ } else {
+ appendActionItem(bar, item);
+ }
+};
+
+const insertActionItemBefore = (bar: NgActionBar, item: ActionItem, next: ActionItem) => {
+ const actionItems: ActionItems = bar.actionItems;
+ const actionItemsCollection: ActionItem[] = actionItems.getItems();
+
+ const indexToInsert = actionItemsCollection.indexOf(next);
+ actionItemsCollection.splice(indexToInsert, 0, item);
+
+ (actionItems).setItems(actionItemsCollection);
+};
+
+const appendActionItem = (bar: NgActionBar, item: ActionItem) => {
+ bar.actionItems.addItem(item);
+};
+
registerElement("ActionBar", () => require("ui/action-bar").ActionBar, actionBarMeta);
registerElement("ActionItem", () => require("ui/action-bar").ActionItem);
registerElement("NavigationButton", () => require("ui/action-bar").NavigationButton);
diff --git a/nativescript-angular/element-registry.ts b/nativescript-angular/element-registry.ts
index 8c0fad5f1..35f111cf8 100644
--- a/nativescript-angular/element-registry.ts
+++ b/nativescript-angular/element-registry.ts
@@ -72,14 +72,22 @@ const getClassName = instance => instance.constructor.name;
export interface ViewClassMeta {
skipAddToDom?: boolean;
- insertChild?: (parent: NgView, child: NgView) => void;
- removeChild?: (parent: NgView, child: NgView) => void;
+ insertChild?: (parent: any, child: any, next?: any) => void;
+ removeChild?: (parent: any, child: any) => void;
}
export function isDetachedElement(element): boolean {
return (element && element.meta && element.meta.skipAddToDom);
}
+export function isView(view: any): view is NgView {
+ return view instanceof View;
+}
+
+export function isInvisibleNode(view: any): view is InvisibleNode {
+ return view instanceof InvisibleNode;
+}
+
export type ViewResolver = () => ViewClass;
const elementMap = new Map();
diff --git a/nativescript-angular/view-util.ts b/nativescript-angular/view-util.ts
index 5532a39dc..f1075bac0 100644
--- a/nativescript-angular/view-util.ts
+++ b/nativescript-angular/view-util.ts
@@ -12,7 +12,9 @@ import {
getViewClass,
getViewMeta,
isDetachedElement,
+ isInvisibleNode,
isKnownView,
+ isView,
} from "./element-registry";
import { platformNames, Device } from "tns-core-modules/platform";
@@ -29,10 +31,6 @@ export type NgContentView = ContentView & ViewExtensions;
export type NgPlaceholder = Placeholder & ViewExtensions;
export type BeforeAttachAction = (view: View) => void;
-export function isView(view: any): view is NgView {
- return view instanceof View;
-}
-
export function isLayout(view: any): view is NgLayoutBase {
return view instanceof LayoutBase;
}
@@ -53,23 +51,30 @@ export class ViewUtil {
}
public insertChild(
- parent: NgView,
- child: NgView,
- previous: NgView = parent.lastChild,
+ parent: View,
+ child: View,
+ previous?: NgView,
next?: NgView
) {
if (!parent) {
return;
}
- this.addToQueue(parent, child, previous, next);
+ const extendedParent = this.ensureNgViewExtensions(parent);
+ const extendedChild = this.ensureNgViewExtensions(child);
- if (child instanceof InvisibleNode) {
- child.templateParent = parent;
+ if (!previous) {
+ previous = extendedParent.lastChild;
+ }
+ this.addToQueue(extendedParent, extendedChild, previous, next);
+
+ if (isInvisibleNode(child)) {
+ extendedChild.templateParent = extendedParent;
}
if (!isDetachedElement(child)) {
- this.addToVisualTree(parent, child, next);
+ const nextVisual = this.findNextVisual(next);
+ this.addToVisualTree(extendedParent, extendedChild, nextVisual);
}
}
@@ -79,6 +84,9 @@ export class ViewUtil {
previous: NgView,
next: NgView
): void {
+ traceLog(`ViewUtil.addToQueue parent: ${parent}, view: ${child}, ` +
+ `previous: ${previous}, next: ${next}`);
+
if (previous) {
previous.nextSibling = child;
} else {
@@ -94,6 +102,7 @@ export class ViewUtil {
private appendToQueue(parent: NgView, view: NgView) {
traceLog(`ViewUtil.appendToQueue parent: ${parent} view: ${view}`);
+
if (parent.lastChild) {
parent.lastChild.nextSibling = view;
}
@@ -102,8 +111,10 @@ export class ViewUtil {
}
private addToVisualTree(parent: NgView, child: NgView, next: NgView): void {
+ traceLog(`ViewUtil.addToVisualTree parent: ${parent}, view: ${child}, next: ${next}`);
+
if (parent.meta && parent.meta.insertChild) {
- parent.meta.insertChild(parent, child);
+ parent.meta.insertChild(parent, child, next);
} else if (isLayout(parent)) {
this.insertToLayout(parent, child, next);
} else if (isContentView(parent)) {
@@ -131,7 +142,7 @@ export class ViewUtil {
}
}
- private findNextVisual(view: NgView) {
+ private findNextVisual(view: NgView): NgView {
let next = view;
while (next && isDetachedElement(next)) {
next = next.nextSibling;
@@ -140,37 +151,22 @@ export class ViewUtil {
return next;
}
- public removeChild(parent: NgView, child: NgView) {
+ public removeChild(parent: View, child: View) {
+ traceLog(`ViewUtil.removeChild parent: ${parent} child: ${child}`);
+
if (!parent) {
return;
}
- if (parent.meta && parent.meta.removeChild) {
- parent.meta.removeChild(parent, child);
- } else if (isLayout(parent)) {
- this.removeLayoutChild(parent, child);
- } else if (isContentView(parent) && parent.content === child) {
- parent.content = null;
- parent.lastChild = null;
- parent.firstChild = null;
- } else if (isView(parent)) {
- parent._removeView(child);
- }
- }
+ const extendedParent = this.ensureNgViewExtensions(parent);
+ const extendedChild = this.ensureNgViewExtensions(child);
- private removeLayoutChild(parent: NgLayoutBase, child: NgView): void {
- const index = parent.getChildIndex(child);
- this.removeFromQueue(parent, child, index);
- if (index === -1) {
- return;
- }
-
- parent.removeChild(child);
+ this.removeFromQueue(extendedParent, extendedChild);
+ this.removeFromVisualTree(extendedParent, extendedChild);
}
- private removeFromQueue(parent: NgLayoutBase, child: NgView, index: number) {
- traceLog(`ViewUtil.removeFromQueue ` +
- `parent: ${parent} child: ${child} index: ${index}`);
+ private removeFromQueue(parent: NgView, child: NgView) {
+ traceLog(`ViewUtil.removeFromQueue parent: ${parent} child: ${child}`);
if (parent.firstChild === child && parent.lastChild === child) {
parent.firstChild = null;
@@ -182,7 +178,7 @@ export class ViewUtil {
parent.firstChild = child.nextSibling;
}
- const previous = this.findPreviousElement(parent, child, index);
+ const previous = this.findPreviousElement(parent, child);
if (parent.lastChild === child) {
parent.lastChild = previous;
}
@@ -193,8 +189,14 @@ export class ViewUtil {
}
// NOTE: This one is O(n) - use carefully
- private findPreviousElement(parent: NgLayoutBase, child: NgView, elementIndex: number): NgView {
- const previousVisual = this.getPreviousVisualElement(parent, elementIndex);
+ private findPreviousElement(parent: NgView, child: NgView): NgView {
+ traceLog(`ViewUtil.findPreviousElement parent: ${parent} child: ${child}`);
+
+ let previousVisual;
+ if (isLayout(parent)) {
+ previousVisual = this.getPreviousVisualElement(parent, child);
+ }
+
let previous = previousVisual || parent.firstChild;
// since detached elements are not added to the visual tree,
@@ -207,7 +209,9 @@ export class ViewUtil {
return previous;
}
- private getPreviousVisualElement(parent: NgLayoutBase, elementIndex: number): NgView {
+ private getPreviousVisualElement(parent: NgLayoutBase, child: NgView): NgView {
+ const elementIndex = parent.getChildIndex(child);
+
if (elementIndex > 0) {
return parent.getChildAt(elementIndex - 1) as NgView;
}
@@ -222,6 +226,30 @@ export class ViewUtil {
}
}
+ private removeFromVisualTree(parent: NgView, child: NgView) {
+ traceLog(`ViewUtil.findPreviousElement parent: ${parent} child: ${child}`);
+
+ if (parent.meta && parent.meta.removeChild) {
+ parent.meta.removeChild(parent, child);
+ } else if (isLayout(parent)) {
+ this.removeLayoutChild(parent, child);
+ } else if (isContentView(parent) && parent.content === child) {
+ parent.content = null;
+ parent.lastChild = null;
+ parent.firstChild = null;
+ } else if (isView(parent)) {
+ parent._removeView(child);
+ }
+ }
+
+ private removeLayoutChild(parent: NgLayoutBase, child: NgView): void {
+ const index = parent.getChildIndex(child);
+
+ if (index !== -1) {
+ parent.removeChild(child);
+ }
+ }
+
public createComment(): InvisibleNode {
return new CommentNode();
}
@@ -238,16 +266,34 @@ export class ViewUtil {
}
const viewClass = getViewClass(name);
- let view = new viewClass();
- view.nodeName = name;
- view.meta = getViewMeta(name);
+ const view = new viewClass();
+ const ngView = this.setNgViewExtensions(view, name);
+
+ return ngView;
+ }
+
+ private ensureNgViewExtensions(view: View): NgView {
+ if (view.hasOwnProperty("meta")) {
+ return view as NgView;
+ } else {
+ const name = view.typeName;
+ const ngView = this.setNgViewExtensions(view, name);
+
+ return ngView;
+ }
+ }
+
+ private setNgViewExtensions(view: View, name: string): NgView {
+ const ngView = view as NgView;
+ ngView.nodeName = name;
+ ngView.meta = getViewMeta(name);
// we're setting the node type of the view
// to 'element' because of checks done in the
// dom animation engine
- view.nodeType = ELEMENT_NODE_TYPE;
+ ngView.nodeType = ELEMENT_NODE_TYPE;
- return view;
+ return ngView;
}
public setProperty(view: NgView, attributeName: string, value: any, namespace?: string): void {
@@ -363,4 +409,3 @@ export class ViewUtil {
view.style[styleName] = unsetValue;
}
}
-