diff --git a/packages/angular/package.json b/packages/angular/package.json
index d41652de8c..adfd12c617 100644
--- a/packages/angular/package.json
+++ b/packages/angular/package.json
@@ -21,7 +21,7 @@
},
"scripts": {
"build": "npm run clean && npm run compile && npm run clean-generated",
- "build.link": "node scripts/link-copy.js",
+ "build.link": "npm run build && node scripts/link-copy.js",
"clean": "node scripts/clean.js",
"clean-generated": "node ./scripts/clean-generated.js",
"compile": "./node_modules/.bin/ngc",
diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts
index 7aae62cc84..d880a04f28 100644
--- a/packages/angular/src/index.ts
+++ b/packages/angular/src/index.ts
@@ -1,29 +1,24 @@
export { IonicAngularModule } from './module';
+/* Navigation */
+export { IonNav } from './navigation/ion-nav';
+export { IonRouterOutlet } from './navigation/ion-router-outlet';
+export { IonTab } from './navigation/ion-tab';
+export { IonTabs } from './navigation/ion-tabs';
+
/* Directives */
-export { IonNav } from './directives/ion-nav';
export { VirtualScroll } from './directives/virtual-scroll';
export { VirtualItem } from './directives/virtual-item';
export { VirtualHeader } from './directives/virtual-header';
export { VirtualFooter } from './directives/virtual-footer';
-/* Router */
-export { RouterOutlet } from './router/outlet';
-export { AsyncActivateRoutes } from './router/async-activated-routes';
-export { OutletInjector } from './router/outlet-injector';
-export { IonicRouterModule } from './router/router-module';
-
/* Providers */
export { ActionSheetController, ActionSheetProxy } from './providers/action-sheet-controller';
export { AlertController, AlertProxy } from './providers/alert-controller';
-export { AngularComponentMounter } from './providers/angular-component-mounter';
-export { App } from './providers/app';
export { Events } from './providers/events';
export { LoadingController, LoadingProxy } from './providers/loading-controller';
export { MenuController } from './providers/menu-controller';
export { ModalController, ModalProxy } from './providers/modal-controller';
-export { NavController } from './providers/nav-controller';
-export { NavParams } from './providers/nav-params';
export { Platform } from './providers/platform';
export { PopoverController, PopoverProxy } from './providers/popover-controller';
export { ToastController, ToastProxy } from './providers/toast-controller';
diff --git a/packages/angular/src/module.ts b/packages/angular/src/module.ts
index b47145908a..5b81265d00 100644
--- a/packages/angular/src/module.ts
+++ b/packages/angular/src/module.ts
@@ -12,8 +12,13 @@ import { RadioValueAccessor } from './control-value-accessors/radio-value-access
import { SelectValueAccessor } from './control-value-accessors/select-value-accessor';
import { TextValueAccessor } from './control-value-accessors/text-value-accessor';
+/* Navigation */
+import { IonNav } from './navigation/ion-nav';
+import { IonRouterOutlet } from './navigation/ion-router-outlet';
+import { IonTab } from './navigation/ion-tab';
+import { IonTabs } from './navigation/ion-tabs';
+
/* Directives */
-import { IonNav } from './directives/ion-nav';
import { VirtualScroll } from './directives/virtual-scroll';
import { VirtualItem } from './directives/virtual-item';
import { VirtualHeader } from './directives/virtual-header';
@@ -22,8 +27,6 @@ import { VirtualFooter } from './directives/virtual-footer';
/* Providers */
import { ActionSheetController } from './providers/action-sheet-controller';
import { AlertController } from './providers/alert-controller';
-import { AngularComponentMounter } from './providers/angular-component-mounter';
-import { App } from './providers/app';
import { Events, setupProvideEvents } from './providers/events';
import { LoadingController } from './providers/loading-controller';
import { MenuController } from './providers/menu-controller';
@@ -36,6 +39,9 @@ import { ToastController } from './providers/toast-controller';
declarations: [
BooleanValueAccessor,
IonNav,
+ IonRouterOutlet,
+ IonTab,
+ IonTabs,
NumericValueAccessor,
RadioValueAccessor,
SelectValueAccessor,
@@ -48,6 +54,9 @@ import { ToastController } from './providers/toast-controller';
exports: [
BooleanValueAccessor,
IonNav,
+ IonRouterOutlet,
+ IonTab,
+ IonTabs,
NumericValueAccessor,
RadioValueAccessor,
SelectValueAccessor,
@@ -63,8 +72,7 @@ import { ToastController } from './providers/toast-controller';
],
providers: [
ModalController,
- PopoverController,
- AngularComponentMounter
+ PopoverController
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
@@ -77,7 +85,6 @@ export class IonicAngularModule {
providers: [
AlertController,
ActionSheetController,
- App,
Events,
LoadingController,
MenuController,
diff --git a/packages/angular/src/navigation/ion-nav.ts b/packages/angular/src/navigation/ion-nav.ts
new file mode 100644
index 0000000000..87f99236bb
--- /dev/null
+++ b/packages/angular/src/navigation/ion-nav.ts
@@ -0,0 +1,32 @@
+import { Component, ViewEncapsulation } from '@angular/core';
+import {
+ NavigationEnd,
+ NavigationStart,
+ Router
+} from '@angular/router';
+
+
+@Component({
+ selector: 'ion-nav',
+ template: '',
+ styles: [`
+ ion-nav > :not(.show-page) { display: none; }
+ `],
+ encapsulation: ViewEncapsulation.None
+})
+export class IonNav {
+
+ constructor(router: Router) {
+ console.log('ion-nav');
+
+ router.events.subscribe(ev => {
+ if (ev instanceof NavigationStart) {
+ console.log('NavigationStart', ev.url);
+
+ } else if (ev instanceof NavigationEnd) {
+ console.log('NavigationEnd', ev.url);
+ }
+ });
+ }
+
+}
diff --git a/packages/angular/src/navigation/ion-router-outlet.ts b/packages/angular/src/navigation/ion-router-outlet.ts
new file mode 100644
index 0000000000..64e0559e7c
--- /dev/null
+++ b/packages/angular/src/navigation/ion-router-outlet.ts
@@ -0,0 +1,160 @@
+import { Attribute, ChangeDetectorRef, ComponentFactoryResolver, ComponentRef, Directive, EventEmitter, Injector, OnDestroy, OnInit, Output, ViewContainerRef } from '@angular/core';
+import { ActivatedRoute, ChildrenOutletContexts } from '@angular/router';
+import * as ctrl from './router-controller';
+import { runTransition } from './router-transition';
+
+
+@Directive({selector: 'ion-router-outlet', exportAs: 'ionOutlet'})
+export class IonRouterOutlet implements OnDestroy, OnInit {
+ private activated: ComponentRef|null = null;
+ private _activatedRoute: ActivatedRoute|null = null;
+ private name: string;
+
+ private views: ctrl.RouteView[] = [];
+
+ @Output('activate') activateEvents = new EventEmitter();
+ @Output('deactivate') deactivateEvents = new EventEmitter();
+
+ constructor(
+ private parentContexts: ChildrenOutletContexts, private location: ViewContainerRef,
+ private resolver: ComponentFactoryResolver, @Attribute('name') name: string,
+ private changeDetector: ChangeDetectorRef) {
+ this.name = name || 'primary';
+ parentContexts.onChildOutletCreated(this.name, this as any);
+ }
+
+ ngOnDestroy(): void {
+ ctrl.destoryViews(this.views);
+ this.parentContexts.onChildOutletDestroyed(this.name);
+ }
+
+ ngOnInit(): void {
+ if (!this.activated) {
+ // If the outlet was not instantiated at the time the route got activated we need to populate
+ // the outlet when it is initialized (ie inside a NgIf)
+ const context = this.parentContexts.getContext(this.name);
+ if (context && context.route) {
+ if (context.attachRef) {
+ // `attachRef` is populated when there is an existing component to mount
+ this.attach(context.attachRef, context.route);
+ } else {
+ // otherwise the component defined in the configuration is created
+ this.activateWith(context.route, context.resolver || null);
+ }
+ }
+ }
+ }
+
+ get isActivated(): boolean { return !!this.activated; }
+
+ get component(): Object {
+ if (!this.activated) throw new Error('Outlet is not activated');
+ return this.activated.instance;
+ }
+
+ get activatedRoute(): ActivatedRoute {
+ if (!this.activated) throw new Error('Outlet is not activated');
+ return this._activatedRoute as ActivatedRoute;
+ }
+
+ get activatedRouteData() {
+ if (this._activatedRoute) {
+ return this._activatedRoute.snapshot.data;
+ }
+ return {};
+ }
+
+ /**
+ * Called when the `RouteReuseStrategy` instructs to detach the subtree
+ */
+ detach(): ComponentRef {
+ if (!this.activated) throw new Error('Outlet is not activated');
+ this.location.detach();
+ const cmp = this.activated;
+ this.activated = null;
+ this._activatedRoute = null;
+ return cmp;
+ }
+
+ /**
+ * Called when the `RouteReuseStrategy` instructs to re-attach a previously detached subtree
+ */
+ attach(ref: ComponentRef, activatedRoute: ActivatedRoute) {
+ this.activated = ref;
+ this._activatedRoute = activatedRoute;
+
+ ctrl.attachView(this.views, this.location, ref, activatedRoute);
+ }
+
+ deactivate(): void {
+ if (this.activated) {
+ const c = this.component;
+
+ ctrl.deactivateView(this.views, this.activated);
+
+ this.activated = null;
+ this._activatedRoute = null;
+ this.deactivateEvents.emit(c);
+ }
+ }
+
+ activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver|null) {
+ if (this.isActivated) {
+ throw new Error('Cannot activate an already activated outlet');
+ }
+
+ this._activatedRoute = activatedRoute;
+
+ const existingView = ctrl.getExistingView(this.views, activatedRoute);
+ if (existingView) {
+ // we've already got a view hanging around
+ this.activated = existingView.ref;
+
+ } else {
+ // haven't created this view yet
+ const snapshot = (activatedRoute as any)._futureSnapshot;
+
+ const component = snapshot.routeConfig !.component;
+ resolver = resolver || this.resolver;
+
+ const factory = resolver.resolveComponentFactory(component);
+ const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
+
+ const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector);
+ this.activated = this.location.createComponent(factory, this.location.length, injector);
+
+ // keep a ref
+ ctrl.initRouteViewElm(this.views, this.activated, activatedRoute);
+ }
+
+ // Calling `markForCheck` to make sure we will run the change detection when the
+ // `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component.
+ this.changeDetector.markForCheck();
+
+ const lastDeactivatedRef = ctrl.getLastDeactivatedRef(this.views);
+
+ runTransition(this.activated, lastDeactivatedRef).then(() => {
+ console.log('transition end');
+ this.activateEvents.emit(this.activated.instance);
+ });
+ }
+}
+
+
+class OutletInjector implements Injector {
+ constructor(
+ private route: ActivatedRoute, private childContexts: ChildrenOutletContexts,
+ private parent: Injector) {}
+
+ get(token: any, notFoundValue?: any): any {
+ if (token === ActivatedRoute) {
+ return this.route;
+ }
+
+ if (token === ChildrenOutletContexts) {
+ return this.childContexts;
+ }
+
+ return this.parent.get(token, notFoundValue);
+ }
+}
diff --git a/packages/angular/src/navigation/ion-tab.ts b/packages/angular/src/navigation/ion-tab.ts
new file mode 100644
index 0000000000..5cdfab7dc6
--- /dev/null
+++ b/packages/angular/src/navigation/ion-tab.ts
@@ -0,0 +1,17 @@
+import { Directive, ElementRef, Input, OnInit } from '@angular/core';
+
+
+@Directive({
+ selector: 'ion-tab'
+})
+export class IonTab implements OnInit {
+
+ @Input() tabLink: string;
+
+ constructor(private elementRef: ElementRef) {}
+
+ ngOnInit() {
+ console.log('routerLink', this.tabLink, this.elementRef.nativeElement);
+ }
+
+}
diff --git a/packages/angular/src/navigation/ion-tabs.ts b/packages/angular/src/navigation/ion-tabs.ts
new file mode 100644
index 0000000000..6509c38d12
--- /dev/null
+++ b/packages/angular/src/navigation/ion-tabs.ts
@@ -0,0 +1,25 @@
+import { Directive, HostListener } from '@angular/core';
+import { Router } from '@angular/router';
+
+
+@Directive({
+ selector: 'ion-tabs'
+})
+export class IonTabs {
+
+ constructor(private router: Router) {}
+
+ @HostListener('ionTabbarClick', ['$event'])
+ ionTabbarClick(ev: UIEvent) {
+ console.log('ionTabbarClick', ev);
+
+ const tabElm: HTMLIonTabElement = ev.detail as any;
+ if (tabElm && tabElm.href) {
+ console.log('tabElm', tabElm.href);
+
+ this.router.navigateByUrl(tabElm.href);
+ }
+ }
+
+}
+
diff --git a/packages/angular/src/navigation/router-controller.ts b/packages/angular/src/navigation/router-controller.ts
new file mode 100644
index 0000000000..08fdf330c8
--- /dev/null
+++ b/packages/angular/src/navigation/router-controller.ts
@@ -0,0 +1,81 @@
+import { ComponentRef, ViewContainerRef } from '@angular/core';
+import { ActivatedRoute, UrlSegment } from '@angular/router';
+
+
+export function attachView(views: RouteView[], location: ViewContainerRef, ref: ComponentRef, activatedRoute: ActivatedRoute) {
+ initRouteViewElm(views, ref, activatedRoute);
+ location.insert(ref.hostView);
+}
+
+
+export function initRouteViewElm(views: RouteView[], ref: ComponentRef, activatedRoute: ActivatedRoute) {
+ views.push({
+ ref: ref,
+ urlKey: getUrlKey(activatedRoute),
+ deactivatedId: -1
+ });
+
+ (ref.location.nativeElement as HTMLElement).classList.add('ion-page');
+}
+
+
+export function getExistingView(views: RouteView[], activatedRoute: ActivatedRoute) {
+ return views.find(vw => {
+ return isMatchingActivatedRoute(vw.urlKey, activatedRoute);
+ });
+}
+
+
+function isMatchingActivatedRoute(existingUrlKey: string, activatedRoute: ActivatedRoute) {
+ const activatedUrlKey = getUrlKey(activatedRoute);
+
+ return activatedUrlKey === existingUrlKey;
+}
+
+
+export function getLastDeactivatedRef(views: RouteView[]) {
+ if (views.length < 2) {
+ return null;
+ }
+
+ return views.sort((a, b) => {
+ if (a.deactivatedId > b.deactivatedId) return -1;
+ if (a.deactivatedId < b.deactivatedId) return 1;
+ return 0;
+ })[0].ref;
+}
+
+
+function getUrlKey(activatedRoute: ActivatedRoute) {
+ const url: UrlSegment[] = (activatedRoute.url as any).value;
+
+ return url.map(u => {
+ return u.path + '$$' + JSON.stringify(u.parameters);
+ }).join('/');
+}
+
+
+export function deactivateView(views: RouteView[], ref: ComponentRef) {
+ const view = views.find(vw => vw.ref === ref);
+ if (view) {
+ view.deactivatedId = deactivatedIds++;
+ }
+}
+
+
+export function destoryViews(views: RouteView[]) {
+ views.forEach(vw => {
+ vw.ref.destroy();
+ });
+ views.length = 0;
+}
+
+
+export interface RouteView {
+ urlKey: string;
+ ref: ComponentRef;
+ deactivatedId: number;
+}
+
+
+let deactivatedIds = 0;
diff --git a/packages/angular/src/navigation/router-transition.ts b/packages/angular/src/navigation/router-transition.ts
new file mode 100644
index 0000000000..c9a589560d
--- /dev/null
+++ b/packages/angular/src/navigation/router-transition.ts
@@ -0,0 +1,34 @@
+import { ComponentRef } from '@angular/core';
+
+
+export function runTransition(enteringRef: ComponentRef, leavingRef: ComponentRef): Promise {
+ const enteringElm = (enteringRef && enteringRef.location && enteringRef.location.nativeElement);
+ const leavingElm = (leavingRef && leavingRef.location && leavingRef.location.nativeElement);
+
+ if (!enteringElm && !leavingElm) {
+ return Promise.resolve();
+ }
+
+ return transition(enteringElm, leavingElm);
+}
+
+
+function transition(enteringElm: HTMLElement, leavingElm: HTMLElement): Promise {
+ console.log('transition start');
+
+ return new Promise(resolve => {
+
+ setTimeout(() => {
+
+ if (enteringElm) {
+ enteringElm.classList.add('show-page');
+ }
+
+ if (leavingElm) {
+ leavingElm.classList.remove('show-page');
+ }
+
+ resolve();
+ }, 750);
+ });
+}