diff --git a/aio/content/guide/fundamentals.md b/aio/content/guide/fundamentals.md
new file mode 100644
index 0000000000..aac1ca458b
--- /dev/null
+++ b/aio/content/guide/fundamentals.md
@@ -0,0 +1,3 @@
+# Fundamentals of Angular
+
+Learn the fundamental features of Angular in this section of the guide.
\ No newline at end of file
diff --git a/aio/content/guide/techniques.md b/aio/content/guide/techniques.md
new file mode 100644
index 0000000000..7460f96e98
--- /dev/null
+++ b/aio/content/guide/techniques.md
@@ -0,0 +1,3 @@
+# Techniques
+
+Learn important Angular application techniques such as how to setup, secure, and deploy your application.
\ No newline at end of file
diff --git a/aio/content/marketing/docs.md b/aio/content/marketing/docs.md
index c6a8566db3..e46e7e7763 100644
--- a/aio/content/marketing/docs.md
+++ b/aio/content/marketing/docs.md
@@ -1,79 +1,55 @@
-# Angular Docs
+# What is Angular?
-Welcome to the Angular documentation where you'll find guidance for beginners and experts alike.
+Angular is a platform that makes it easy to build applications with the web. Angular combines declarative templates, dependency injection, end to end tooling, and integrated best practices to solve development challenges. Angular empowers developers to build applications that live on the web, mobile, or the desktop
-## What is Angular?
+
+
+
+ A quick look at an Angular application.
+
+
-Angular is a platform for building and running web applications on desktop and mobile devices.
-It's an architecture, a modular library, and a set of tools to help teams
-build amazing web apps that run anywhere at scales large and small.
-
-## Organization
-
-Navigate the docs with the tree-view in the left side panel. Each top level category unfolds into topics that cover Angular from a distinct perspective:
-
-- **Getting Started** is a taste of Angular in under five minutes.
-
-- **Tutorial** is a step-by-step introduction to the essentials of Angular as you build a data-driven application with multi-page navigation.
-
-
-- **Fundamentals** explains each Angular concept and feature in practical terms with loads of examples.
-
-- **Techniques** covers tools and techniques for setting up, testing, securing, and deploying your application.
-
-- **API** is the comprehensive, searchable documentation for every Angular class, interface, and programmable feature.
-
-- **References** include answers to common questions about usage and style.
-
-## Sample code
-
-
-
-Guide pages are full of code snippets that you can copy and use in your own projects. The snippets are typically drawn from an example app.
-Look for the
**link** that launches a browser-based editor where you can see it run, inspect the code, modify it, and save changes.
-
-In most cases you can also
download the example,
-unzip it, and run locally with these terminal commands:
-
-
-npm install
-npm start
-
-
## Assumptions
-
-While we strive to keep these pages beginner-friendly, we have to make a few assumptions about your skills and experience in order to stay focused on Angular.
-
-We assume that you are a seasoned, front-end web developer with a working knowledge of
-**HTML, CSS, JavaScript**. The [Mozilla Developer Network](https://developer.mozilla.org/en-US/ "MDN - Mozilla Developer Network") is an excellent resource for reference and general learning.
-
-Effective Angular developers become familiar with two other technologies:
-
-**npm** - Modern web development depends on the [npm package management](https://www.npmjs.com/ "npm") system for distribution and installation of third party libraries. Angular is one such library.
-
-**TypeScript** - [TypeScript](http://www.typescriptlang.org/ "TypeScript") is a _typed_ superset of JavaScript. For the most part it is ES2015 JavaScript with type annotations to improve your design time experience and make it easier for teams to develop sophisticated applications.
-
-You _can_ write Angular applications in [JavaScript without TypeScript](guide/ts-to-js "Writing Angular in JavaScript"). But you should be able to _read_ TypeScript to understand this documentation and participate in conversations within the Angular community. The Angular CLI productivity tool and AOT high performance compiler only apply to TypeScript applications.
-
-You don't have to be an expert in npm or TypeScript to get started with Angular. A little knowledge will get you going and you can pick up what you need along the way.
-
-## Versions
-
-This is the Angular **version 4** documentation. See what's new in the [documentation changelog](guide/change-log). View the [Angular change log](https://github.com/angular/angular/blob/master/CHANGELOG.md) for enhancement and fixes to Angular itself.
-
-The Angular **version 2** documentation has been archived at [v2.angular.io](https://v2.angular.io "Angular v2 Docs").
-
+This documentation assumes that you are already familiar with
+[JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/A_re-introduction_to_JavaScript "Learn JavaScript"),
+and some of the tools from the
+[latest standards](https://babeljs.io/learn-es2015/ "Latest JavaScript standards") such as
+[classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes "ES2015 Classes")
+and [modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import "ES2015 Modules").
+The code samples are written using [TypeScript](https://www.typescriptlang.org/ "TypeScript").
+Most Angular code can be written with just the latest JavaScript,
+using [types](https://www.typescriptlang.org/docs/handbook/classes.html "TypeScript Types") for dependency injection,
+and using [decorators](https://www.typescriptlang.org/docs/handbook/decorators.html "Decorators") for metadata.
## Feedback
-We welcome feedback!
+You can sit with us!
-You can file documentation [issues](https://github.com/angular/angular/issues "Angular Github Issues") and create [pull requests](https://github.com/angular/angular/pulls "Angular Github PRs") on the Angular Github repository.
-Please prefix your issue or pull request title with "**docs:**" so that we know it concerns _documentation_ and draws the prompt attention of the appropriate people.
-
-Remember that a respectful, supportive approach produce the best results. Please consult and adhere to our [code of conduct](https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md "contributor code of conduct") when engaging with the Angular community.
\ No newline at end of file
+You can file documentation
+[issues](https://github.com/angular/angular/issues "Angular Github issues") and create
+[pull requests](https://github.com/angular/angular/pulls "Angular Github pull requests")
+on the Angular Github repository.
+The [contributing guide](https://github.com/angular/angular/blob/master/CONTRIBUTING.md "Contributing guide")
+will help you contribute to the community.
+Our community values respectful, supportive communication.
+Please consult and adhere to the
+[code of conduct](https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md "contributor code of conduct").
diff --git a/aio/content/navigation.json b/aio/content/navigation.json
index cc945d0870..5c926ec954 100644
--- a/aio/content/navigation.json
+++ b/aio/content/navigation.json
@@ -88,6 +88,7 @@
{
"title": "Fundamentals",
+ "url": "guide/fundamentals",
"tooltip": "The fundamentals of Angular",
"children": [
@@ -260,6 +261,7 @@
{
"title": "Techniques",
+ "url": "guide/techniques",
"tooltip": "Techniques for putting Angular to work in your environment",
"children": [
@@ -369,6 +371,7 @@
{
"title": "References",
+ "tooltip": "References on Angular usage and style.",
"children": [
{
"url": "guide/change-log",
@@ -482,5 +485,10 @@
}
]
}
+ ],
+
+ "docVersions": [
+ { "title": "v4.0.0", "url": null },
+ { "title": "v2", "url": "https://v2.angular.io" }
]
}
diff --git a/aio/src/app/app.component.html b/aio/src/app/app.component.html
index 74ae5198c0..c94680d7c4 100644
--- a/aio/src/app/app.component.html
+++ b/aio/src/app/app.component.html
@@ -15,6 +15,12 @@
+
+
+
+ {{version.title}}
+
+
diff --git a/aio/src/app/app.component.spec.ts b/aio/src/app/app.component.spec.ts
index 67c0cfabac..10fa8bb6ee 100644
--- a/aio/src/app/app.component.spec.ts
+++ b/aio/src/app/app.component.spec.ts
@@ -15,12 +15,13 @@ import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
import { GaService } from 'app/shared/ga.service';
import { LocationService } from 'app/shared/location.service';
import { Logger } from 'app/shared/logger.service';
-import { MockLogger } from 'testing/logger.service';
import { MockLocationService } from 'testing/location.service';
+import { MockLogger } from 'testing/logger.service';
import { MockSearchService } from 'testing/search.service';
import { MockSwUpdateNotificationsService } from 'testing/sw-update-notifications.service';
-import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
+import { NavigationNode } from 'app/navigation/navigation.service';
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
+import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
import { SearchService } from 'app/search/search.service';
import { SwUpdateNotificationsService } from 'app/sw-updates/sw-update-notifications.service';
@@ -194,6 +195,31 @@ describe('AppComponent', () => {
});
});
+ describe('SideNav version selector', () => {
+ beforeEach(() => {
+ component.onResize(1033); // side-by-side
+ });
+
+ it('should pick first (current) version by default', () => {
+ const versionSelector = sidenav.querySelector('select');
+ expect(versionSelector.value).toEqual(TestHttp.docVersions[0].title);
+ expect(versionSelector.selectedIndex).toEqual(0);
+ });
+
+ // Older docs versions have an href
+ it('should navigate when change to a version with an href', () => {
+ component.onDocVersionChange(1);
+ expect(locationService.go).toHaveBeenCalledWith(TestHttp.docVersions[1].url);
+ });
+
+ // The current docs version should not have an href
+ // This may change when we perfect our docs versioning approach
+ it('should not navigate when change to a version without an href', () => {
+ component.onDocVersionChange(0);
+ expect(locationService.go).not.toHaveBeenCalled();
+ });
+ });
+
describe('pageId', () => {
it('should set the id of the doc viewer container based on the current doc', () => {
@@ -437,6 +463,11 @@ class TestSearchService {
class TestHttp {
static versionFull = '4.0.0-local+sha.73808dd';
+ static docVersions: NavigationNode[] = [
+ { title: 'v4.0.0' },
+ { title: 'v2', url: 'https://v2.angular.io' }
+ ];
+
// tslint:disable:quotemark
navJson = {
"TopBar": [
@@ -472,6 +503,8 @@ class TestHttp {
"tooltip": "Details of the Angular classes and values."
}
],
+ "docVersions": TestHttp.docVersions,
+
"__versionInfo": {
"raw": "4.0.0-rc.6",
"major": 4,
diff --git a/aio/src/app/app.component.ts b/aio/src/app/app.component.ts
index 36a0ec0dff..062f410731 100644
--- a/aio/src/app/app.component.ts
+++ b/aio/src/app/app.component.ts
@@ -33,6 +33,9 @@ export class AppComponent implements OnInit {
private sideBySideWidth = 1032;
sideNavNodes: NavigationNode[];
topMenuNodes: NavigationNode[];
+
+ currentDocVersion: NavigationNode;
+ docVersions: NavigationNode[];
versionInfo: VersionInfo;
get homeImageUrl() {
@@ -95,9 +98,12 @@ export class AppComponent implements OnInit {
});
this.navigationService.navigationViews.subscribe(views => {
+ this.docVersions = views['docVersions'] || [];
this.footerNodes = views['Footer'] || [];
this.sideNavNodes = views['SideNav'] || [];
this.topMenuNodes = views['TopBar'] || [];
+
+ this.currentDocVersion = this.docVersions[0];
});
this.navigationService.versionInfo.subscribe( vi => this.versionInfo = vi );
@@ -116,6 +122,13 @@ export class AppComponent implements OnInit {
this.isStarting = false;
}
+ onDocVersionChange(versionIndex: number) {
+ const version = this.docVersions[versionIndex];
+ if (version.url) {
+ this.locationService.go(version.url);
+ }
+ }
+
@HostListener('window:resize', ['$event.target.innerWidth'])
onResize(width) {
this.isSideBySide = width > this.sideBySideWidth;
diff --git a/aio/src/app/navigation/navigation.service.spec.ts b/aio/src/app/navigation/navigation.service.spec.ts
index 565bf02aa3..5788939eb7 100644
--- a/aio/src/app/navigation/navigation.service.spec.ts
+++ b/aio/src/app/navigation/navigation.service.spec.ts
@@ -9,6 +9,8 @@ import { Logger } from 'app/shared/logger.service';
describe('NavigationService', () => {
let injector: ReflectiveInjector;
+ let backend: MockBackend;
+ let navService: NavigationService;
function createResponse(body: any) {
return new Response(new ResponseOptions({ body: JSON.stringify(body) }));
@@ -25,19 +27,16 @@ describe('NavigationService', () => {
]);
});
+ beforeEach(() => {
+ backend = injector.get(ConnectionBackend);
+ navService = injector.get(NavigationService);
+ });
+
it('should be creatable', () => {
- const navService: NavigationService = injector.get(NavigationService);
expect(navService).toBeTruthy();
});
describe('navigationViews', () => {
- let backend: MockBackend;
- let navService: NavigationService;
-
- beforeEach(() => {
- backend = injector.get(ConnectionBackend);
- navService = injector.get(NavigationService);
- });
it('should make a single connection to the server', () => {
expect(backend.connectionsArray.length).toEqual(1);
@@ -78,14 +77,12 @@ describe('NavigationService', () => {
expect(views3).toBe(views1);
});
-
it('should do WHAT(?) if the request fails');
});
describe('currentNode', () => {
let currentNode: CurrentNode;
let locationService: MockLocationService;
- let navService: NavigationService;
const topBarNodes: NavigationNode[] = [{ url: 'features', title: 'Features' }];
const sideNavNodes: NavigationNode[] = [
@@ -105,14 +102,9 @@ describe('NavigationService', () => {
__versionInfo: {}
};
-
beforeEach(() => {
locationService = injector.get(LocationService);
-
- navService = injector.get(NavigationService);
navService.currentNode.subscribe(selected => currentNode = selected);
-
- const backend = injector.get(ConnectionBackend);
backend.connectionsArray[0].mockRespond(createResponse(navJson));
});
@@ -190,13 +182,10 @@ describe('NavigationService', () => {
});
describe('versionInfo', () => {
- let navService: NavigationService, versionInfo: VersionInfo;
+ let versionInfo: VersionInfo;
beforeEach(() => {
- navService = injector.get(NavigationService);
navService.versionInfo.subscribe(info => versionInfo = info);
-
- const backend = injector.get(ConnectionBackend);
backend.connectionsArray[0].mockRespond(createResponse({
__versionInfo: { raw: '4.0.0' }
}));
@@ -206,4 +195,24 @@ describe('NavigationService', () => {
expect(versionInfo).toEqual({ raw: '4.0.0' });
});
});
+
+ describe('docVersions', () => {
+ let actualDocVersions: NavigationNode[];
+ let docVersions: NavigationNode[];
+
+ beforeEach(() => {
+ actualDocVersions = [];
+ docVersions = [
+ { title: 'v4.0.0' },
+ { title: 'v2', url: 'https://v2.angular.io' }
+ ];
+
+ navService.navigationViews.subscribe(views => actualDocVersions = views.docVersions);
+ });
+
+ it('should extract the docVersions', () => {
+ backend.connectionsArray[0].mockRespond(createResponse({ docVersions }));
+ expect(actualDocVersions).toEqual(docVersions);
+ });
+ });
});
diff --git a/aio/src/app/navigation/navigation.service.ts b/aio/src/app/navigation/navigation.service.ts
index ec98cd0909..bbb1fd27a8 100644
--- a/aio/src/app/navigation/navigation.service.ts
+++ b/aio/src/app/navigation/navigation.service.ts
@@ -4,6 +4,7 @@ import { Http } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { AsyncSubject } from 'rxjs/AsyncSubject';
import { combineLatest } from 'rxjs/observable/combineLatest';
+import 'rxjs/add/operator/map';
import 'rxjs/add/operator/publishLast';
import 'rxjs/add/operator/publishReplay';
@@ -37,10 +38,11 @@ export class NavigationService {
constructor(private http: Http, private location: LocationService, private logger: Logger) {
const navigationInfo = this.fetchNavigationInfo();
+ this.navigationViews = this.getNavigationViews(navigationInfo);
+
+ this.currentNode = this.getCurrentNode(this.navigationViews);
// The version information is packaged inside the navigation response to save us an extra request.
this.versionInfo = this.getVersionInfo(navigationInfo);
- this.navigationViews = this.getNavigationViews(navigationInfo);
- this.currentNode = this.getCurrentNode(this.navigationViews);
}
/**
@@ -69,7 +71,13 @@ export class NavigationService {
}
private getNavigationViews(navigationInfo: Observable): Observable {
- const navigationViews = navigationInfo.map(response => unpluck(response, '__versionInfo')).publishReplay(1);
+ const navigationViews = navigationInfo.map(response => {
+ const views: NavigationViews = Object.assign({}, response);
+ Object.keys(views).forEach(key => {
+ if (key[0] === '_') { delete views[key]; }
+ });
+ return views;
+ }).publishReplay(1);
navigationViews.connect();
return navigationViews;
}
@@ -120,9 +128,3 @@ export class NavigationService {
}
}
}
-
-function unpluck(obj: any, property: string) {
- const result = Object.assign({}, obj);
- delete result[property];
- return result;
-}
diff --git a/aio/src/app/shared/location.service.spec.ts b/aio/src/app/shared/location.service.spec.ts
index 1c32343391..d45e5d0078 100644
--- a/aio/src/app/shared/location.service.spec.ts
+++ b/aio/src/app/shared/location.service.spec.ts
@@ -6,8 +6,9 @@ import { GaService } from 'app/shared/ga.service';
import { LocationService } from './location.service';
describe('LocationService', () => {
-
let injector: ReflectiveInjector;
+ let location: MockLocationStrategy;
+ let service: LocationService;
beforeEach(() => {
injector = ReflectiveInjector.resolveAndCreate([
@@ -17,19 +18,18 @@ describe('LocationService', () => {
{ provide: LocationStrategy, useClass: MockLocationStrategy },
{ provide: PlatformLocation, useClass: MockPlatformLocation }
]);
+
+ location = injector.get(LocationStrategy);
+ service = injector.get(LocationService);
});
describe('currentUrl', () => {
it('should emit the latest url at the time it is subscribed to', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
-
location.simulatePopState('/initial-url1');
location.simulatePopState('/initial-url2');
location.simulatePopState('/initial-url3');
- const service: LocationService = injector.get(LocationService);
-
location.simulatePopState('/next-url1');
location.simulatePopState('/next-url2');
location.simulatePopState('/next-url3');
@@ -40,9 +40,6 @@ describe('LocationService', () => {
});
it('should emit all location changes after it has been subscribed to', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
- const service: LocationService = injector.get(LocationService);
-
location.simulatePopState('/initial-url1');
location.simulatePopState('/initial-url2');
location.simulatePopState('/initial-url3');
@@ -63,9 +60,6 @@ describe('LocationService', () => {
});
it('should pass only the latest and later urls to each subscriber', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
- const service: LocationService = injector.get(LocationService);
-
location.simulatePopState('/initial-url1');
location.simulatePopState('/initial-url2');
location.simulatePopState('/initial-url3');
@@ -95,8 +89,6 @@ describe('LocationService', () => {
});
it('should strip leading and trailing slashes', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
- const service: LocationService = injector.get(LocationService);
const urls: string[] = [];
service.currentUrl.subscribe(u => urls.push(u));
@@ -117,8 +109,6 @@ describe('LocationService', () => {
describe('currentPath', () => {
it('should strip leading and trailing slashes off the url', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
- const service: LocationService = injector.get(LocationService);
const paths: string[] = [];
service.currentPath.subscribe(p => paths.push(p));
@@ -137,8 +127,6 @@ describe('LocationService', () => {
});
it('should not strip other slashes off the url', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
- const service: LocationService = injector.get(LocationService);
const paths: string[] = [];
service.currentPath.subscribe(p => paths.push(p));
@@ -157,8 +145,6 @@ describe('LocationService', () => {
});
it('should strip the query off the url', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
- const service: LocationService = injector.get(LocationService);
let path: string;
service.currentPath.subscribe(p => path = p);
@@ -169,8 +155,6 @@ describe('LocationService', () => {
});
it('should strip the hash fragment off the url', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
- const service: LocationService = injector.get(LocationService);
const paths: string[] = [];
service.currentPath.subscribe(p => paths.push(p));
@@ -185,14 +169,10 @@ describe('LocationService', () => {
});
it('should emit the latest path at the time it is subscribed to', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
-
location.simulatePopState('/initial/url1');
location.simulatePopState('/initial/url2');
location.simulatePopState('/initial/url3');
- const service: LocationService = injector.get(LocationService);
-
location.simulatePopState('/next/url1');
location.simulatePopState('/next/url2');
location.simulatePopState('/next/url3');
@@ -204,9 +184,6 @@ describe('LocationService', () => {
});
it('should emit all location changes after it has been subscribed to', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
- const service: LocationService = injector.get(LocationService);
-
location.simulatePopState('/initial/url1');
location.simulatePopState('/initial/url2');
location.simulatePopState('/initial/url3');
@@ -227,9 +204,6 @@ describe('LocationService', () => {
});
it('should pass only the latest and later paths to each subscriber', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
- const service: LocationService = injector.get(LocationService);
-
location.simulatePopState('/initial/url1');
location.simulatePopState('/initial/url2');
location.simulatePopState('/initial/url3');
@@ -260,27 +234,19 @@ describe('LocationService', () => {
});
describe('go', () => {
+
it('should update the location', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
- const service: LocationService = injector.get(LocationService);
-
service.go('some-new-url');
-
expect(location.internalPath).toEqual('some-new-url');
expect(location.path(true)).toEqual('some-new-url');
});
it('should emit the new url', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
- const service: LocationService = injector.get(LocationService);
-
+ const urls = [];
service.go('some-initial-url');
- const urls = [];
service.currentUrl.subscribe(url => urls.push(url));
-
service.go('some-new-url');
-
expect(urls).toEqual([
'some-initial-url',
'some-new-url'
@@ -288,8 +254,6 @@ describe('LocationService', () => {
});
it('should strip leading and trailing slashes', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
- const service: LocationService = injector.get(LocationService);
let url: string;
service.currentUrl.subscribe(u => url = u);
@@ -299,21 +263,48 @@ describe('LocationService', () => {
expect(location.path(true)).toEqual('some/url');
expect(url).toBe('some/url');
});
+
+ it('should ignore undefined URL string', noUrlTest(undefined));
+ it('should ignore null URL string', noUrlTest(null));
+ it('should ignore empty URL string', noUrlTest(''));
+ function noUrlTest(testUrl: string) {
+ return function() {
+ const initialUrl = 'some/url';
+ const goExternalSpy = spyOn(service, 'goExternal');
+ let url: string;
+
+ service.go(initialUrl);
+ service.currentUrl.subscribe(u => url = u);
+
+ service.go(testUrl);
+ expect(url).toEqual(initialUrl, 'should not have re-navigated locally');
+ expect(goExternalSpy.wasCalled).toBeFalsy('should not have navigated externally');
+ };
+ }
+
+ it('should leave the site for external url that starts with "http"', () => {
+ const goExternalSpy = spyOn(service, 'goExternal');
+ const externalUrl = 'http://some/far/away/land';
+ service.go(externalUrl);
+ expect(goExternalSpy).toHaveBeenCalledWith(externalUrl);
+ });
+
+ it('should not update currentUrl for external url that starts with "http"', () => {
+ let localUrl: string;
+ spyOn(service, 'goExternal');
+ service.currentUrl.subscribe(url => localUrl = url);
+ service.go('https://some/far/away/land');
+ expect(localUrl).toBeFalsy('should not set local url');
+ });
});
describe('search', () => {
it('should read the query from the current location.path', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
- const service: LocationService = injector.get(LocationService);
-
location.simulatePopState('a/b/c?foo=bar&moo=car');
expect(service.search()).toEqual({ foo: 'bar', moo: 'car' });
});
it('should cope with an empty query', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
- const service: LocationService = injector.get(LocationService);
-
location.simulatePopState('a/b/c');
expect(service.search()).toEqual({ });
@@ -328,25 +319,16 @@ describe('LocationService', () => {
});
it('should URL decode query values', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
- const service: LocationService = injector.get(LocationService);
-
location.simulatePopState('a/b/c?query=a%26b%2Bc%20d');
expect(service.search()).toEqual({ query: 'a&b+c d' });
});
it('should URL decode query keys', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
- const service: LocationService = injector.get(LocationService);
-
location.simulatePopState('a/b/c?a%26b%2Bc%20d=value');
expect(service.search()).toEqual({ 'a&b+c d': 'value' });
});
it('should cope with a hash on the URL', () => {
- const location: MockLocationStrategy = injector.get(LocationStrategy);
- const service: LocationService = injector.get(LocationService);
-
spyOn(location, 'path').and.callThrough();
service.search();
expect(location.path).toHaveBeenCalledWith(false);
@@ -354,53 +336,47 @@ describe('LocationService', () => {
});
describe('setSearch', () => {
- it('should call replaceState on PlatformLocation', () => {
- const location: MockPlatformLocation = injector.get(PlatformLocation);
- const service: LocationService = injector.get(LocationService);
+ let platformLocation: MockPlatformLocation;
+ beforeEach(() => {
+ platformLocation = injector.get(PlatformLocation);
+ });
+
+ it('should call replaceState on PlatformLocation', () => {
const params = {};
service.setSearch('Some label', params);
- expect(location.replaceState).toHaveBeenCalledWith(jasmine.any(Object), 'Some label', 'a/b/c');
+ expect(platformLocation.replaceState).toHaveBeenCalledWith(jasmine.any(Object), 'Some label', 'a/b/c');
});
it('should convert the params to a query string', () => {
- const location: MockPlatformLocation = injector.get(PlatformLocation);
- const service: LocationService = injector.get(LocationService);
-
const params = { foo: 'bar', moo: 'car' };
service.setSearch('Some label', params);
- expect(location.replaceState).toHaveBeenCalledWith(jasmine.any(Object), 'Some label', jasmine.any(String));
- const [path, query] = location.replaceState.calls.mostRecent().args[2].split('?');
+ expect(platformLocation.replaceState).toHaveBeenCalledWith(jasmine.any(Object), 'Some label', jasmine.any(String));
+ const [path, query] = platformLocation.replaceState.calls.mostRecent().args[2].split('?');
expect(path).toEqual('a/b/c');
expect(query).toContain('foo=bar');
expect(query).toContain('moo=car');
});
it('should URL encode param values', () => {
- const location: MockPlatformLocation = injector.get(PlatformLocation);
- const service: LocationService = injector.get(LocationService);
-
const params = { query: 'a&b+c d' };
service.setSearch('', params);
- const [, query] = location.replaceState.calls.mostRecent().args[2].split('?');
+ const [, query] = platformLocation.replaceState.calls.mostRecent().args[2].split('?');
expect(query).toContain('query=a%26b%2Bc%20d');
});
it('should URL encode param keys', () => {
- const location: MockPlatformLocation = injector.get(PlatformLocation);
- const service: LocationService = injector.get(LocationService);
-
const params = { 'a&b+c d': 'value' };
service.setSearch('', params);
- const [, query] = location.replaceState.calls.mostRecent().args[2].split('?');
+ const [, query] = platformLocation.replaceState.calls.mostRecent().args[2].split('?');
expect(query).toContain('a%26b%2Bc%20d=value');
});
});
describe('handleAnchorClick', () => {
- let service: LocationService, anchor: HTMLAnchorElement;
+ let anchor: HTMLAnchorElement;
+
beforeEach(() => {
- service = injector.get(LocationService);
anchor = document.createElement('a');
});
@@ -520,14 +496,10 @@ describe('LocationService', () => {
describe('google analytics - GaService#locationChanged', () => {
let gaLocationChanged: jasmine.Spy;
- let location: Location;
- let service: LocationService;
beforeEach(() => {
const gaService = injector.get(GaService);
gaLocationChanged = gaService.locationChanged;
- location = injector.get(Location);
- service = injector.get(LocationService);
});
it('should call locationChanged with initial URL', () => {
@@ -546,8 +518,7 @@ describe('LocationService', () => {
});
it('should call locationChanged when window history changes', () => {
- const locationStrategy: MockLocationStrategy = injector.get(LocationStrategy);
- locationStrategy.simulatePopState('/next-url');
+ location.simulatePopState('/next-url');
expect(gaLocationChanged.calls.count()).toBe(2, 'gaService.locationChanged');
const args = gaLocationChanged.calls.argsFor(1);
diff --git a/aio/src/app/shared/location.service.ts b/aio/src/app/shared/location.service.ts
index 50c3668996..d1c533a5d2 100644
--- a/aio/src/app/shared/location.service.ts
+++ b/aio/src/app/shared/location.service.ts
@@ -36,9 +36,19 @@ export class LocationService {
// TODO?: ignore if url-without-hash-or-search matches current location?
go(url: string) {
+ if (!url) { return; }
url = this.stripSlashes(url);
- this.location.go(url);
- this.urlSubject.next(url);
+ if (/^http/.test(url)) {
+ // Has http protocol so leave the site
+ this.goExternal(url);
+ } else {
+ this.location.go(url);
+ this.urlSubject.next(url);
+ }
+ }
+
+ goExternal(url: string) {
+ location.assign(url);
}
private stripSlashes(url: string) {
diff --git a/aio/src/styles/1-layouts/_sidenav.scss b/aio/src/styles/1-layouts/_sidenav.scss
index 3471a66651..8896bc7fb5 100644
--- a/aio/src/styles/1-layouts/_sidenav.scss
+++ b/aio/src/styles/1-layouts/_sidenav.scss
@@ -169,3 +169,13 @@ aio-nav-menu.top-menu {
}
}
+
+// Angular version selector
+md-sidenav .doc-version {
+ padding: 10px;
+
+ &:hover {
+ text-shadow: 0 0 5px #ffffff;
+ background-color: $lightgray;
+ }
+}
diff --git a/aio/src/styles/2-modules/_card.scss b/aio/src/styles/2-modules/_card.scss
index e53604b462..31aaded6b7 100644
--- a/aio/src/styles/2-modules/_card.scss
+++ b/aio/src/styles/2-modules/_card.scss
@@ -16,7 +16,7 @@
&:hover {
text-decoration: none;
- h2 {
+ section {
color: $blue;
}
@@ -34,7 +34,7 @@
}
- h2 {
+ section {
color: $darkgray;
font-size: 20px;
line-height: 24px;
@@ -69,4 +69,7 @@
font-size: 13px;
}
}
-}
\ No newline at end of file
+ .card-footer.center {
+ text-align: center;
+ }
+}
diff --git a/aio/src/styles/2-modules/_contributor.scss b/aio/src/styles/2-modules/_contributor.scss
index 8532bfc1d8..6d0e456213 100644
--- a/aio/src/styles/2-modules/_contributor.scss
+++ b/aio/src/styles/2-modules/_contributor.scss
@@ -133,7 +133,7 @@ aio-contributor {
padding: 16px 24px;
transform:rotateY(180deg);
- h3 {
+ section {
display: none;
}
@@ -168,7 +168,7 @@ aio-contributor {
transition: all .2s ease-in-out;
}
- h3 {
+ section {
font-size: 14px;
font-weight: 500;
padding: 8px;
@@ -188,4 +188,4 @@ aio-contributor {
overflow: scroll;
font-weight: 400;
}
-}
\ No newline at end of file
+}
diff --git a/aio/src/testing/location.service.ts b/aio/src/testing/location.service.ts
index ad8bb2d329..2656769b95 100644
--- a/aio/src/testing/location.service.ts
+++ b/aio/src/testing/location.service.ts
@@ -9,6 +9,7 @@ export class MockLocationService {
setSearch = jasmine.createSpy('setSearch');
go = jasmine.createSpy('Location.go').and
.callFake((url: string) => this.urlSubject.next(url));
+ goExternal = jasmine.createSpy('Location.goExternal');
handleAnchorClick = jasmine.createSpy('Location.handleAnchorClick')
.and.returnValue(false); // prevent click from causing a browser navigation
diff --git a/aio/tools/transforms/angular.io-package/processors/processNavigationMap.js b/aio/tools/transforms/angular.io-package/processors/processNavigationMap.js
index 049b1ac4c5..7582ff9788 100644
--- a/aio/tools/transforms/angular.io-package/processors/processNavigationMap.js
+++ b/aio/tools/transforms/angular.io-package/processors/processNavigationMap.js
@@ -34,13 +34,15 @@ function walk(node, map, path) {
let errors = [];
for(const key in node) {
const child = node[key];
- if (key === 'url') {
- const url = child.replace(/#.*$/, ''); // strip hash
- if (isRelative(url) && !map[url]) {
- errors.push({ path: path.join('.'), url });
+ if (child !== null) { // null is allowed
+ if (key === 'url') {
+ const url = child.replace(/#.*$/, ''); // strip hash
+ if (isRelative(url) && !map[url]) {
+ errors.push({ path: path.join('.'), url });
+ }
+ } else if (typeof child !== 'string') {
+ errors = errors.concat(walk(child, map, path.concat([key])));
}
- } else if (typeof child !== 'string') {
- errors = errors.concat(walk(child, map, path.concat([key])));
}
}
return errors;