feat(aio): revise Docs page; docs version selector in sidenav

This commit is contained in:
Ward Bell 2017-04-25 14:48:01 -07:00 committed by Pete Bacon Darwin
parent de25cfc0cb
commit 4be1966a21
16 changed files with 245 additions and 195 deletions

View File

@ -0,0 +1,3 @@
# Fundamentals of Angular
Learn the fundamental features of Angular in this section of the guide.

View File

@ -0,0 +1,3 @@
# Techniques
Learn important Angular application techniques such as how to setup, secure, and deploy your application.

View File

@ -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?
<div class="card-container clearfix">
<a href="/content/live-examples/quickstart/eplnkr.html" target="_blank" class="card"
title="Experience Angular in a live coding environment">
<section>Experience Angular</section>
<p>A quick look at an Angular application.</p>
<p class="card-footer">Angular in Action</p>
</a>
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.
<div class="alert is-important">
If you're new to Angular, we recommend that you start with these two categories before moving selectively through the others.
<div class="card">
<section>Get Going with Angular</section>
<p>Get going on your own environment with the Quickstart and Tutorial</p>
<p class="card-footer center" >
<a href="guide/quickstart" title="Angular Quickstart">Quickstart</a> &nbsp;
<a href="tutorial" title="Angular Tutorial">Tutorial</a>
</p>
<!--<p class="card-footer"><a href="guide/quickstart">Quickstart</a></p>
<p class="card-footer"><a href="guide/tutorial">Tutorial</a></p>-->
</div>
<a href="fundamentals" class="card" title="Angular Fundamentals">
<section>Fundamentals</section>
<p>Get additional information on specific topics in the Fundamentals section.</p>
<p class="card-footer">Fundamentals</p>
</a>
</div>
- **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
<style>live-example a {font-weight: 800}</style>
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 <live-example name="quickstart" noDownload style="font-weight: 800 !important;"></live-example> **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 <live-example name="quickstart" downloadOnly>download the example,</live-example>
unzip it, and run locally with these terminal commands:
<code-example language="sh" class="code-shell" linenums="false">
npm install
npm start
</code-example>
## 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.
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").

View File

@ -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" }
]
}

View File

@ -15,6 +15,12 @@
<md-sidenav [ngClass]="{'collapsed': !isSideBySide }" #sidenav class="sidenav" [opened]="isOpened" [mode]="mode">
<aio-nav-menu *ngIf="!isSideBySide" class="top-menu" [nodes]="topMenuNodes" [currentNode]="currentNode"></aio-nav-menu>
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNode" ></aio-nav-menu>
<div class="doc-version" title="Angular docs version {{currentDocVersion?.title}}">
<select (change)="onDocVersionChange($event.target.selectedIndex)">
<option *ngFor="let version of docVersions" [value]="version.title">{{version.title}}</option>
</select>
</div>
</md-sidenav>
<section class="sidenav-content" [id]="pageId" role="content">

View File

@ -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,

View File

@ -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;

View File

@ -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);
});
});
});

View File

@ -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<NavigationResponse>): Observable<NavigationViews> {
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;
}

View File

@ -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);

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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;
}
}
}
.card-footer.center {
text-align: center;
}
}

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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;