diff --git a/.gitignore b/.gitignore
index 9995f912d..7b21aa525 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,7 @@ tags
.DS_Store
npm-debug.log
nativescript-angular*.tgz
+package-lock.json
tests/app/**/*.js
tests/test-output.txt
diff --git a/e2e/router/.gitignore b/e2e/router/.gitignore
new file mode 100644
index 000000000..d3df84cc8
--- /dev/null
+++ b/e2e/router/.gitignore
@@ -0,0 +1,8 @@
+platforms
+node_modules
+hooks
+
+app/**/*.js
+e2e/**/*.js
+test-results.xml
+
diff --git a/e2e/router/app/App_Resources/Android/AndroidManifest.xml b/e2e/router/app/App_Resources/Android/AndroidManifest.xml
new file mode 100644
index 000000000..9db832151
--- /dev/null
+++ b/e2e/router/app/App_Resources/Android/AndroidManifest.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/e2e/router/app/App_Resources/Android/app.gradle b/e2e/router/app/App_Resources/Android/app.gradle
new file mode 100644
index 000000000..8df47fb37
--- /dev/null
+++ b/e2e/router/app/App_Resources/Android/app.gradle
@@ -0,0 +1,23 @@
+// Add your native dependencies here:
+
+// Uncomment to add recyclerview-v7 dependency
+//dependencies {
+// compile 'com.android.support:recyclerview-v7:+'
+//}
+
+android {
+ defaultConfig {
+ generatedDensities = []
+ applicationId = "org.nativescript.router"
+
+ //override supported platforms
+ // ndk {
+ // abiFilters.clear()
+ // abiFilters "armeabi-v7a"
+ // }
+
+ }
+ aaptOptions {
+ additionalParameters "--no-version-vectors"
+ }
+}
diff --git a/e2e/router/app/App_Resources/Android/drawable-nodpi/background.png b/e2e/router/app/App_Resources/Android/drawable-nodpi/background.png
new file mode 100644
index 000000000..748b2adf5
Binary files /dev/null and b/e2e/router/app/App_Resources/Android/drawable-nodpi/background.png differ
diff --git a/e2e/router/app/App_Resources/Android/drawable-nodpi/icon.png b/e2e/router/app/App_Resources/Android/drawable-nodpi/icon.png
new file mode 100755
index 000000000..ddfc17a71
Binary files /dev/null and b/e2e/router/app/App_Resources/Android/drawable-nodpi/icon.png differ
diff --git a/e2e/router/app/App_Resources/Android/drawable-nodpi/logo.png b/e2e/router/app/App_Resources/Android/drawable-nodpi/logo.png
new file mode 100644
index 000000000..b9e102a76
Binary files /dev/null and b/e2e/router/app/App_Resources/Android/drawable-nodpi/logo.png differ
diff --git a/e2e/router/app/App_Resources/Android/drawable-nodpi/splash_screen.xml b/e2e/router/app/App_Resources/Android/drawable-nodpi/splash_screen.xml
new file mode 100644
index 000000000..ada77f92c
--- /dev/null
+++ b/e2e/router/app/App_Resources/Android/drawable-nodpi/splash_screen.xml
@@ -0,0 +1,8 @@
+
+ -
+
+
+ -
+
+
+
\ No newline at end of file
diff --git a/e2e/router/app/App_Resources/Android/values-v21/colors.xml b/e2e/router/app/App_Resources/Android/values-v21/colors.xml
new file mode 100644
index 000000000..a64641a9d
--- /dev/null
+++ b/e2e/router/app/App_Resources/Android/values-v21/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #3d5afe
+
\ No newline at end of file
diff --git a/e2e/router/app/App_Resources/Android/values-v21/styles.xml b/e2e/router/app/App_Resources/Android/values-v21/styles.xml
new file mode 100644
index 000000000..dac8727c8
--- /dev/null
+++ b/e2e/router/app/App_Resources/Android/values-v21/styles.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/e2e/router/app/App_Resources/Android/values/colors.xml b/e2e/router/app/App_Resources/Android/values/colors.xml
new file mode 100644
index 000000000..74ad8829c
--- /dev/null
+++ b/e2e/router/app/App_Resources/Android/values/colors.xml
@@ -0,0 +1,7 @@
+
+
+ #F5F5F5
+ #757575
+ #33B5E5
+ #272734
+
\ No newline at end of file
diff --git a/e2e/router/app/App_Resources/Android/values/styles.xml b/e2e/router/app/App_Resources/Android/values/styles.xml
new file mode 100644
index 000000000..1e8c7f29b
--- /dev/null
+++ b/e2e/router/app/App_Resources/Android/values/styles.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..1953734f4
--- /dev/null
+++ b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,92 @@
+{
+ "images" : [
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "icon-29.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "icon-29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "icon-29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "icon-40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "icon-40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "icon-60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "icon-60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "icon-29.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "icon-29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "icon-40.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "icon-40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "icon-76.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "icon-76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "icon-83.5@2x.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29.png
new file mode 100644
index 000000000..9e15af09d
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png
new file mode 100644
index 000000000..7b9e55537
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png
new file mode 100644
index 000000000..76f61ec1f
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40.png
new file mode 100644
index 000000000..15b06db11
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png
new file mode 100644
index 000000000..585065f94
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png
new file mode 100644
index 000000000..a450c421d
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png
new file mode 100644
index 000000000..457b6d94c
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png
new file mode 100644
index 000000000..fa5a6ac86
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76.png
new file mode 100644
index 000000000..94abcf70d
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png
new file mode 100644
index 000000000..2e71dd3a0
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png
new file mode 100644
index 000000000..4abc9ec50
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/Contents.json b/e2e/router/app/App_Resources/iOS/Assets.xcassets/Contents.json
new file mode 100644
index 000000000..da4a164c9
--- /dev/null
+++ b/e2e/router/app/App_Resources/iOS/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Contents.json b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Contents.json
new file mode 100644
index 000000000..4414bad08
--- /dev/null
+++ b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Contents.json
@@ -0,0 +1,158 @@
+{
+ "images" : [
+ {
+ "extent" : "full-screen",
+ "idiom" : "iphone",
+ "subtype" : "736h",
+ "filename" : "Default-736h@3x.png",
+ "minimum-system-version" : "8.0",
+ "orientation" : "portrait",
+ "scale" : "3x"
+ },
+ {
+ "extent" : "full-screen",
+ "idiom" : "iphone",
+ "subtype" : "736h",
+ "filename" : "Default-Landscape@3x.png",
+ "minimum-system-version" : "8.0",
+ "orientation" : "landscape",
+ "scale" : "3x"
+ },
+ {
+ "extent" : "full-screen",
+ "idiom" : "iphone",
+ "subtype" : "667h",
+ "filename" : "Default-667h@2x.png",
+ "minimum-system-version" : "8.0",
+ "orientation" : "portrait",
+ "scale" : "2x"
+ },
+ {
+ "orientation" : "portrait",
+ "idiom" : "iphone",
+ "filename" : "Default@2x.png",
+ "extent" : "full-screen",
+ "minimum-system-version" : "7.0",
+ "scale" : "2x"
+ },
+ {
+ "extent" : "full-screen",
+ "idiom" : "iphone",
+ "subtype" : "retina4",
+ "filename" : "Default-568h@2x.png",
+ "minimum-system-version" : "7.0",
+ "orientation" : "portrait",
+ "scale" : "2x"
+ },
+ {
+ "orientation" : "portrait",
+ "idiom" : "ipad",
+ "filename" : "Default-Portrait.png",
+ "extent" : "full-screen",
+ "minimum-system-version" : "7.0",
+ "scale" : "1x"
+ },
+ {
+ "orientation" : "landscape",
+ "idiom" : "ipad",
+ "filename" : "Default-Landscape.png",
+ "extent" : "full-screen",
+ "minimum-system-version" : "7.0",
+ "scale" : "1x"
+ },
+ {
+ "orientation" : "portrait",
+ "idiom" : "ipad",
+ "filename" : "Default-Portrait@2x.png",
+ "extent" : "full-screen",
+ "minimum-system-version" : "7.0",
+ "scale" : "2x"
+ },
+ {
+ "orientation" : "landscape",
+ "idiom" : "ipad",
+ "filename" : "Default-Landscape@2x.png",
+ "extent" : "full-screen",
+ "minimum-system-version" : "7.0",
+ "scale" : "2x"
+ },
+ {
+ "orientation" : "portrait",
+ "idiom" : "iphone",
+ "filename" : "Default.png",
+ "extent" : "full-screen",
+ "scale" : "1x"
+ },
+ {
+ "orientation" : "portrait",
+ "idiom" : "iphone",
+ "filename" : "Default@2x.png",
+ "extent" : "full-screen",
+ "scale" : "2x"
+ },
+ {
+ "orientation" : "portrait",
+ "idiom" : "iphone",
+ "filename" : "Default-568h@2x.png",
+ "extent" : "full-screen",
+ "subtype" : "retina4",
+ "scale" : "2x"
+ },
+ {
+ "orientation" : "portrait",
+ "idiom" : "ipad",
+ "extent" : "to-status-bar",
+ "scale" : "1x"
+ },
+ {
+ "orientation" : "portrait",
+ "idiom" : "ipad",
+ "filename" : "Default-Portrait.png",
+ "extent" : "full-screen",
+ "scale" : "1x"
+ },
+ {
+ "orientation" : "landscape",
+ "idiom" : "ipad",
+ "extent" : "to-status-bar",
+ "scale" : "1x"
+ },
+ {
+ "orientation" : "landscape",
+ "idiom" : "ipad",
+ "filename" : "Default-Landscape.png",
+ "extent" : "full-screen",
+ "scale" : "1x"
+ },
+ {
+ "orientation" : "portrait",
+ "idiom" : "ipad",
+ "extent" : "to-status-bar",
+ "scale" : "2x"
+ },
+ {
+ "orientation" : "portrait",
+ "idiom" : "ipad",
+ "filename" : "Default-Portrait@2x.png",
+ "extent" : "full-screen",
+ "scale" : "2x"
+ },
+ {
+ "orientation" : "landscape",
+ "idiom" : "ipad",
+ "extent" : "to-status-bar",
+ "scale" : "2x"
+ },
+ {
+ "orientation" : "landscape",
+ "idiom" : "ipad",
+ "filename" : "Default-Landscape@2x.png",
+ "extent" : "full-screen",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-568h@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-568h@2x.png
new file mode 100644
index 000000000..d7f17fcd2
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-568h@2x.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-667h@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-667h@2x.png
new file mode 100644
index 000000000..b88415405
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-667h@2x.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-736h@3x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-736h@3x.png
new file mode 100644
index 000000000..faab4b631
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-736h@3x.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape.png
new file mode 100644
index 000000000..3365ba3cd
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png
new file mode 100644
index 000000000..a44945c1a
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@3x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@3x.png
new file mode 100644
index 000000000..e6dca6269
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@3x.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait.png
new file mode 100644
index 000000000..1a5007962
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png
new file mode 100644
index 000000000..73d8b920f
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default.png
new file mode 100644
index 000000000..9f1f6ce3e
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default@2x.png
new file mode 100644
index 000000000..514fc5cde
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default@2x.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/Contents.json b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/Contents.json
new file mode 100644
index 000000000..4f4e9c506
--- /dev/null
+++ b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/Contents.json
@@ -0,0 +1,22 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchScreen-AspectFill.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchScreen-AspectFill@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill.png
new file mode 100644
index 000000000..c293f9c7a
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@2x.png
new file mode 100644
index 000000000..233693a6e
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@2x.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/Contents.json b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/Contents.json
new file mode 100644
index 000000000..23c0ffd7a
--- /dev/null
+++ b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/Contents.json
@@ -0,0 +1,22 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchScreen-Center.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchScreen-Center@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center.png
new file mode 100644
index 000000000..a5a775a2b
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@2x.png
new file mode 100644
index 000000000..154c19343
Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@2x.png differ
diff --git a/e2e/router/app/App_Resources/iOS/Info.plist b/e2e/router/app/App_Resources/iOS/Info.plist
new file mode 100644
index 000000000..ea3e3ea23
--- /dev/null
+++ b/e2e/router/app/App_Resources/iOS/Info.plist
@@ -0,0 +1,47 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleDisplayName
+ ${PRODUCT_NAME}
+ CFBundleExecutable
+ ${EXECUTABLE_NAME}
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ ${PRODUCT_NAME}
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIRequiresFullScreen
+
+ UIRequiredDeviceCapabilities
+
+ armv7
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
diff --git a/e2e/router/app/App_Resources/iOS/LaunchScreen.storyboard b/e2e/router/app/App_Resources/iOS/LaunchScreen.storyboard
new file mode 100644
index 000000000..2ad9471e1
--- /dev/null
+++ b/e2e/router/app/App_Resources/iOS/LaunchScreen.storyboard
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/e2e/router/app/App_Resources/iOS/build.xcconfig b/e2e/router/app/App_Resources/iOS/build.xcconfig
new file mode 100644
index 000000000..4b0118490
--- /dev/null
+++ b/e2e/router/app/App_Resources/iOS/build.xcconfig
@@ -0,0 +1,7 @@
+// You can add custom settings here
+// for example you can uncomment the following line to force distribution code signing
+// CODE_SIGN_IDENTITY = iPhone Distribution
+// To build for device with Xcode 8 you need to specify your development team. More info: https://developer.apple.com/library/prerelease/content/releasenotes/DeveloperTools/RN-Xcode/Introduction.html
+// DEVELOPMENT_TEAM = YOUR_TEAM_ID;
+ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
diff --git a/e2e/router/app/README.md b/e2e/router/app/README.md
new file mode 100644
index 000000000..ebe60c416
--- /dev/null
+++ b/e2e/router/app/README.md
@@ -0,0 +1,5 @@
+# NativeScript Tutorial Angular Template
+
+This repo serves as the starting point for NativeScript’s [Angular Getting Started Guide](https://docs.nativescript.org/angular/tutorial/ng-chapter-0).
+
+Please file any issues with this template on the [NativeScript/docs repository](https://github.com/nativescript/docs), which is where the tutorial content lives.
\ No newline at end of file
diff --git a/e2e/router/app/app-routing.module.ts b/e2e/router/app/app-routing.module.ts
new file mode 100644
index 000000000..dc4cf285c
--- /dev/null
+++ b/e2e/router/app/app-routing.module.ts
@@ -0,0 +1,40 @@
+import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core";
+import { NativeScriptRouterModule } from "nativescript-angular/router";
+
+import { FirstComponent } from "./first/first.component"
+import { SecondComponent } from "./second/second.component"
+import { MasterComponent } from "./second/master.component"
+import { DetailComponent } from "./second/detail.component"
+
+export const routes = [
+ {
+ path: "",
+ redirectTo: "/first",
+ pathMatch: "full"
+ },
+ {
+ path: "first",
+ component: FirstComponent,
+ },
+ {
+ path: "second/:depth", component: SecondComponent,
+ children: [
+ { path: "", component: MasterComponent },
+ { path: "detail/:id", component: DetailComponent }
+ ]
+ },
+];
+
+export const navigatableComponents = [
+ FirstComponent,
+ SecondComponent,
+ MasterComponent,
+ DetailComponent
+];
+
+@NgModule({
+ imports: [NativeScriptRouterModule.forRoot(routes)],
+ exports: [NativeScriptRouterModule],
+})
+export class AppRoutingModule { }
+
diff --git a/e2e/router/app/app.component.ts b/e2e/router/app/app.component.ts
new file mode 100644
index 000000000..2311d63e0
--- /dev/null
+++ b/e2e/router/app/app.component.ts
@@ -0,0 +1,7 @@
+import { Component } from "@angular/core";
+
+@Component({
+ template: ``
+})
+export class AppComponent { }
+
diff --git a/e2e/router/app/app.css b/e2e/router/app/app.css
new file mode 100644
index 000000000..54e8ca56c
--- /dev/null
+++ b/e2e/router/app/app.css
@@ -0,0 +1,16 @@
+.header {
+ font-size: 32;
+}
+
+.nested-header {
+ font-size: 26;
+}
+
+.nested-outlet {
+ margin: 20;
+ background-color: lightgreen;
+}
+
+Label {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/e2e/router/app/app.module.ts b/e2e/router/app/app.module.ts
new file mode 100644
index 000000000..e6b2aeb6e
--- /dev/null
+++ b/e2e/router/app/app.module.ts
@@ -0,0 +1,32 @@
+import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core";
+import { NativeScriptModule } from "nativescript-angular/nativescript.module";
+
+import "./rxjs-operators";
+
+import {
+ AppRoutingModule,
+ navigatableComponents,
+} from "./app-routing.module";
+
+import { AppComponent } from "./app.component";
+
+import { rendererTraceCategory, viewUtilCategory } from "nativescript-angular/trace";
+import { setCategories, enable } from "trace";
+setCategories(rendererTraceCategory + "," + viewUtilCategory);
+enable();
+
+@NgModule({
+ declarations: [
+ AppComponent,
+ ...navigatableComponents,
+ ],
+ bootstrap: [AppComponent],
+ providers: [],
+ imports: [
+ NativeScriptModule,
+ AppRoutingModule,
+ ],
+ schemas: [NO_ERRORS_SCHEMA],
+})
+export class AppModule { }
+
diff --git a/e2e/router/app/first/first.component.ts b/e2e/router/app/first/first.component.ts
new file mode 100644
index 000000000..e1509ae76
--- /dev/null
+++ b/e2e/router/app/first/first.component.ts
@@ -0,0 +1,42 @@
+import { Component, OnInit, OnDestroy, OnChanges } from "@angular/core";
+import { ActivatedRoute, Router, Route } from "@angular/router";
+import { Location } from "@angular/common";
+import { RouterExtensions } from "nativescript-angular/router";
+
+import { Page } from "ui/page";
+import { Observable } from "rxjs/Observable";
+
+@Component({
+ selector: "first",
+ template: `
+
+
+
+
+
+
+ `
+})
+export class FirstComponent implements OnInit, OnDestroy {
+ public message: string = "";
+ constructor(private routerExt: RouterExtensions, page: Page) {
+ console.log("FirstComponent - constructor() page: " + page);
+ }
+
+ ngOnInit() {
+ console.log("FirstComponent - ngOnInit()");
+ }
+
+ ngOnDestroy() {
+ console.log("FirstComponent - ngOnDestroy()");
+ }
+
+ goBack() {
+ this.message = "";
+ if (this.routerExt.canGoBack()) {
+ this.routerExt.back();
+ } else {
+ this.message = "canGoBack() - false"
+ }
+ }
+}
diff --git a/e2e/router/app/main.aot.ts b/e2e/router/app/main.aot.ts
new file mode 100644
index 000000000..98bf134fc
--- /dev/null
+++ b/e2e/router/app/main.aot.ts
@@ -0,0 +1,4 @@
+import { platformNativeScript } from "nativescript-angular/platform-static";
+import { AppModuleNgFactory } from "./app.module.ngfactory";
+
+platformNativeScript().bootstrapModuleFactory(AppModuleNgFactory);
diff --git a/e2e/router/app/main.ts b/e2e/router/app/main.ts
new file mode 100644
index 000000000..639bfd513
--- /dev/null
+++ b/e2e/router/app/main.ts
@@ -0,0 +1,4 @@
+import { platformNativeScriptDynamic } from "nativescript-angular/platform";
+import { AppModule } from "./app.module";
+
+platformNativeScriptDynamic().bootstrapModule(AppModule);
diff --git a/e2e/router/app/package.json b/e2e/router/app/package.json
new file mode 100644
index 000000000..d5356c0cc
--- /dev/null
+++ b/e2e/router/app/package.json
@@ -0,0 +1,5 @@
+{
+ "main": "main.js",
+ "name": "nativescript-template-ng-tutorial",
+ "version": "3.1.0"
+}
\ No newline at end of file
diff --git a/e2e/router/app/rxjs-operators.ts b/e2e/router/app/rxjs-operators.ts
new file mode 100644
index 000000000..480bbe40f
--- /dev/null
+++ b/e2e/router/app/rxjs-operators.ts
@@ -0,0 +1 @@
+import "rxjs/add/operator/map";
\ No newline at end of file
diff --git a/e2e/router/app/second/detail.component.ts b/e2e/router/app/second/detail.component.ts
new file mode 100644
index 000000000..e005f6dee
--- /dev/null
+++ b/e2e/router/app/second/detail.component.ts
@@ -0,0 +1,32 @@
+import { Component, OnInit, OnDestroy } from "@angular/core";
+import { ActivatedRoute, Router, Route } from "@angular/router";
+import { Location } from "@angular/common";
+import { Page } from "ui/page";
+import { Observable } from "rxjs/Observable";
+
+@Component({
+ selector: "detail",
+ template: `
+
+
+
+
+
+ `
+})
+export class DetailComponent {
+ public id$: Observable;
+
+ constructor(private router: Router, private route: ActivatedRoute) {
+ console.log("DetailComponent - constructor()");
+ this.id$ = route.params.map(r => r["id"]);
+ }
+
+ ngOnInit() {
+ console.log("DetailComponent - ngOnInit()");
+ }
+
+ ngOnDestroy() {
+ console.log("DetailComponent - ngOnDestroy()");
+ }
+}
\ No newline at end of file
diff --git a/e2e/router/app/second/master.component.ts b/e2e/router/app/second/master.component.ts
new file mode 100644
index 000000000..08605d5e5
--- /dev/null
+++ b/e2e/router/app/second/master.component.ts
@@ -0,0 +1,27 @@
+import { Component, OnInit, OnDestroy } from "@angular/core";
+
+@Component({
+ selector: "master",
+ template: `
+
+
+
+
+
+ `
+})
+export class MasterComponent implements OnInit, OnDestroy {
+ public details: Array = [1, 2, 3];
+
+ constructor() {
+ console.log("MasterComponent - constructor()");
+ }
+
+ ngOnInit() {
+ console.log("MasterComponent - ngOnInit()");
+ }
+
+ ngOnDestroy() {
+ console.log("MasterComponent - ngOnDestroy()");
+ }
+}
\ No newline at end of file
diff --git a/e2e/router/app/second/second.component.ts b/e2e/router/app/second/second.component.ts
new file mode 100644
index 000000000..75acb847b
--- /dev/null
+++ b/e2e/router/app/second/second.component.ts
@@ -0,0 +1,46 @@
+import { Component, OnInit, OnDestroy } from "@angular/core";
+import { ActivatedRoute, Router, Route } from "@angular/router";
+
+import { RouterExtensions } from "nativescript-angular/router";
+import { Page } from "ui/page";
+import { Observable } from "rxjs/Observable";
+
+@Component({
+ selector: "second",
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+})
+export class SecondComponent implements OnInit, OnDestroy {
+ public depth$: Observable;
+ public nextDepth$: Observable;
+
+ constructor(private routerExt: RouterExtensions, route: ActivatedRoute, page: Page) {
+ console.log("SecondComponent - constructor() page: " + page);
+ this.depth$ = route.params.map(r => r["depth"]);
+ this.nextDepth$ = route.params.map(r => +r["depth"] + 1);
+ }
+
+ ngOnInit() {
+ console.log("SecondComponent - ngOnInit()");
+ }
+
+ ngOnDestroy() {
+ console.log("SecondComponent - ngOnDestroy()");
+ }
+
+ goBack() {
+ this.routerExt.back();
+ }
+}
\ No newline at end of file
diff --git a/e2e/router/e2e/config/appium.capabilities.json b/e2e/router/e2e/config/appium.capabilities.json
new file mode 100644
index 000000000..676f5e070
--- /dev/null
+++ b/e2e/router/e2e/config/appium.capabilities.json
@@ -0,0 +1,84 @@
+{
+ "nexus5": {
+ "browserName": "",
+ "appium-version": "1.6.5",
+ "platformName": "Android",
+ "platformVersion": "6.0",
+ "deviceName": "device",
+ "udid": "077e4a47003b7698",
+ "-lt": 60000,
+ "automationName": "Appium",
+ "appActivity": "com.tns.NativeScriptActivity",
+ "app": ""
+ },
+ "android19": {
+ "browserName": "",
+ "appium-version": "1.6.5",
+ "platformName": "Android",
+ "platformVersion": "4.4",
+ "deviceName": "Emulator-Api19-Default",
+ "avd": "Emulator-Api19-Default",
+ "-lt": 60000,
+ "automationName": "Appium",
+ "appActivity": "com.tns.NativeScriptActivity",
+ "newCommandTimeout": 720,
+ "noReset": true,
+ "fullReset": false,
+ "app": ""
+ },
+ "android21": {
+ "browserName": "",
+ "appium-version": "1.6.5",
+ "platformName": "Android",
+ "platformVersion": "5.0",
+ "deviceName": "Emulator-Api21-Default",
+ "avd": "Emulator-Api21-Default",
+ "-lt": 60000,
+ "automationName": "Appium",
+ "appActivity": "com.tns.NativeScriptActivity",
+ "newCommandTimeout": 720,
+ "noReset": true,
+ "fullReset": false,
+ "app": ""
+ },
+ "android23": {
+ "browserName": "",
+ "appium-version": "1.6.5",
+ "platformName": "Android",
+ "platformVersion": "6.0",
+ "deviceName": "Emulator-Api23-Default",
+ "avd": "Emulator-Api23-Default",
+ "-lt": 60000,
+ "automationName": "Appium",
+ "appActivity": "com.tns.NativeScriptActivity",
+ "newCommandTimeout": 720,
+ "noReset": true,
+ "fullReset": false,
+ "app": ""
+ },
+ "android24": {
+ "browserName": "",
+ "appium-version": "1.6.5",
+ "platformName": "Android",
+ "platformVersion": "7.0",
+ "deviceName": "Emulator-Api24-Default",
+ "avd": "Emulator-Api24-Default",
+ "-lt": 60000,
+ "automationName": "UIAutomator2",
+ "appActivity": "com.tns.NativeScriptActivity",
+ "newCommandTimeout": 720,
+ "noReset": true,
+ "fullReset": false,
+ "app": ""
+ },
+ "sim.iPhone7.iOS100": {
+ "browserName": "",
+ "appium-version": "1.6.5",
+ "platformName": "iOS",
+ "platformVersion": "10.0",
+ "deviceName": "iPhone 7 100",
+ "noReset": true,
+ "fullReset": false,
+ "app": ""
+ }
+}
diff --git a/e2e/router/e2e/config/mocha.opts b/e2e/router/e2e/config/mocha.opts
new file mode 100644
index 000000000..796ec4724
--- /dev/null
+++ b/e2e/router/e2e/config/mocha.opts
@@ -0,0 +1,4 @@
+--timeout 80000
+--recursive e2e
+--reporter mocha-multi
+--reporter-options spec=-,mocha-junit-reporter=test-results.xml
\ No newline at end of file
diff --git a/e2e/router/e2e/helpers/appium-elements.ts b/e2e/router/e2e/helpers/appium-elements.ts
new file mode 100644
index 000000000..26a020a4d
--- /dev/null
+++ b/e2e/router/e2e/helpers/appium-elements.ts
@@ -0,0 +1,41 @@
+import { AppiumDriver } from "nativescript-dev-appium";
+
+import { UIElement } from "nativescript-dev-appium/ui-element";
+
+export class ExtendedUIElement extends UIElement {
+ refetch(): Promise {
+ return Promise.resolve(this);
+ }
+}
+
+const refetchable = () =>
+ (target: any, propertyKey: string, descriptor: PropertyDescriptor): any => {
+ const originalMethod = descriptor.value;
+ const patchRefetch = async (args, fetchMethod) => {
+ const result = await fetchMethod() as ExtendedUIElement;
+ result.refetch = () => patchRefetch(args, fetchMethod);
+
+ return result;
+ }
+
+ descriptor.value = async function (...args: any[]): Promise {
+ const fetchMethod = () => originalMethod.apply(this, args);
+ const result = await patchRefetch(args, fetchMethod);
+
+ return result;
+ }
+
+ return descriptor;
+ };
+
+export class DriverWrapper {
+ constructor(private driver: AppiumDriver) {
+ }
+
+ @refetchable()
+ async findElementByText(...args: any[]): Promise {
+ const result = await (this.driver).findElementByText(...args);
+
+ return result;
+ }
+}
diff --git a/e2e/router/e2e/router.e2e-spec.ts b/e2e/router/e2e/router.e2e-spec.ts
new file mode 100644
index 000000000..f5b983a8d
--- /dev/null
+++ b/e2e/router/e2e/router.e2e-spec.ts
@@ -0,0 +1,309 @@
+import {
+ AppiumDriver,
+ createDriver,
+ SearchOptions,
+} from "nativescript-dev-appium";
+
+import { DriverWrapper, ExtendedUIElement } from "./helpers/appium-elements";
+
+describe("Simple navigate and back", () => {
+ let driver: AppiumDriver;
+ let driverWrapper: DriverWrapper;
+
+ before(async () => {
+ driver = await createDriver();
+ driverWrapper = new DriverWrapper(driver);
+ });
+
+ after(async () => {
+ await driver.quit();
+ console.log("Driver quits!");
+ });
+
+ it("should find First", async () => {
+ await assureFirstComponent(driverWrapper);
+ });
+
+ it("should navigate to Second(1)/master", async () => {
+ await goFromFirstToSecond(driverWrapper);
+
+ await assureSecondComponent(driverWrapper, 1);
+ await assureNestedMasterComponent(driverWrapper);
+ });
+
+ it("should navigate back to First", async () => {
+ await goBack(driverWrapper);
+ await assureFirstComponent(driverWrapper);
+ });
+});
+
+describe("Navigate inside nested outlet", () => {
+ let driver: AppiumDriver;
+ let driverWrapper: DriverWrapper;
+
+ before(async () => {
+ driver = await createDriver();
+ driverWrapper = new DriverWrapper(driver);
+ });
+
+ after(async () => {
+ await driver.quit();
+ console.log("Driver quits!");
+ });
+
+ it("should find First", async () => {
+ await assureFirstComponent(driverWrapper);
+ });
+
+ it("should navigate to Second(1)/master", async () => {
+ await goFromFirstToSecond(driverWrapper);
+
+ await assureSecondComponent(driverWrapper, 1)
+ await assureNestedMasterComponent(driverWrapper);
+ });
+
+ it("should navigate to Second(1)/detail(1) and back", async () => {
+ const detailBtn = await driverWrapper.findElementByText("DETAIL 1", SearchOptions.exact);
+ detailBtn.click();
+ await assureSecondComponent(driverWrapper, 1)
+ await assureNestedDetailComponent(driverWrapper, 1);
+
+ await goBack(driverWrapper);
+ await assureSecondComponent(driverWrapper, 1)
+ await assureNestedMasterComponent(driverWrapper);
+ });
+
+ it("should navigate to Second(1)/detail(2) and back", async () => {
+ const detailBtn = await driverWrapper.findElementByText("DETAIL 2", SearchOptions.exact);
+ detailBtn.click();
+ await assureSecondComponent(driverWrapper, 1)
+ await assureNestedDetailComponent(driverWrapper, 2);
+
+ await goBack(driverWrapper);
+ await assureSecondComponent(driverWrapper, 1)
+ await assureNestedMasterComponent(driverWrapper);
+ });
+
+ it("should navigate back to First", async () => {
+ await goBack(driverWrapper);
+ await assureFirstComponent(driverWrapper);
+ });
+});
+
+describe("Navigate to same component with different param", () => {
+ let driver: AppiumDriver;
+ let driverWrapper: DriverWrapper;
+
+ before(async () => {
+ driver = await createDriver();
+ driverWrapper = new DriverWrapper(driver);
+ });
+
+ after(async () => {
+ await driver.quit();
+ console.log("Driver quits!");
+ });
+
+ it("should find First", async () => {
+ await assureFirstComponent(driverWrapper);
+ });
+
+ it("should navigate to Second(1)/master", async () => {
+ await goFromFirstToSecond(driverWrapper);
+
+ await assureSecondComponent(driverWrapper, 1)
+ await assureNestedMasterComponent(driverWrapper);
+ });
+
+ it("should navigate to Second(2)/master", async () => {
+ const navigationButton =
+ await driverWrapper.findElementByText("GO TO NEXT SECOND", SearchOptions.exact);
+ navigationButton.click();
+
+ await assureSecondComponent(driverWrapper, 2)
+ await assureNestedMasterComponent(driverWrapper);
+ });
+
+ it("should navigate back to Second(1)/master", async () => {
+ await goBack(driverWrapper);
+
+ await assureSecondComponent(driverWrapper, 1)
+ await assureNestedMasterComponent(driverWrapper);
+ });
+
+ it("should navigate back to First", async () => {
+ await goBack(driverWrapper);
+ await assureFirstComponent(driverWrapper);
+ });
+});
+
+describe("Nested navigation + page navigation", () => {
+ let driver: AppiumDriver;
+ let driverWrapper: DriverWrapper;
+
+ before(async () => {
+ driver = await createDriver();
+ driverWrapper = new DriverWrapper(driver);
+ });
+
+ after(async () => {
+ await driver.quit();
+ console.log("Driver quits!");
+ });
+
+ it("should find First", async () => {
+ await assureFirstComponent(driverWrapper);
+ });
+
+ it("should navigate to Second(1)/master", async () => {
+ await goFromFirstToSecond(driverWrapper);
+
+ await assureSecondComponent(driverWrapper, 1)
+ await assureNestedMasterComponent(driverWrapper);
+ });
+
+ it("should navigate to Second(1)/detail(1)", async () => {
+ const detailBtn = await driverWrapper.findElementByText("DETAIL 1", SearchOptions.exact);
+ detailBtn.click();
+
+ await assureSecondComponent(driverWrapper, 1)
+ await assureNestedDetailComponent(driverWrapper, 1);
+ });
+
+ it("should navigate to Second(2)/master", async () => {
+ const navigationButton =
+ await driverWrapper.findElementByText("GO TO NEXT SECOND", SearchOptions.exact);
+ navigationButton.click();
+
+ await assureSecondComponent(driverWrapper, 2)
+ await assureNestedMasterComponent(driverWrapper);
+ });
+
+ it("should navigate to Second(2)/detail(2)", async () => {
+ const detailBtn = await driverWrapper.findElementByText("DETAIL 2", SearchOptions.exact);
+ detailBtn.click();
+
+ await assureSecondComponent(driverWrapper, 2)
+ await assureNestedDetailComponent(driverWrapper, 2);
+ });
+
+ it("should navigate to First", async () => {
+ const navigationButton =
+ await driverWrapper.findElementByText("GO TO FIRST", SearchOptions.exact);
+ navigationButton.click();
+
+ await assureFirstComponent(driverWrapper);
+ });
+
+ it("should navigate the whole stack", async () => {
+ await goBack(driverWrapper);
+ await assureSecondComponent(driverWrapper, 2)
+ await assureNestedDetailComponent(driverWrapper, 2);
+
+ await goBack(driverWrapper);
+ await assureSecondComponent(driverWrapper, 2)
+ await assureNestedMasterComponent(driverWrapper);
+
+ await goBack(driverWrapper);
+ await assureSecondComponent(driverWrapper, 1)
+ await assureNestedDetailComponent(driverWrapper, 1);
+
+ await goBack(driverWrapper);
+ await assureSecondComponent(driverWrapper, 1)
+ await assureNestedMasterComponent(driverWrapper);
+
+ await goBack(driverWrapper);
+ await assureFirstComponent(driverWrapper);
+ });
+});
+
+describe("Shouldn't be able to navigate back on startup", () => {
+ let driver: AppiumDriver;
+ let driverWrapper: DriverWrapper;
+
+ before(async () => {
+ driver = await createDriver();
+ driverWrapper = new DriverWrapper(driver);
+ });
+
+ after(async () => {
+ await driver.quit();
+ console.log("Driver quits!");
+ });
+
+ it("should find First", async () => {
+ await assureFirstComponent(driverWrapper);
+ });
+
+ it("shouldn't be able to go back", async () => {
+ await goBack(driverWrapper);
+ await driverWrapper.findElementByText("canGoBack() - false", SearchOptions.exact);
+ });
+});
+
+describe("Shouldn't be able to navigate back after cleared history", () => {
+ let driver: AppiumDriver;
+ let driverWrapper: DriverWrapper;
+
+ before(async () => {
+ driver = await createDriver();
+ driverWrapper = new DriverWrapper(driver);
+ });
+
+ after(async () => {
+ await driver.quit();
+ console.log("Driver quits!");
+ });
+
+ it("should find First", async () => {
+ await assureFirstComponent(driverWrapper);
+ });
+
+ it("should navigate to Second(1)/master", async () => {
+ await goFromFirstToSecond(driverWrapper);
+
+ await assureSecondComponent(driverWrapper, 1)
+ await assureNestedMasterComponent(driverWrapper);
+ });
+
+ it("should navigate to Second(1)/master", async () => {
+ const navigationButton =
+ await driverWrapper.findElementByText("GO TO FIRST(CLEAR)", SearchOptions.exact);
+ navigationButton.click();
+ await assureFirstComponent(driverWrapper);
+ });
+
+ it("shouldn't be able to go back", async () => {
+ await goBack(driverWrapper);
+ await driverWrapper.findElementByText("canGoBack() - false", SearchOptions.exact);
+ });
+});
+
+async function assureFirstComponent(driverWrapper: DriverWrapper) {
+ await driverWrapper.findElementByText("FirstComponent", SearchOptions.exact);
+}
+
+async function assureSecondComponent(driverWrapper: DriverWrapper, param: number) {
+ await driverWrapper.findElementByText("SecondComponent", SearchOptions.exact);
+ await driverWrapper.findElementByText(`param: ${param}`, SearchOptions.exact);
+}
+
+async function assureNestedMasterComponent(driverWrapper: DriverWrapper) {
+ await driverWrapper.findElementByText("NestedMaster", SearchOptions.exact);
+}
+
+async function assureNestedDetailComponent(driverWrapper: DriverWrapper, param: number) {
+ await driverWrapper.findElementByText("NestedDetail", SearchOptions.exact);
+ await driverWrapper.findElementByText(`nested-param: ${param}`, SearchOptions.exact);
+}
+
+async function goBack(driverWrapper: DriverWrapper) {
+ const backButton = await driverWrapper.findElementByText("BACK", SearchOptions.exact);
+ await backButton.click();
+}
+
+async function goFromFirstToSecond(driverWrapper: DriverWrapper) {
+ const navigationButton =
+ await driverWrapper.findElementByText("GO TO SECOND", SearchOptions.exact);
+ navigationButton.click();
+}
\ No newline at end of file
diff --git a/e2e/router/e2e/setup.ts b/e2e/router/e2e/setup.ts
new file mode 100644
index 000000000..8b26e66e9
--- /dev/null
+++ b/e2e/router/e2e/setup.ts
@@ -0,0 +1,9 @@
+import { startServer, stopServer } from "nativescript-dev-appium";
+
+before("start server", async () => {
+ await startServer();
+});
+
+after("stop server", async () => {
+ await stopServer();
+});
diff --git a/e2e/router/e2e/tsconfig.json b/e2e/router/e2e/tsconfig.json
new file mode 100644
index 000000000..040f56ed4
--- /dev/null
+++ b/e2e/router/e2e/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../tsconfig",
+ "compilerOptions": {
+ "importHelpers": true,
+ "types": [
+ "node",
+ "mocha",
+ "chai"
+ ]
+ }
+}
diff --git a/e2e/router/package.json b/e2e/router/package.json
new file mode 100644
index 000000000..52b596b89
--- /dev/null
+++ b/e2e/router/package.json
@@ -0,0 +1,50 @@
+{
+ "description": "NativeScript Application",
+ "license": "SEE LICENSE IN ",
+ "readme": "NativeScript Application",
+ "repository": "",
+ "nativescript": {
+ "id": "org.nativescript.router",
+ "tns-android": {
+ "version": "3.2.0-2017-9-4-1"
+ }
+ },
+ "dependencies": {
+ "@angular/animations": "~4.2.0",
+ "@angular/common": "~4.2.0",
+ "@angular/compiler": "~4.2.0",
+ "@angular/core": "~4.2.0",
+ "@angular/forms": "~4.2.0",
+ "@angular/http": "~4.2.0",
+ "@angular/platform-browser": "~4.2.0",
+ "@angular/router": "~4.2.0",
+ "nativescript-angular": "file:../../nativescript-angular",
+ "nativescript-intl": "^3.0.0",
+ "reflect-metadata": "~0.1.8",
+ "rxjs": "~5.3.0",
+ "tns-core-modules": "next",
+ "zone.js": "~0.8.2"
+ },
+ "devDependencies": {
+ "@types/chai": "^4.0.2",
+ "@types/mocha": "^2.2.41",
+ "@types/node": "^7.0.5",
+ "babel-traverse": "6.25.0",
+ "babel-types": "6.25.0",
+ "babylon": "6.17.4",
+ "chai": "~4.1.1",
+ "chai-as-promised": "~7.1.1",
+ "colors": "^1.1.2",
+ "lazy": "1.0.11",
+ "mocha": "~3.5.0",
+ "mocha-junit-reporter": "^1.13.0",
+ "mocha-multi": "^0.11.0",
+ "nativescript-dev-appium": "next",
+ "nativescript-dev-typescript": "~0.4.0",
+ "tslib": "^1.7.1",
+ "typescript": "~2.2.1"
+ },
+ "scripts": {
+ "e2e": "tsc -p e2e && mocha --opts ./e2e/config/mocha.opts"
+ }
+}
diff --git a/e2e/router/tsconfig.json b/e2e/router/tsconfig.json
new file mode 100644
index 000000000..17700d54e
--- /dev/null
+++ b/e2e/router/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "es5",
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
+ "noEmitHelpers": true,
+ "noEmitOnError": true,
+ "lib": [
+ "es6",
+ "dom",
+ "es2015.iterable"
+ ],
+ "baseUrl": ".",
+ "paths": {
+ "*": [
+ "./node_modules/tns-core-modules/*",
+ "./node_modules/*"
+ ]
+ }
+ },
+ "exclude": [
+ "node_modules",
+ "platforms",
+ "**/*.aot.ts"
+ ]
+}
\ No newline at end of file
diff --git a/nativescript-angular/router.ts b/nativescript-angular/router.ts
index d5494bcfb..6bfc6b359 100644
--- a/nativescript-angular/router.ts
+++ b/nativescript-angular/router.ts
@@ -5,7 +5,7 @@ import {
Optional,
SkipSelf,
} from "@angular/core";
-import { RouterModule, Routes, ExtraOptions } from "@angular/router";
+import { RouterModule, Routes, ExtraOptions, RouteReuseStrategy } from "@angular/router";
import { LocationStrategy, PlatformLocation } from "@angular/common";
import { Frame } from "tns-core-modules/ui/frame";
import { NSRouterLink } from "./router/ns-router-link";
@@ -13,6 +13,7 @@ import { NSRouterLinkActive } from "./router/ns-router-link-active";
import { PageRouterOutlet } from "./router/page-router-outlet";
import { NSLocationStrategy, LocationState } from "./router/ns-location-strategy";
import { NativescriptPlatformLocation } from "./router/ns-platform-location";
+import { NSRouteReuseStrategy } from "./router/ns-route-reuse-strategy";
import { RouterExtensions } from "./router/router-extensions";
import { NativeScriptCommonModule } from "./common";
@@ -37,6 +38,8 @@ export type LocationState = LocationState;
NativescriptPlatformLocation,
{ provide: PlatformLocation, useClass: NativescriptPlatformLocation },
RouterExtensions,
+ NSRouteReuseStrategy,
+ { provide: RouteReuseStrategy, useExisting: NSRouteReuseStrategy }
],
imports: [
RouterModule,
diff --git a/nativescript-angular/router/ns-location-strategy.ts b/nativescript-angular/router/ns-location-strategy.ts
index 3a0f2a0b1..c474f2a24 100644
--- a/nativescript-angular/router/ns-location-strategy.ts
+++ b/nativescript-angular/router/ns-location-strategy.ts
@@ -67,7 +67,7 @@ export class NSLocationStrategy extends LocationStrategy {
replaceState(state: any, title: string, url: string, queryParams: string): void {
if (this.states.length > 0) {
- routerLog("NSLocationStrategy.replaceState changing exisitng state: " +
+ routerLog("NSLocationStrategy.replaceState changing existing state: " +
`${state}, title: ${title}, url: ${url}, queryParams: ${queryParams}`);
const topState = this.peekState();
topState.state = state;
@@ -104,7 +104,7 @@ export class NSLocationStrategy extends LocationStrategy {
if (state.isPageNavigation) {
// This was a page navigation - so navigate through frame.
routerLog("NSLocationStrategy.back() while not navigating back but top" +
- " state is page - will call frame.goback()");
+ " state is page - will call frame.goBack()");
this.frame.goBack();
} else {
// Nested navigation - just pop the state
diff --git a/nativescript-angular/router/ns-route-reuse-strategy.ts b/nativescript-angular/router/ns-route-reuse-strategy.ts
new file mode 100644
index 000000000..d69b924dd
--- /dev/null
+++ b/nativescript-angular/router/ns-route-reuse-strategy.ts
@@ -0,0 +1,133 @@
+import { Injectable } from "@angular/core";
+import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from "@angular/router";
+
+import { routeReuseStrategyLog as log } from "../trace";
+import { NSLocationStrategy } from "./ns-location-strategy";
+import { pageRouterActivatedSymbol, destroyComponentRef } from "./page-router-outlet";
+
+interface CacheItem {
+ key: string;
+ state: DetachedRouteHandle;
+}
+
+/**
+ * Detached state cache
+ */
+class DetachedStateCache {
+ private cache = new Array();
+
+ public get length(): number {
+ return this.cache.length;
+ }
+
+ public push(cacheItem: CacheItem) {
+ this.cache.push(cacheItem);
+ }
+
+ public pop(): CacheItem {
+ return this.cache.pop();
+ }
+
+ public peek(): CacheItem {
+ return this.cache[this.cache.length - 1];
+ }
+
+ public clear() {
+ log(`DetachedStateCache.clear() ${this.cache.length} items will be destroyed`);
+
+ while (this.cache.length > 0) {
+ const state = this.cache.pop().state;
+ if (!state.componentRef) {
+ throw new Error("No componentRed found in DetachedRouteHandle");
+ }
+
+ destroyComponentRef(state.componentRef);
+ }
+ }
+}
+
+/**
+ * Does not detach any subtrees. Reuses routes as long as their route config is the same.
+ */
+@Injectable()
+export class NSRouteReuseStrategy implements RouteReuseStrategy {
+ private cache: DetachedStateCache = new DetachedStateCache();
+
+ constructor(private location: NSLocationStrategy) { }
+
+ shouldDetach(route: ActivatedRouteSnapshot): boolean {
+ const key = getSnapshotKey(route);
+ const isPageActivated = route[pageRouterActivatedSymbol];
+ const isBack = this.location._isPageNavigatingBack();
+ const shouldDetach = !isBack && isPageActivated;
+
+ log(`shouldDetach isBack: ${isBack} key: ${key} result: ${shouldDetach}`);
+
+ return shouldDetach;
+ }
+
+ shouldAttach(route: ActivatedRouteSnapshot): boolean {
+ const key = getSnapshotKey(route);
+ const isBack = this.location._isPageNavigatingBack();
+ const shouldAttach = isBack && this.cache.peek().key === key;
+
+ log(`shouldAttach isBack: ${isBack} key: ${key} result: ${shouldAttach}`);
+
+ return shouldAttach;
+ }
+
+
+ store(route: ActivatedRouteSnapshot, state: DetachedRouteHandle): void {
+ const key = getSnapshotKey(route);
+ log(`store key: ${key}, state: ${state}`);
+
+ if (state) {
+ this.cache.push({ key, state });
+ } else {
+ const topItem = this.cache.peek();
+ if (topItem.key === key) {
+ this.cache.pop();
+ } else {
+ throw new Error("Trying to pop from DetachedStateCache but keys don't match. " +
+ `expected: ${topItem.key} actual: ${key}`);
+ }
+ }
+ }
+
+ retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
+ const key = getSnapshotKey(route);
+ const isBack = this.location._isPageNavigatingBack();
+ const cachedItem = this.cache.peek();
+
+ let state = null;
+ if (isBack && cachedItem && cachedItem.key === key) {
+ state = cachedItem.state;
+ }
+
+ log(`retrieved isBack: ${isBack} key: ${key} state: ${state}`);
+
+ return state;
+ }
+
+ shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
+ const shouldReuse = future.routeConfig === curr.routeConfig;
+
+ if (shouldReuse && curr && curr[pageRouterActivatedSymbol]) {
+ // When reusing route - copy the pageRouterActivated to the new snapshot
+ // It's needed in shouldDetach to determine if the route should be detached.
+ future[pageRouterActivatedSymbol] = curr[pageRouterActivatedSymbol];
+ }
+
+ log(`shouldReuseRoute result: ${shouldReuse}`);
+
+ return shouldReuse;
+ }
+
+ clearCache() {
+ this.cache.clear();
+ }
+}
+
+function getSnapshotKey(snapshot: ActivatedRouteSnapshot): string {
+ return snapshot.pathFromRoot.join("->");
+}
diff --git a/nativescript-angular/router/page-router-outlet.ts b/nativescript-angular/router/page-router-outlet.ts
index 9f7541ec9..03469703e 100644
--- a/nativescript-angular/router/page-router-outlet.ts
+++ b/nativescript-angular/router/page-router-outlet.ts
@@ -2,7 +2,7 @@ import {
Attribute, ChangeDetectorRef,
ComponentFactory, ComponentFactoryResolver, ComponentRef,
Directive, Inject, InjectionToken, Injector,
- OnDestroy, OnInit,
+ OnDestroy, OnInit, EventEmitter, Output,
Type, ViewContainerRef,
} from "@angular/core";
import {
@@ -24,6 +24,7 @@ import { routerLog } from "../trace";
import { DetachedLoader } from "../common/detached-loader";
import { ViewUtil } from "../view-util";
import { NSLocationStrategy } from "./ns-location-strategy";
+import { NSRouteReuseStrategy } from "./ns-route-reuse-strategy";
export class PageRoute {
activatedRoute: BehaviorSubject;
@@ -33,79 +34,54 @@ export class PageRoute {
}
}
-class ChildInjector implements Injector {
- constructor(
- private providers: ProviderMap,
- private parent: Injector
- ) {}
+// Used to "mark" ActivatedRoute snapshots that are handled in PageRouterOutlet
+export const pageRouterActivatedSymbol = Symbol("page-router-activated");
+export const loaderRefSymbol = Symbol("loader-ref");
- get(token: Type|InjectionToken, notFoundValue?: T): T {
- return this.providers.get(token) || this.parent.get(token, notFoundValue);
+export function destroyComponentRef(componentRef: ComponentRef) {
+ if (isPresent(componentRef)) {
+ const loaderRef = componentRef[loaderRefSymbol];
+ if (isPresent(loaderRef)) {
+ loaderRef.destroy();
+ }
+ componentRef.destroy();
}
}
-/**
- * Reference Cache
- */
-class RefCache {
- private cache = new Array();
-
- public get length(): number {
- return this.cache.length;
- }
-
- public push(cacheItem: CacheItem) {
- this.cache.push(cacheItem);
- }
-
- public pop(): CacheItem {
- return this.cache.pop();
- }
-
- public peek(): CacheItem {
- return this.cache[this.cache.length - 1];
- }
-
- public clear(): void {
- while (this.length) {
- RefCache.destroyItem(this.pop());
- }
- }
+class ChildInjector implements Injector {
+ constructor(
+ private providers: ProviderMap,
+ private parent: Injector
+ ) { }
- public static destroyItem(item: CacheItem) {
- if (isPresent(item.componentRef)) {
- item.componentRef.destroy();
+ get(token: Type | InjectionToken, notFoundValue?: T): T {
+ let localValue = this.providers.get(token);
+ if (localValue) {
+ return localValue;
}
- if (isPresent(item.loaderRef)) {
- item.loaderRef.destroy();
- }
+ return this.parent.get(token, notFoundValue);
}
}
-interface CacheItem {
- componentRef: ComponentRef;
- reusedRoute: PageRoute;
- loaderRef?: ComponentRef;
-}
-
-
-type ProviderMap = Map|InjectionToken, any>;
+type ProviderMap = Map | InjectionToken, any>;
const log = (msg: string) => routerLog(msg);
@Directive({ selector: "page-router-outlet" }) // tslint:disable-line:directive-selector
export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-line:directive-class-suffix
- private activated: ComponentRef|null = null;
- private _activatedRoute: ActivatedRoute|null = null;
- private refCache: RefCache = new RefCache();
+ private activated: ComponentRef | null = null;
+ private _activatedRoute: ActivatedRoute | null = null;
+
private isInitialPage: boolean = true;
private detachedLoaderFactory: ComponentFactory;
- private itemsToDestroy: CacheItem[] = [];
private name: string;
private viewUtil: ViewUtil;
+ @Output("activate") activateEvents = new EventEmitter(); // tslint:disable-line:no-output-rename
+ @Output("deactivate") deactivateEvents = new EventEmitter(); // tslint:disable-line:no-output-rename
+
/** @deprecated from Angular since v4 */
get locationInjector(): Injector { return this.location.injector; }
/** @deprecated from Angular since v4 */
@@ -140,7 +116,8 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l
private frame: Frame,
private changeDetector: ChangeDetectorRef,
@Inject(DEVICE) device: Device,
- @Inject(PAGE_FACTORY) private pageFactory: PageFactory
+ @Inject(PAGE_FACTORY) private pageFactory: PageFactory,
+ private routeReuseStrategy: NSRouteReuseStrategy
) {
this.name = name || PRIMARY_OUTLET;
@@ -148,11 +125,6 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l
this.viewUtil = new ViewUtil(device);
this.detachedLoaderFactory = resolver.resolveComponentFactory(DetachedLoader);
- log("DetachedLoaderFactory loaded");
- }
-
- ngOnDestroy(): void {
- this.parentContexts.onChildOutletDestroyed(this.name);
}
ngOnInit(): void {
@@ -176,41 +148,29 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l
}
}
- deactivate(): void {
- if (this.locationStrategy._isPageNavigatingBack()) {
- log("PageRouterOutlet.deactivate() while going back - should destroy");
- if (!this.isActivated) {
- return;
- }
+ ngOnDestroy(): void {
+ this.parentContexts.onChildOutletDestroyed(this.name);
+ }
- const poppedItem = this.refCache.pop();
- const poppedRef = poppedItem.componentRef;
+ deactivate(): void {
+ if (!this.locationStrategy._isPageNavigatingBack()) {
+ throw new Error("Currently not in page back navigation" +
+ " - component should be detached instead of deactivated.");
+ }
- if (this.activated !== poppedRef) {
- throw new Error("Current componentRef is different for cached componentRef");
- }
+ log("PageRouterOutlet.deactivate() while going back - should destroy");
- RefCache.destroyItem(poppedItem);
- this.activated = null;
- } else {
- log("PageRouterOutlet.deactivate() while going forward - do nothing");
+ if (!this.isActivated) {
+ return;
}
- }
- private destroyQueuedCacheItems() {
- while (this.itemsToDestroy.length > 0) {
- this.destroyCacheItem(this.itemsToDestroy.pop());
- }
- }
+ const c = this.activated.instance;
+ destroyComponentRef(this.activated);
- private destroyCacheItem(poppedItem: CacheItem) {
- if (isPresent(poppedItem.componentRef)) {
- poppedItem.componentRef.destroy();
- }
+ this.activated = null;
+ this._activatedRoute = null;
- if (isPresent(poppedItem.loaderRef)) {
- poppedItem.loaderRef.destroy();
- }
+ this.deactivateEvents.emit(c);
}
/**
@@ -221,11 +181,10 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l
throw new Error("Outlet is not activated");
}
- this.location.detach();
- const cmp = this.activated;
+ const component = this.activated;
this.activated = null;
this._activatedRoute = null;
- return cmp;
+ return component;
}
/**
@@ -238,7 +197,9 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l
this.activated = ref;
this._activatedRoute = activatedRoute;
- this.location.insert(ref.hostView);
+ this._activatedRoute.snapshot[pageRouterActivatedSymbol] = true;
+
+ this.locationStrategy._finishBackPageNavigation();
}
/**
@@ -248,20 +209,21 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l
@profile
activateWith(
activatedRoute: ActivatedRoute,
- resolver: ComponentFactoryResolver|null
- ): void {
+ resolver: ComponentFactoryResolver | null): void {
- log("PageRouterOutlet.activateWith() - " +
- "instanciating new component during commit phase of a navigation");
+ if (this.locationStrategy._isPageNavigatingBack()) {
+ throw new Error("Currently in page back navigation - component should be reattached instead of activated.");
+ }
+
+ log("PageRouterOutlet.activateWith() - instantiating new component");
this._activatedRoute = activatedRoute;
+ this._activatedRoute.snapshot[pageRouterActivatedSymbol] = true;
+
resolver = resolver || this.resolver;
- if (this.locationStrategy._isPageNavigatingBack()) {
- this.activateOnGoBack(activatedRoute);
- } else {
- this.activateOnGoForward(activatedRoute, resolver);
- }
+ this.activateOnGoForward(activatedRoute, resolver);
+ this.activateEvents.emit(this.activated.instance);
}
private activateOnGoForward(
@@ -271,6 +233,7 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l
const pageRoute = new PageRoute(activatedRoute);
const providers = this.initProvidersMap(activatedRoute, pageRoute);
+
const childInjector = new ChildInjector(providers, this.location.injector);
const factory = this.getComponentFactory(activatedRoute, loadedResolver);
@@ -282,12 +245,6 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l
this.activated = this.location.createComponent(
factory, this.location.length, childInjector, []);
this.changeDetector.markForCheck();
-
- this.refCache.push({
- componentRef: this.activated,
- reusedRoute: pageRoute,
- loaderRef: null,
- });
} else {
log("PageRouterOutlet.activate() forward navigation - " +
"create detached loader in the loader container");
@@ -306,11 +263,7 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l
this.activated = loaderRef.instance.loadWithFactory(factory);
this.loadComponentInPage(page, this.activated);
- this.refCache.push({
- componentRef: this.activated,
- reusedRoute: pageRoute,
- loaderRef,
- });
+ this.activated[loaderRefSymbol] = loaderRef;
}
}
@@ -329,17 +282,6 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l
return providers;
}
- private activateOnGoBack(activatedRoute: ActivatedRoute): void {
- log("PageRouterOutlet.activate() - Back navigation, so load from cache");
-
- this.locationStrategy._finishBackPageNavigation();
-
- const cacheItem = this.refCache.peek();
- cacheItem.reusedRoute.activatedRoute.next(activatedRoute);
-
- this.activated = cacheItem.componentRef;
- }
-
@profile
private loadComponentInPage(page: Page, componentRef: ComponentRef): void {
// Component loaded. Find its root native view.
@@ -349,9 +291,6 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l
// Add it to the new page
page.content = componentView;
- page.on(Page.navigatedToEvent, () => setTimeout(() => {
- this.destroyQueuedCacheItems();
- }));
page.on(Page.navigatedFromEvent, (global).Zone.current.wrap((args: NavigatedData) => {
if (args.isBackNavigation) {
this.locationStrategy._beginBackPageNavigation();
@@ -360,17 +299,23 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l
}));
const navOptions = this.locationStrategy._beginPageNavigation();
+
+ // Clear refCache if navigation with clearHistory
+ if (navOptions.clearHistory) {
+ const clearCallback = () => setTimeout(() => {
+ this.routeReuseStrategy.clearCache();
+ page.off(Page.navigatedToEvent, clearCallback);
+ });
+
+ page.on(Page.navigatedToEvent, clearCallback);
+ }
+
this.frame.navigate({
create: () => { return page; },
clearHistory: navOptions.clearHistory,
animated: navOptions.animated,
transition: navOptions.transition
});
-
- // Clear refCache if navigation with clearHistory
- if (navOptions.clearHistory) {
- this.refCache.clear();
- }
}
// NOTE: Using private APIs - potential break point!
diff --git a/nativescript-angular/trace.ts b/nativescript-angular/trace.ts
index efe8ddab6..8f81c0005 100644
--- a/nativescript-angular/trace.ts
+++ b/nativescript-angular/trace.ts
@@ -4,6 +4,7 @@ export const animationsTraceCategory = "ns-animations";
export const rendererTraceCategory = "ns-renderer";
export const viewUtilCategory = "ns-view-util";
export const routerTraceCategory = "ns-router";
+export const routeReuseStrategyTraceCategory = "ns-route-reuse-strategy";
export const listViewTraceCategory = "ns-list-view";
export function animationsLog(message: string): void {
@@ -26,6 +27,10 @@ export function routerLog(message: string): void {
write(message, routerTraceCategory);
}
+export function routeReuseStrategyLog(message: string): void {
+ write(message, routeReuseStrategyTraceCategory);
+}
+
export function styleError(message: string): void {
write(message, categories.Style, messageType.error);
}
diff --git a/ng-sample/app/app.ts b/ng-sample/app/app.ts
index 9934ffea8..581f039fe 100644
--- a/ng-sample/app/app.ts
+++ b/ng-sample/app/app.ts
@@ -12,16 +12,18 @@ import {
routerTraceCategory,
listViewTraceCategory,
animationsTraceCategory,
+ routeReuseStrategyTraceCategory,
} from "nativescript-angular/trace";
import { PAGE_FACTORY, PageFactory, PageFactoryOptions } from "nativescript-angular/platform-providers";
import { Page } from "ui/page";
import { Color } from "color";
import { setCategories, enable } from "trace";
-setCategories(
- `${animationsTraceCategory},${rendererTraceCategory}`
-);
+// setCategories(
+// `${animationsTraceCategory},${rendererTraceCategory}`
+// );
// setCategories(routerTraceCategory);
// setCategories(listViewTraceCategory);
+setCategories(`${routeReuseStrategyTraceCategory}`);
enable();
import { RendererTest } from "./examples/renderer-test";
@@ -132,10 +134,10 @@ const customPageFactoryProvider = {
// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(ActionBarTest));
// router
-platformNativeScriptDynamic().bootstrapModule(makeExampleModule(RouterOutletAppComponent));
+// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(RouterOutletAppComponent));
// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(PageRouterOutletAppComponent));
// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(PageRouterOutletNestedAppComponent));
-// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(ClearHistoryAppComponent));
+platformNativeScriptDynamic().bootstrapModule(makeExampleModule(ClearHistoryAppComponent));
// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(LoginAppComponent));
// animations
diff --git a/ng-sample/app/examples/router/page-router-outlet-nested-test.ts b/ng-sample/app/examples/router/page-router-outlet-nested-test.ts
index 104278c94..5f5e07da5 100644
--- a/ng-sample/app/examples/router/page-router-outlet-nested-test.ts
+++ b/ng-sample/app/examples/router/page-router-outlet-nested-test.ts
@@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy } from "@angular/core";
-import { ActivatedRoute, Router } from "@angular/router";
+import { ActivatedRoute, Router, Route } from "@angular/router";
import { Location } from "@angular/common";
import { Page } from "ui/page";
import { Observable } from "rxjs/Observable";
@@ -12,8 +12,8 @@ import "rxjs/add/operator/map";
-
-
+
+
`
})
@@ -70,6 +70,14 @@ class DetailComponent {
console.log("DetailComponent.constructor()");
this.id$ = route.params.map(r => r["id"]);
}
+
+ ngOnInit() {
+ console.log("DetailComponent - ngOnInit()");
+ }
+
+ ngOnDestroy() {
+ console.log("DetailComponent - ngOnDestroy()");
+ }
}
@Component({
@@ -79,7 +87,7 @@ class DetailComponent {
-
+
@@ -121,8 +129,11 @@ class SecondComponent implements OnInit, OnDestroy {
template: ``
})
export class PageRouterOutletNestedAppComponent {
- static routes = [
- { path: "", component: FirstComponent },
+ static routes: Route[] = [
+ { path: "", redirectTo: "/second/1/detail/3", pathMatch: "full" },
+ {
+ path: "first", component: FirstComponent,
+ },
{
path: "second/:depth", component: SecondComponent,
children: [