1980 lines
64 KiB
HTML
1980 lines
64 KiB
HTML
<html lang="en"><head></head><body>
|
|
<form id="mainForm" method="post" action="https://run.stackblitz.com/api/angular/v1?file=src/app/app.component.ts" target="_self"><input type="hidden" name="files[src/app/app.component.ts]" value="import { Component } from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'app-root',
|
|
templateUrl: './app.component.html'
|
|
})
|
|
export class AppComponent {
|
|
showHeroes = true;
|
|
showConfig = true;
|
|
showDownloader = true;
|
|
showUploader = true;
|
|
showSearch = true;
|
|
|
|
toggleHeroes() { this.showHeroes = !this.showHeroes; }
|
|
toggleConfig() { this.showConfig = !this.showConfig; }
|
|
toggleDownloader() { this.showDownloader = !this.showDownloader; }
|
|
toggleUploader() { this.showUploader = !this.showUploader; }
|
|
toggleSearch() { this.showSearch = !this.showSearch; }
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/app.module.ts]" value="import { NgModule } from '@angular/core';
|
|
import { BrowserModule } from '@angular/platform-browser';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { HttpClientModule } from '@angular/common/http';
|
|
import { HttpClientXsrfModule } from '@angular/common/http';
|
|
|
|
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
|
|
import { InMemoryDataService } from './in-memory-data.service';
|
|
|
|
import { RequestCache, RequestCacheWithMap } from './request-cache.service';
|
|
|
|
import { AppComponent } from './app.component';
|
|
import { AuthService } from './auth.service';
|
|
import { ConfigComponent } from './config/config.component';
|
|
import { DownloaderComponent } from './downloader/downloader.component';
|
|
import { HeroesComponent } from './heroes/heroes.component';
|
|
import { HttpErrorHandler } from './http-error-handler.service';
|
|
import { MessageService } from './message.service';
|
|
import { MessagesComponent } from './messages/messages.component';
|
|
import { PackageSearchComponent } from './package-search/package-search.component';
|
|
import { UploaderComponent } from './uploader/uploader.component';
|
|
|
|
import { httpInterceptorProviders } from './http-interceptors/index';
|
|
|
|
@NgModule({
|
|
imports: [
|
|
BrowserModule,
|
|
FormsModule,
|
|
// import HttpClientModule after BrowserModule.
|
|
HttpClientModule,
|
|
HttpClientXsrfModule.withOptions({
|
|
cookieName: 'My-Xsrf-Cookie',
|
|
headerName: 'My-Xsrf-Header',
|
|
}),
|
|
|
|
// The HttpClientInMemoryWebApiModule module intercepts HTTP requests
|
|
// and returns simulated server responses.
|
|
// Remove it when a real server is ready to receive requests.
|
|
HttpClientInMemoryWebApiModule.forRoot(
|
|
InMemoryDataService, {
|
|
dataEncapsulation: false,
|
|
passThruUnknownUrl: true,
|
|
put204: false // return entity after PUT/update
|
|
}
|
|
)
|
|
],
|
|
declarations: [
|
|
AppComponent,
|
|
ConfigComponent,
|
|
DownloaderComponent,
|
|
HeroesComponent,
|
|
MessagesComponent,
|
|
UploaderComponent,
|
|
PackageSearchComponent,
|
|
],
|
|
providers: [
|
|
AuthService,
|
|
HttpErrorHandler,
|
|
MessageService,
|
|
{ provide: RequestCache, useClass: RequestCacheWithMap },
|
|
httpInterceptorProviders
|
|
],
|
|
bootstrap: [ AppComponent ]
|
|
})
|
|
export class AppModule {}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/auth.service.ts]" value="import { Injectable } from '@angular/core';
|
|
|
|
/** Mock client-side authentication/authorization service */
|
|
@Injectable()
|
|
export class AuthService {
|
|
getAuthorizationToken() {
|
|
return 'some-auth-token';
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/config/config.component.ts]" value="import { Component } from '@angular/core';
|
|
import { Config, ConfigService } from './config.service';
|
|
|
|
@Component({
|
|
selector: 'app-config',
|
|
templateUrl: './config.component.html',
|
|
providers: [ ConfigService ],
|
|
styles: ['.error {color: red;}']
|
|
})
|
|
export class ConfigComponent {
|
|
error: any;
|
|
headers: string[];
|
|
config: Config;
|
|
|
|
constructor(private configService: ConfigService) {}
|
|
|
|
clear() {
|
|
this.config = undefined;
|
|
this.error = undefined;
|
|
this.headers = undefined;
|
|
}
|
|
|
|
showConfig() {
|
|
this.configService.getConfig()
|
|
.subscribe(
|
|
(data: Config) => this.config = { ...data }, // success path
|
|
error => this.error = error // error path
|
|
);
|
|
}
|
|
|
|
showConfig_v1() {
|
|
this.configService.getConfig_1()
|
|
.subscribe((data: Config) => this.config = {
|
|
heroesUrl: data.heroesUrl,
|
|
textfile: data.textfile,
|
|
date: data.date,
|
|
});
|
|
}
|
|
|
|
showConfig_v2() {
|
|
this.configService.getConfig()
|
|
// clone the data object, using its known Config shape
|
|
.subscribe((data: Config) => this.config = { ...data });
|
|
}
|
|
|
|
showConfigResponse() {
|
|
this.configService.getConfigResponse()
|
|
// resp is of type `HttpResponse<Config>`
|
|
.subscribe(resp => {
|
|
// display its headers
|
|
const keys = resp.headers.keys();
|
|
this.headers = keys.map(key =>
|
|
`${key}: ${resp.headers.get(key)}`);
|
|
|
|
// access the body directly, which is typed as `Config`.
|
|
this.config = { ... resp.body };
|
|
});
|
|
}
|
|
makeError() {
|
|
this.configService.makeIntentionalError().subscribe(null, error => this.error = error );
|
|
}
|
|
|
|
getType(val: any): string {
|
|
return val instanceof Date ? 'date' : Array.isArray(val) ? 'array' : typeof val;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/config/config.service.ts]" value="import { Injectable } from '@angular/core';
|
|
import { HttpClient } from '@angular/common/http';
|
|
import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
|
|
|
|
import { Observable, throwError } from 'rxjs';
|
|
import { catchError, retry } from 'rxjs/operators';
|
|
|
|
export interface Config {
|
|
heroesUrl: string;
|
|
textfile: string;
|
|
date: any;
|
|
}
|
|
|
|
@Injectable()
|
|
export class ConfigService {
|
|
configUrl = 'assets/config.json';
|
|
|
|
constructor(private http: HttpClient) { }
|
|
|
|
getConfig() {
|
|
return this.http.get<Config>(this.configUrl)
|
|
.pipe(
|
|
retry(3), // retry a failed request up to 3 times
|
|
catchError(this.handleError) // then handle the error
|
|
);
|
|
}
|
|
|
|
getConfig_1() {
|
|
return this.http.get(this.configUrl);
|
|
}
|
|
|
|
getConfig_2() {
|
|
// now returns an Observable of Config
|
|
return this.http.get<Config>(this.configUrl);
|
|
}
|
|
|
|
getConfig_3() {
|
|
return this.http.get<Config>(this.configUrl)
|
|
.pipe(
|
|
catchError(this.handleError)
|
|
);
|
|
}
|
|
|
|
getConfigResponse(): Observable<HttpResponse<Config>> {
|
|
return this.http.get<Config>(
|
|
this.configUrl, { observe: 'response' });
|
|
}
|
|
|
|
private handleError(error: HttpErrorResponse) {
|
|
if (error.error instanceof ErrorEvent) {
|
|
// A client-side or network error occurred. Handle it accordingly.
|
|
console.error('An error occurred:', error.error.message);
|
|
} else {
|
|
// The backend returned an unsuccessful response code.
|
|
// The response body may contain clues as to what went wrong.
|
|
console.error(
|
|
`Backend returned code ${error.status}, ` +
|
|
`body was: ${error.error}`);
|
|
}
|
|
// Return an observable with a user-facing error message.
|
|
return throwError(
|
|
'Something bad happened; please try again later.');
|
|
}
|
|
|
|
makeIntentionalError() {
|
|
return this.http.get('not/a/real/url')
|
|
.pipe(
|
|
catchError(this.handleError)
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/downloader/downloader.component.ts]" value="import { Component } from '@angular/core';
|
|
import { DownloaderService } from './downloader.service';
|
|
|
|
@Component({
|
|
selector: 'app-downloader',
|
|
templateUrl: './downloader.component.html',
|
|
providers: [ DownloaderService ]
|
|
})
|
|
export class DownloaderComponent {
|
|
contents: string;
|
|
constructor(private downloaderService: DownloaderService) {}
|
|
|
|
clear() {
|
|
this.contents = undefined;
|
|
}
|
|
|
|
download() {
|
|
this.downloaderService.getTextFile('assets/textfile.txt')
|
|
.subscribe(results => this.contents = results);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/downloader/downloader.service.ts]" value="import { Injectable } from '@angular/core';
|
|
import { HttpClient } from '@angular/common/http';
|
|
|
|
import { tap } from 'rxjs/operators';
|
|
|
|
import { MessageService } from '../message.service';
|
|
|
|
@Injectable()
|
|
export class DownloaderService {
|
|
constructor(
|
|
private http: HttpClient,
|
|
private messageService: MessageService) { }
|
|
|
|
getTextFile(filename: string) {
|
|
// The Observable returned by get() is of type Observable<string>
|
|
// because a text response was specified.
|
|
// There's no need to pass a <string> type parameter to get().
|
|
return this.http.get(filename, {responseType: 'text'})
|
|
.pipe(
|
|
tap( // Log the result or error
|
|
data => this.log(filename, data),
|
|
error => this.logError(filename, error)
|
|
)
|
|
);
|
|
}
|
|
|
|
private log(filename: string, data: string) {
|
|
const message = `DownloaderService downloaded "${filename}" and got "${data}".`;
|
|
this.messageService.add(message);
|
|
}
|
|
|
|
private logError(filename: string, error: any) {
|
|
const message = `DownloaderService failed to download "${filename}"; got error "${error.message}".`;
|
|
console.error(message);
|
|
this.messageService.add(message);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/heroes/hero.ts]" value="export interface Hero {
|
|
id: number;
|
|
name: string;
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/heroes/heroes.component.ts]" value="import { Component, OnInit } from '@angular/core';
|
|
|
|
import { Hero } from './hero';
|
|
import { HeroesService } from './heroes.service';
|
|
|
|
@Component({
|
|
selector: 'app-heroes',
|
|
templateUrl: './heroes.component.html',
|
|
providers: [HeroesService],
|
|
styleUrls: ['./heroes.component.css']
|
|
})
|
|
export class HeroesComponent implements OnInit {
|
|
heroes: Hero[];
|
|
editHero: Hero; // the hero currently being edited
|
|
|
|
constructor(private heroesService: HeroesService) {}
|
|
|
|
ngOnInit() {
|
|
this.getHeroes();
|
|
}
|
|
|
|
getHeroes(): void {
|
|
this.heroesService.getHeroes()
|
|
.subscribe(heroes => (this.heroes = heroes));
|
|
}
|
|
|
|
add(name: string): void {
|
|
this.editHero = undefined;
|
|
name = name.trim();
|
|
if (!name) {
|
|
return;
|
|
}
|
|
|
|
// The server will generate the id for this new hero
|
|
const newHero: Hero = { name } as Hero;
|
|
this.heroesService
|
|
.addHero(newHero)
|
|
.subscribe(hero => this.heroes.push(hero));
|
|
}
|
|
|
|
delete(hero: Hero): void {
|
|
this.heroes = this.heroes.filter(h => h !== hero);
|
|
this.heroesService
|
|
.deleteHero(hero.id)
|
|
.subscribe();
|
|
/*
|
|
// oops ... subscribe() is missing so nothing happens
|
|
this.heroesService.deleteHero(hero.id);
|
|
*/
|
|
}
|
|
|
|
edit(hero: Hero) {
|
|
this.editHero = hero;
|
|
}
|
|
|
|
search(searchTerm: string) {
|
|
this.editHero = undefined;
|
|
if (searchTerm) {
|
|
this.heroesService
|
|
.searchHeroes(searchTerm)
|
|
.subscribe(heroes => (this.heroes = heroes));
|
|
}
|
|
}
|
|
|
|
update() {
|
|
if (this.editHero) {
|
|
this.heroesService
|
|
.updateHero(this.editHero)
|
|
.subscribe(hero => {
|
|
// replace the hero in the heroes list with update from server
|
|
const ix = hero ? this.heroes.findIndex(h => h.id === hero.id) : -1;
|
|
if (ix > -1) {
|
|
this.heroes[ix] = hero;
|
|
}
|
|
});
|
|
this.editHero = undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/heroes/heroes.service.ts]" value="import { Injectable } from '@angular/core';
|
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
import { HttpHeaders } from '@angular/common/http';
|
|
|
|
|
|
import { Observable } from 'rxjs';
|
|
import { catchError } from 'rxjs/operators';
|
|
|
|
import { Hero } from './hero';
|
|
import { HttpErrorHandler, HandleError } from '../http-error-handler.service';
|
|
|
|
const httpOptions = {
|
|
headers: new HttpHeaders({
|
|
'Content-Type': 'application/json',
|
|
Authorization: 'my-auth-token'
|
|
})
|
|
};
|
|
|
|
@Injectable()
|
|
export class HeroesService {
|
|
heroesUrl = 'api/heroes'; // URL to web api
|
|
private handleError: HandleError;
|
|
|
|
constructor(
|
|
private http: HttpClient,
|
|
httpErrorHandler: HttpErrorHandler) {
|
|
this.handleError = httpErrorHandler.createHandleError('HeroesService');
|
|
}
|
|
|
|
/** GET heroes from the server */
|
|
getHeroes(): Observable<Hero[]> {
|
|
return this.http.get<Hero[]>(this.heroesUrl)
|
|
.pipe(
|
|
catchError(this.handleError('getHeroes', []))
|
|
);
|
|
}
|
|
|
|
/* GET heroes whose name contains search term */
|
|
searchHeroes(term: string): Observable<Hero[]> {
|
|
term = term.trim();
|
|
|
|
// Add safe, URL encoded search parameter if there is a search term
|
|
const options = term ?
|
|
{ params: new HttpParams().set('name', term) } : {};
|
|
|
|
return this.http.get<Hero[]>(this.heroesUrl, options)
|
|
.pipe(
|
|
catchError(this.handleError<Hero[]>('searchHeroes', []))
|
|
);
|
|
}
|
|
|
|
//////// Save methods //////////
|
|
|
|
/** POST: add a new hero to the database */
|
|
addHero(hero: Hero): Observable<Hero> {
|
|
return this.http.post<Hero>(this.heroesUrl, hero, httpOptions)
|
|
.pipe(
|
|
catchError(this.handleError('addHero', hero))
|
|
);
|
|
}
|
|
|
|
/** DELETE: delete the hero from the server */
|
|
deleteHero(id: number): Observable<{}> {
|
|
const url = `${this.heroesUrl}/${id}`; // DELETE api/heroes/42
|
|
return this.http.delete(url, httpOptions)
|
|
.pipe(
|
|
catchError(this.handleError('deleteHero'))
|
|
);
|
|
}
|
|
|
|
/** PUT: update the hero on the server. Returns the updated hero upon success. */
|
|
updateHero(hero: Hero): Observable<Hero> {
|
|
httpOptions.headers =
|
|
httpOptions.headers.set('Authorization', 'my-new-auth-token');
|
|
|
|
return this.http.put<Hero>(this.heroesUrl, hero, httpOptions)
|
|
.pipe(
|
|
catchError(this.handleError('updateHero', hero))
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/http-error-handler.service.ts]" value="import { Injectable } from '@angular/core';
|
|
import { HttpErrorResponse } from '@angular/common/http';
|
|
|
|
import { Observable, of } from 'rxjs';
|
|
|
|
import { MessageService } from './message.service';
|
|
|
|
/** Type of the handleError function returned by HttpErrorHandler.createHandleError */
|
|
export type HandleError =
|
|
<T> (operation?: string, result?: T) => (error: HttpErrorResponse) => Observable<T>;
|
|
|
|
/** Handles HttpClient errors */
|
|
@Injectable()
|
|
export class HttpErrorHandler {
|
|
constructor(private messageService: MessageService) { }
|
|
|
|
/** Create curried handleError function that already knows the service name */
|
|
createHandleError = (serviceName = '') => {
|
|
return <T>(operation = 'operation', result = {} as T) =>
|
|
this.handleError(serviceName, operation, result);
|
|
}
|
|
|
|
/**
|
|
* Returns a function that handles Http operation failures.
|
|
* This error handler lets the app continue to run as if no error occurred.
|
|
* @param serviceName = name of the data service that attempted the operation
|
|
* @param operation - name of the operation that failed
|
|
* @param result - optional value to return as the observable result
|
|
*/
|
|
handleError<T>(serviceName = '', operation = 'operation', result = {} as T) {
|
|
|
|
return (error: HttpErrorResponse): Observable<T> => {
|
|
// TODO: send the error to remote logging infrastructure
|
|
console.error(error); // log to console instead
|
|
|
|
const message = (error.error instanceof ErrorEvent) ?
|
|
error.error.message :
|
|
`server returned code ${error.status} with body "${error.error}"`;
|
|
|
|
// TODO: better job of transforming error for user consumption
|
|
this.messageService.add(`${serviceName}: ${operation} failed: ${message}`);
|
|
|
|
// Let the app keep running by returning a safe result.
|
|
return of( result );
|
|
};
|
|
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/http-interceptors/auth-interceptor.ts]" value="import { Injectable } from '@angular/core';
|
|
import {
|
|
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
|
|
} from '@angular/common/http';
|
|
|
|
import { AuthService } from '../auth.service';
|
|
|
|
@Injectable()
|
|
export class AuthInterceptor implements HttpInterceptor {
|
|
|
|
constructor(private auth: AuthService) {}
|
|
|
|
intercept(req: HttpRequest<any>, next: HttpHandler) {
|
|
// Get the auth token from the service.
|
|
const authToken = this.auth.getAuthorizationToken();
|
|
|
|
/*
|
|
* The verbose way:
|
|
// Clone the request and replace the original headers with
|
|
// cloned headers, updated with the authorization.
|
|
const authReq = req.clone({
|
|
headers: req.headers.set('Authorization', authToken)
|
|
});
|
|
*/
|
|
// Clone the request and set the new header in one step.
|
|
const authReq = req.clone({ setHeaders: { Authorization: authToken } });
|
|
|
|
// send cloned request with header to the next handler.
|
|
return next.handle(authReq);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/http-interceptors/caching-interceptor.ts]" value="import { Injectable } from '@angular/core';
|
|
import {
|
|
HttpEvent, HttpHeaders, HttpRequest, HttpResponse,
|
|
HttpInterceptor, HttpHandler
|
|
} from '@angular/common/http';
|
|
|
|
import { Observable, of } from 'rxjs';
|
|
import { startWith, tap } from 'rxjs/operators';
|
|
|
|
import { RequestCache } from '../request-cache.service';
|
|
import { searchUrl } from '../package-search/package-search.service';
|
|
|
|
|
|
/**
|
|
* If request is cacheable (e.g., package search) and
|
|
* response is in cache return the cached response as observable.
|
|
* If has 'x-refresh' header that is true,
|
|
* then also re-run the package search, using response from next(),
|
|
* returning an observable that emits the cached response first.
|
|
*
|
|
* If not in cache or not cacheable,
|
|
* pass request through to next()
|
|
*/
|
|
@Injectable()
|
|
export class CachingInterceptor implements HttpInterceptor {
|
|
constructor(private cache: RequestCache) {}
|
|
|
|
intercept(req: HttpRequest<any>, next: HttpHandler) {
|
|
// continue if not cacheable.
|
|
if (!isCacheable(req)) { return next.handle(req); }
|
|
|
|
const cachedResponse = this.cache.get(req);
|
|
// cache-then-refresh
|
|
if (req.headers.get('x-refresh')) {
|
|
const results$ = sendRequest(req, next, this.cache);
|
|
return cachedResponse ?
|
|
results$.pipe( startWith(cachedResponse) ) :
|
|
results$;
|
|
}
|
|
// cache-or-fetch
|
|
return cachedResponse ?
|
|
of(cachedResponse) : sendRequest(req, next, this.cache);
|
|
}
|
|
}
|
|
|
|
|
|
/** Is this request cacheable? */
|
|
function isCacheable(req: HttpRequest<any>) {
|
|
// Only GET requests are cacheable
|
|
return req.method === 'GET' &&
|
|
// Only npm package search is cacheable in this app
|
|
-1 < req.url.indexOf(searchUrl);
|
|
}
|
|
|
|
/**
|
|
* Get server response observable by sending request to `next()`.
|
|
* Will add the response to the cache on the way out.
|
|
*/
|
|
function sendRequest(
|
|
req: HttpRequest<any>,
|
|
next: HttpHandler,
|
|
cache: RequestCache): Observable<HttpEvent<any>> {
|
|
|
|
// No headers allowed in npm search request
|
|
const noHeaderReq = req.clone({ headers: new HttpHeaders() });
|
|
|
|
return next.handle(noHeaderReq).pipe(
|
|
tap(event => {
|
|
// There may be other events besides the response.
|
|
if (event instanceof HttpResponse) {
|
|
cache.put(req, event); // Update the cache.
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/http-interceptors/custom-json-interceptor.ts]" value="import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http';
|
|
import { Injectable } from '@angular/core';
|
|
import { map } from 'rxjs/operators';
|
|
|
|
// The JsonParser class acts as a base class for custom parsers and as the DI token.
|
|
@Injectable()
|
|
export abstract class JsonParser {
|
|
abstract parse(text: string): any;
|
|
}
|
|
|
|
@Injectable()
|
|
export class CustomJsonInterceptor implements HttpInterceptor {
|
|
constructor(private jsonParser: JsonParser) {}
|
|
|
|
intercept(httpRequest: HttpRequest<any>, next: HttpHandler) {
|
|
if (httpRequest.responseType === 'json') {
|
|
// If the expected response type is JSON then handle it here.
|
|
return this.handleJsonResponse(httpRequest, next);
|
|
} else {
|
|
return next.handle(httpRequest);
|
|
}
|
|
}
|
|
|
|
private handleJsonResponse(httpRequest: HttpRequest<any>, next: HttpHandler) {
|
|
// Override the responseType to disable the default JSON parsing.
|
|
httpRequest = httpRequest.clone({responseType: 'text'});
|
|
// Handle the response using the custom parser.
|
|
return next.handle(httpRequest).pipe(map(event => this.parseJsonResponse(event)));
|
|
}
|
|
|
|
private parseJsonResponse(event: HttpEvent<any>) {
|
|
if (event instanceof HttpResponse && typeof event.body === 'string') {
|
|
return event.clone({body: this.jsonParser.parse(event.body)});
|
|
} else {
|
|
return event;
|
|
}
|
|
}
|
|
}
|
|
|
|
@Injectable()
|
|
export class CustomJsonParser implements JsonParser {
|
|
parse(text: string): any {
|
|
return JSON.parse(text, dateReviver);
|
|
}
|
|
}
|
|
|
|
function dateReviver(key: string, value: any) {
|
|
if (typeof value !== 'string') {
|
|
return value;
|
|
}
|
|
const match = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(value);
|
|
if (!match) {
|
|
return value;
|
|
}
|
|
return new Date(+match[1], +match[2] - 1, +match[3]);
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/http-interceptors/ensure-https-interceptor.ts]" value="import { Injectable } from '@angular/core';
|
|
import {
|
|
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
|
|
} from '@angular/common/http';
|
|
|
|
import { Observable } from 'rxjs';
|
|
|
|
@Injectable()
|
|
export class EnsureHttpsInterceptor implements HttpInterceptor {
|
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
|
// clone request and replace 'http://' with 'https://' at the same time
|
|
const secureReq = req.clone({
|
|
url: req.url.replace('http://', 'https://')
|
|
});
|
|
// send the cloned, "secure" request to the next handler.
|
|
return next.handle(secureReq);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/http-interceptors/index.ts]" value="/* "Barrel" of Http Interceptors */
|
|
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
|
|
|
import { AuthInterceptor } from './auth-interceptor';
|
|
import { CachingInterceptor } from './caching-interceptor';
|
|
import { CustomJsonInterceptor , CustomJsonParser, JsonParser} from './custom-json-interceptor';
|
|
import { EnsureHttpsInterceptor } from './ensure-https-interceptor';
|
|
import { LoggingInterceptor } from './logging-interceptor';
|
|
import { NoopInterceptor } from './noop-interceptor';
|
|
import { TrimNameInterceptor } from './trim-name-interceptor';
|
|
import { UploadInterceptor } from './upload-interceptor';
|
|
|
|
|
|
/** Http interceptor providers in outside-in order */
|
|
export const httpInterceptorProviders = [
|
|
{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },
|
|
{ provide: HTTP_INTERCEPTORS, useClass: CustomJsonInterceptor, multi: true },
|
|
{ provide: JsonParser, useClass: CustomJsonParser },
|
|
|
|
{ provide: HTTP_INTERCEPTORS, useClass: EnsureHttpsInterceptor, multi: true },
|
|
{ provide: HTTP_INTERCEPTORS, useClass: TrimNameInterceptor, multi: true },
|
|
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
|
{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
|
|
{ provide: HTTP_INTERCEPTORS, useClass: UploadInterceptor, multi: true },
|
|
{ provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true },
|
|
|
|
];
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/http-interceptors/logging-interceptor.ts]" value="import { Injectable } from '@angular/core';
|
|
import {
|
|
HttpEvent, HttpInterceptor, HttpHandler,
|
|
HttpRequest, HttpResponse
|
|
} from '@angular/common/http';
|
|
|
|
import { finalize, tap } from 'rxjs/operators';
|
|
import { MessageService } from '../message.service';
|
|
|
|
@Injectable()
|
|
export class LoggingInterceptor implements HttpInterceptor {
|
|
constructor(private messenger: MessageService) {}
|
|
|
|
intercept(req: HttpRequest<any>, next: HttpHandler) {
|
|
const started = Date.now();
|
|
let ok: string;
|
|
|
|
// extend server response observable with logging
|
|
return next.handle(req)
|
|
.pipe(
|
|
tap(
|
|
// Succeeds when there is a response; ignore other events
|
|
event => ok = event instanceof HttpResponse ? 'succeeded' : '',
|
|
// Operation failed; error is an HttpErrorResponse
|
|
error => ok = 'failed'
|
|
),
|
|
// Log when response observable either completes or errors
|
|
finalize(() => {
|
|
const elapsed = Date.now() - started;
|
|
const msg = `${req.method} "${req.urlWithParams}"
|
|
${ok} in ${elapsed} ms.`;
|
|
this.messenger.add(msg);
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/http-interceptors/noop-interceptor.ts]" value="import { Injectable } from '@angular/core';
|
|
import {
|
|
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
|
|
} from '@angular/common/http';
|
|
|
|
import { Observable } from 'rxjs';
|
|
|
|
/** Pass untouched request through to the next request handler. */
|
|
@Injectable()
|
|
export class NoopInterceptor implements HttpInterceptor {
|
|
|
|
intercept(req: HttpRequest<any>, next: HttpHandler):
|
|
Observable<HttpEvent<any>> {
|
|
return next.handle(req);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/http-interceptors/trim-name-interceptor.ts]" value="import { Injectable } from '@angular/core';
|
|
import {
|
|
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
|
|
} from '@angular/common/http';
|
|
|
|
import { Observable } from 'rxjs';
|
|
|
|
@Injectable()
|
|
export class TrimNameInterceptor implements HttpInterceptor {
|
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
|
const body = req.body;
|
|
if (!body || !body.name ) {
|
|
return next.handle(req);
|
|
}
|
|
// copy the body and trim whitespace from the name property
|
|
const newBody = { ...body, name: body.name.trim() };
|
|
// clone request and set its body
|
|
const newReq = req.clone({ body: newBody });
|
|
// send the cloned request to the next handler.
|
|
return next.handle(newReq);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/http-interceptors/upload-interceptor.ts]" value="import { Injectable } from '@angular/core';
|
|
import {
|
|
HttpEvent, HttpInterceptor, HttpHandler,
|
|
HttpRequest, HttpResponse,
|
|
HttpEventType, HttpProgressEvent
|
|
} from '@angular/common/http';
|
|
|
|
import { Observable } from 'rxjs';
|
|
|
|
/** Simulate server replying to file upload request */
|
|
@Injectable()
|
|
export class UploadInterceptor implements HttpInterceptor {
|
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
|
if (req.url.indexOf('/upload/file') === -1) {
|
|
return next.handle(req);
|
|
}
|
|
const delay = 300; // TODO: inject delay?
|
|
return createUploadEvents(delay);
|
|
}
|
|
}
|
|
|
|
/** Create simulation of upload event stream */
|
|
function createUploadEvents(delay: number) {
|
|
// Simulate XHR behavior which would provide this information in a ProgressEvent
|
|
const chunks = 5;
|
|
const total = 12345678;
|
|
const chunkSize = Math.ceil(total / chunks);
|
|
|
|
return new Observable<HttpEvent<any>>(observer => {
|
|
// notify the event stream that the request was sent.
|
|
observer.next({type: HttpEventType.Sent});
|
|
|
|
uploadLoop(0);
|
|
|
|
function uploadLoop(loaded: number) {
|
|
// N.B.: Cannot use setInterval or rxjs delay (which uses setInterval)
|
|
// because e2e test won't complete. A zone thing?
|
|
// Use setTimeout and tail recursion instead.
|
|
setTimeout(() => {
|
|
loaded += chunkSize;
|
|
|
|
if (loaded >= total) {
|
|
const doneResponse = new HttpResponse({
|
|
status: 201, // OK but no body;
|
|
});
|
|
observer.next(doneResponse);
|
|
observer.complete();
|
|
return;
|
|
}
|
|
|
|
const progressEvent: HttpProgressEvent = {
|
|
type: HttpEventType.UploadProgress,
|
|
loaded,
|
|
total
|
|
};
|
|
observer.next(progressEvent);
|
|
uploadLoop(loaded);
|
|
}, delay);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/in-memory-data.service.ts]" value="import { InMemoryDbService } from 'angular-in-memory-web-api';
|
|
|
|
export class InMemoryDataService implements InMemoryDbService {
|
|
createDb() {
|
|
const heroes = [
|
|
{ id: 11, name: 'Dr Nice' },
|
|
{ id: 12, name: 'Narco' },
|
|
{ id: 13, name: 'Bombasto' },
|
|
{ id: 14, name: 'Celeritas' },
|
|
];
|
|
return {heroes};
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/message.service.ts]" value="import { Injectable } from '@angular/core';
|
|
|
|
@Injectable()
|
|
export class MessageService {
|
|
messages: string[] = [];
|
|
|
|
add(message: string) {
|
|
this.messages.push(message);
|
|
}
|
|
|
|
clear() {
|
|
this.messages = [];
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/messages/messages.component.ts]" value="import { Component } from '@angular/core';
|
|
import { MessageService } from '../message.service';
|
|
|
|
@Component({
|
|
selector: 'app-messages',
|
|
templateUrl: './messages.component.html'
|
|
})
|
|
export class MessagesComponent {
|
|
constructor(public messageService: MessageService) {}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/package-search/package-search.component.ts]" value="import { Component, OnInit } from '@angular/core';
|
|
|
|
import { Observable, Subject } from 'rxjs';
|
|
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
|
|
|
import { NpmPackageInfo, PackageSearchService } from './package-search.service';
|
|
|
|
@Component({
|
|
selector: 'app-package-search',
|
|
templateUrl: './package-search.component.html',
|
|
providers: [ PackageSearchService ]
|
|
})
|
|
export class PackageSearchComponent implements OnInit {
|
|
withRefresh = false;
|
|
packages$: Observable<NpmPackageInfo[]>;
|
|
private searchText$ = new Subject<string>();
|
|
|
|
search(packageName: string) {
|
|
this.searchText$.next(packageName);
|
|
}
|
|
|
|
ngOnInit() {
|
|
this.packages$ = this.searchText$.pipe(
|
|
debounceTime(500),
|
|
distinctUntilChanged(),
|
|
switchMap(packageName =>
|
|
this.searchService.search(packageName, this.withRefresh))
|
|
);
|
|
}
|
|
|
|
constructor(private searchService: PackageSearchService) { }
|
|
|
|
|
|
toggleRefresh() { this.withRefresh = ! this.withRefresh; }
|
|
|
|
getValue(target: EventTarget): string {
|
|
return (target as HTMLInputElement).value;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/package-search/package-search.service.ts]" value="import { Injectable } from '@angular/core';
|
|
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
|
|
|
import { Observable, of } from 'rxjs';
|
|
import { catchError, map } from 'rxjs/operators';
|
|
|
|
import { HttpErrorHandler, HandleError } from '../http-error-handler.service';
|
|
|
|
export interface NpmPackageInfo {
|
|
name: string;
|
|
version: string;
|
|
description: string;
|
|
}
|
|
|
|
export const searchUrl = 'https://npmsearch.com/query';
|
|
|
|
const httpOptions = {
|
|
headers: new HttpHeaders({
|
|
'x-refresh': 'true'
|
|
})
|
|
};
|
|
|
|
function createHttpOptions(packageName: string, refresh = false) {
|
|
// npm package name search api
|
|
// e.g., http://npmsearch.com/query?q=dom'
|
|
const params = new HttpParams({ fromObject: { q: packageName } });
|
|
const headerMap = refresh ? {'x-refresh': 'true'} : {};
|
|
const headers = new HttpHeaders(headerMap) ;
|
|
return { headers, params };
|
|
}
|
|
|
|
@Injectable()
|
|
export class PackageSearchService {
|
|
private handleError: HandleError;
|
|
|
|
constructor(
|
|
private http: HttpClient,
|
|
httpErrorHandler: HttpErrorHandler) {
|
|
this.handleError = httpErrorHandler.createHandleError('HeroesService');
|
|
}
|
|
|
|
search(packageName: string, refresh = false): Observable<NpmPackageInfo[]> {
|
|
// clear if no pkg name
|
|
if (!packageName.trim()) { return of([]); }
|
|
|
|
const options = createHttpOptions(packageName, refresh);
|
|
|
|
// TODO: Add error handling
|
|
return this.http.get(searchUrl, options).pipe(
|
|
map((data: any) => {
|
|
return data.results.map((entry: any) => ({
|
|
name: entry.name[0],
|
|
version: entry.version[0],
|
|
description: entry.description[0]
|
|
} as NpmPackageInfo )
|
|
);
|
|
}),
|
|
catchError(this.handleError('search', []))
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/request-cache.service.ts]" value="import { Injectable } from '@angular/core';
|
|
import { HttpRequest, HttpResponse } from '@angular/common/http';
|
|
|
|
import { MessageService } from './message.service';
|
|
|
|
export interface RequestCacheEntry {
|
|
url: string;
|
|
response: HttpResponse<any>;
|
|
lastRead: number;
|
|
}
|
|
|
|
export abstract class RequestCache {
|
|
abstract get(req: HttpRequest<any>): HttpResponse<any> | undefined;
|
|
abstract put(req: HttpRequest<any>, response: HttpResponse<any>): void;
|
|
}
|
|
|
|
const maxAge = 30000; // maximum cache age (ms)
|
|
|
|
@Injectable()
|
|
export class RequestCacheWithMap implements RequestCache {
|
|
|
|
cache = new Map<string, RequestCacheEntry>();
|
|
|
|
constructor(private messenger: MessageService) { }
|
|
|
|
get(req: HttpRequest<any>): HttpResponse<any> | undefined {
|
|
const url = req.urlWithParams;
|
|
const cached = this.cache.get(url);
|
|
|
|
if (!cached) {
|
|
return undefined;
|
|
}
|
|
|
|
const isExpired = cached.lastRead < (Date.now() - maxAge);
|
|
const expired = isExpired ? 'expired ' : '';
|
|
this.messenger.add(
|
|
`Found ${expired}cached response for "${url}".`);
|
|
return isExpired ? undefined : cached.response;
|
|
}
|
|
|
|
put(req: HttpRequest<any>, response: HttpResponse<any>): void {
|
|
const url = req.urlWithParams;
|
|
this.messenger.add(`Caching response from "${url}".`);
|
|
|
|
const newEntry = { url, response, lastRead: Date.now() };
|
|
this.cache.set(url, newEntry);
|
|
|
|
// remove expired cache entries
|
|
const expired = Date.now() - maxAge;
|
|
this.cache.forEach(entry => {
|
|
if (entry.lastRead < expired) {
|
|
this.cache.delete(entry.url);
|
|
}
|
|
});
|
|
|
|
this.messenger.add(`Request cache size: ${this.cache.size}.`);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/uploader/uploader.component.ts]" value="import { Component } from '@angular/core';
|
|
import { UploaderService } from './uploader.service';
|
|
|
|
@Component({
|
|
selector: 'app-uploader',
|
|
templateUrl: './uploader.component.html',
|
|
providers: [ UploaderService ]
|
|
})
|
|
export class UploaderComponent {
|
|
message: string;
|
|
|
|
constructor(private uploaderService: UploaderService) {}
|
|
|
|
onPicked(input: HTMLInputElement) {
|
|
const file = input.files[0];
|
|
if (file) {
|
|
this.uploaderService.upload(file).subscribe(
|
|
msg => {
|
|
input.value = null;
|
|
this.message = msg;
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/uploader/uploader.service.ts]" value="import { Injectable } from '@angular/core';
|
|
import {
|
|
HttpClient, HttpEvent, HttpEventType, HttpProgressEvent,
|
|
HttpRequest, HttpResponse, HttpErrorResponse
|
|
} from '@angular/common/http';
|
|
|
|
import { of } from 'rxjs';
|
|
import { catchError, last, map, tap } from 'rxjs/operators';
|
|
|
|
import { MessageService } from '../message.service';
|
|
|
|
@Injectable()
|
|
export class UploaderService {
|
|
constructor(
|
|
private http: HttpClient,
|
|
private messenger: MessageService) {}
|
|
|
|
// If uploading multiple files, change to:
|
|
// upload(files: FileList) {
|
|
// const formData = new FormData();
|
|
// files.forEach(f => formData.append(f.name, f));
|
|
// new HttpRequest('POST', '/upload/file', formData, {reportProgress: true});
|
|
// ...
|
|
// }
|
|
|
|
upload(file: File) {
|
|
if (!file) { return of<string>(); }
|
|
|
|
// COULD HAVE WRITTEN:
|
|
// return this.http.post('/upload/file', file, {
|
|
// reportProgress: true,
|
|
// observe: 'events'
|
|
// }).pipe(
|
|
|
|
// Create the request object that POSTs the file to an upload endpoint.
|
|
// The `reportProgress` option tells HttpClient to listen and return
|
|
// XHR progress events.
|
|
const req = new HttpRequest('POST', '/upload/file', file, {
|
|
reportProgress: true
|
|
});
|
|
|
|
// The `HttpClient.request` API produces a raw event stream
|
|
// which includes start (sent), progress, and response events.
|
|
return this.http.request(req).pipe(
|
|
map(event => this.getEventMessage(event, file)),
|
|
tap(message => this.showProgress(message)),
|
|
last(), // return last (completed) message to caller
|
|
catchError(this.handleError(file))
|
|
);
|
|
}
|
|
|
|
/** Return distinct message for sent, upload progress, & response events */
|
|
private getEventMessage(event: HttpEvent<any>, file: File) {
|
|
switch (event.type) {
|
|
case HttpEventType.Sent:
|
|
return `Uploading file "${file.name}" of size ${file.size}.`;
|
|
|
|
case HttpEventType.UploadProgress:
|
|
// Compute and show the % done:
|
|
const percentDone = Math.round(100 * event.loaded / event.total);
|
|
return `File "${file.name}" is ${percentDone}% uploaded.`;
|
|
|
|
case HttpEventType.Response:
|
|
return `File "${file.name}" was completely uploaded!`;
|
|
|
|
default:
|
|
return `File "${file.name}" surprising upload event: ${event.type}.`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a function that handles Http upload failures.
|
|
* @param file - File object for file being uploaded
|
|
*
|
|
* When no `UploadInterceptor` and no server,
|
|
* you'll end up here in the error handler.
|
|
*/
|
|
private handleError(file: File) {
|
|
const userMessage = `${file.name} upload failed.`;
|
|
|
|
return (error: HttpErrorResponse) => {
|
|
// TODO: send the error to remote logging infrastructure
|
|
console.error(error); // log to console instead
|
|
|
|
const message = (error.error instanceof Error) ?
|
|
error.error.message :
|
|
`server returned code ${error.status} with body "${error.error}"`;
|
|
|
|
this.messenger.add(`${userMessage} ${message}`);
|
|
|
|
// Let app keep running but indicate failure.
|
|
return of(userMessage);
|
|
};
|
|
}
|
|
|
|
private showProgress(message: string) {
|
|
this.messenger.add(message);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/environments/environment.prod.ts]" value="export const environment = {
|
|
production: true
|
|
};
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/environments/environment.ts]" value="// This file can be replaced during build by using the `fileReplacements` array.
|
|
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
|
// The list of file replacements can be found in `angular.json`.
|
|
|
|
export const environment = {
|
|
production: false
|
|
};
|
|
|
|
/*
|
|
* For easier debugging in development mode, you can import the following file
|
|
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
|
*
|
|
* This import should be commented out in production mode because it will have a negative impact
|
|
* on performance if an error is thrown.
|
|
*/
|
|
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/main.ts]" value="import { enableProdMode } from '@angular/core';
|
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
|
|
|
import { AppModule } from './app/app.module';
|
|
import { environment } from './environments/environment';
|
|
|
|
if (environment.production) {
|
|
enableProdMode();
|
|
}
|
|
|
|
platformBrowserDynamic().bootstrapModule(AppModule);
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/polyfills.ts]" value="/**
|
|
* This file includes polyfills needed by Angular and is loaded before the app.
|
|
* You can add your own extra polyfills to this file.
|
|
*
|
|
* This file is divided into 2 sections:
|
|
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
|
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
|
* file.
|
|
*
|
|
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
|
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
|
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
|
*
|
|
* Learn more in https://angular.io/guide/browser-support
|
|
*/
|
|
|
|
/***************************************************************************************************
|
|
* BROWSER POLYFILLS
|
|
*/
|
|
|
|
/** IE11 requires the following for NgClass support on SVG elements */
|
|
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
|
|
|
/**
|
|
* Web Animations `@angular/platform-browser/animations`
|
|
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
|
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
|
*/
|
|
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
|
|
|
/**
|
|
* By default, zone.js will patch all possible macroTask and DomEvents
|
|
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
|
* because those flags need to be set before `zone.js` being loaded, and webpack
|
|
* will put import in the top of bundle, so user need to create a separate file
|
|
* in this directory (for example: zone-flags.ts), and put the following flags
|
|
* into that file, and then add the following code before importing zone.js.
|
|
* import './zone-flags';
|
|
*
|
|
* The flags allowed in zone-flags.ts are listed here.
|
|
*
|
|
* The following flags will work for all browsers.
|
|
*
|
|
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch
|
|
* requestAnimationFrame
|
|
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
|
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch
|
|
* specified eventNames
|
|
*
|
|
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
|
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
|
*
|
|
* (window as any).__Zone_enable_cross_context_check = true;
|
|
*
|
|
*/
|
|
|
|
/***************************************************************************************************
|
|
* Zone JS is required by default for Angular itself.
|
|
*/
|
|
import 'zone.js'; // Included with Angular CLI.
|
|
|
|
/***************************************************************************************************
|
|
* APPLICATION IMPORTS
|
|
*/
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/heroes/heroes.component.css]" value="/* HeroesComponent's private CSS styles */
|
|
.heroes {
|
|
margin: 0 0 2em 0;
|
|
list-style-type: none;
|
|
padding: 0;
|
|
width: 15em;
|
|
}
|
|
.heroes li {
|
|
position: relative;
|
|
cursor: pointer;
|
|
background-color: #EEE;
|
|
margin: .5em;
|
|
padding: .3em 0;
|
|
height: 1.6em;
|
|
border-radius: 4px;
|
|
width: 19em;
|
|
}
|
|
|
|
.heroes li:hover {
|
|
color: #607D8B;
|
|
background-color: #DDD;
|
|
left: .1em;
|
|
}
|
|
|
|
.heroes a {
|
|
color: #888;
|
|
text-decoration: none;
|
|
position: relative;
|
|
display: block;
|
|
width: 250px;
|
|
}
|
|
|
|
.heroes a:hover {
|
|
color:#607D8B;
|
|
}
|
|
|
|
.heroes .badge {
|
|
display: inline-block;
|
|
font-size: small;
|
|
color: white;
|
|
padding: 0.8em 0.7em 0 0.7em;
|
|
background-color: #607D8B;
|
|
line-height: 1em;
|
|
position: relative;
|
|
left: -1px;
|
|
top: -4px;
|
|
height: 1.8em;
|
|
min-width: 16px;
|
|
text-align: right;
|
|
margin-right: .8em;
|
|
border-radius: 4px 0 0 4px;
|
|
}
|
|
|
|
.button {
|
|
background-color: #eee;
|
|
border: none;
|
|
padding: 5px 10px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
|
|
button:hover {
|
|
background-color: #cfd8dc;
|
|
}
|
|
|
|
button.delete {
|
|
position: relative;
|
|
left: 24em;
|
|
top: -32px;
|
|
background-color: gray !important;
|
|
color: white;
|
|
display: inherit;
|
|
padding: 5px 8px;
|
|
width: 2em;
|
|
}
|
|
|
|
input {
|
|
font-size: 100%;
|
|
margin-bottom: 2px;
|
|
width: 11em;
|
|
}
|
|
|
|
.heroes input {
|
|
position: relative;
|
|
top: -3px;
|
|
width: 12em;
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/styles.css]" value="/* Global Styles */
|
|
* {
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
}
|
|
h1 {
|
|
color: #264D73;
|
|
font-size: 2.5rem;
|
|
}
|
|
h2, h3 {
|
|
color: #444;
|
|
font-weight: lighter;
|
|
}
|
|
h3 {
|
|
font-size: 1.3rem;
|
|
}
|
|
body {
|
|
padding: .5rem;
|
|
max-width: 1000px;
|
|
margin: auto;
|
|
}
|
|
@media (min-width: 600px) {
|
|
body {
|
|
padding: 2rem;
|
|
}
|
|
}
|
|
body, input[text] {
|
|
color: #333;
|
|
font-family: Cambria, Georgia, serif;
|
|
}
|
|
a {
|
|
cursor: pointer;
|
|
}
|
|
button {
|
|
background-color: #eee;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
color: black;
|
|
font-size: 1.2rem;
|
|
padding: 1rem;
|
|
margin-right: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
button:hover {
|
|
background-color: black;
|
|
color: white;
|
|
}
|
|
button:disabled {
|
|
background-color: #eee;
|
|
color: #aaa;
|
|
cursor: auto;
|
|
}
|
|
|
|
/* Navigation link styles */
|
|
nav a {
|
|
padding: 5px 10px;
|
|
text-decoration: none;
|
|
margin-right: 10px;
|
|
margin-top: 10px;
|
|
display: inline-block;
|
|
background-color: #e8e8e8;
|
|
color: #3d3d3d;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
nav a:hover {
|
|
color: white;
|
|
background-color: #42545C;
|
|
}
|
|
nav a.active {
|
|
background-color: black;
|
|
color: white;
|
|
}
|
|
hr {
|
|
margin: 1.5rem 0;
|
|
}
|
|
input[type="text"] {
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
padding: .5rem;
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/test.css]" value="@import "~jasmine-core/lib/jasmine-core/jasmine.css"
|
|
|
|
|
|
/*
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
*/"><input type="hidden" name="files[src/app/app.component.html]" value="<h1>HTTP Sample</h1>
|
|
<div>
|
|
<input type="checkbox" id="heroes" [checked]="showHeroes" (click)="toggleHeroes()">
|
|
<label for="heroes">Heroes</label>
|
|
|
|
<input type="checkbox" id="config" [checked]="showConfig" (click)="toggleConfig()">
|
|
<label for="config">Config</label>
|
|
|
|
<input type="checkbox" id="downloader" [checked]="showDownloader" (click)="toggleDownloader()">
|
|
<label for="downloader">Downloader</label>
|
|
|
|
<input type="checkbox" id="uploader" [checked]="showUploader" (click)="toggleUploader()">
|
|
<label for="uploader">Uploader</label>
|
|
|
|
<input type="checkbox" id="search" [checked]="showSearch" (click)="toggleSearch()">
|
|
<label for="search">Search</label>
|
|
</div>
|
|
|
|
<app-heroes *ngIf="showHeroes"></app-heroes>
|
|
<app-messages></app-messages>
|
|
<app-config *ngIf="showConfig"></app-config>
|
|
<app-downloader *ngIf="showDownloader"></app-downloader>
|
|
<app-uploader *ngIf="showUploader"></app-uploader>
|
|
<app-package-search *ngIf="showSearch"></app-package-search>
|
|
|
|
|
|
<!--
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
-->"><input type="hidden" name="files[src/app/config/config.component.html]" value="<h3>Get configuration from JSON file</h3>
|
|
<div>
|
|
<button (click)="clear(); showConfig()">get</button>
|
|
<button (click)="clear(); showConfigResponse()">getResponse</button>
|
|
<button (click)="clear()">clear</button>
|
|
<button (click)="clear(); makeError()">error</button>
|
|
<span *ngIf="config">
|
|
<p>Heroes API URL is "{{config.heroesUrl}}"</p>
|
|
<p>Textfile URL is "{{config.textfile}}"</p>
|
|
<p>Date is "{{config.date.toDateString()}}" ({{getType(config.date)}})</p>
|
|
<div *ngIf="headers">
|
|
Response headers:
|
|
<ul>
|
|
<li *ngFor="let header of headers">{{header}}</li>
|
|
</ul>
|
|
</div>
|
|
</span>
|
|
</div>
|
|
<p *ngIf="error" class="error">{{error | json}}</p>
|
|
|
|
|
|
<!--
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
-->"><input type="hidden" name="files[src/app/downloader/downloader.component.html]" value="<h3>Download the textfile</h3>
|
|
<button (click)="download()">Download</button>
|
|
<button (click)="clear()">clear</button>
|
|
<p *ngIf="contents">Contents: "{{contents}}"</p>
|
|
|
|
|
|
<!--
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
-->"><input type="hidden" name="files[src/app/heroes/heroes.component.html]" value="<h3>Heroes</h3>
|
|
<div>
|
|
<label>Hero name:
|
|
<input #heroName />
|
|
</label>
|
|
<!-- (click) passes input value to add() and then clears the input -->
|
|
<button (click)="add(heroName.value); heroName.value=''">
|
|
add
|
|
</button>
|
|
<button (click)="search(heroName.value)">
|
|
search
|
|
</button>
|
|
</div>
|
|
|
|
<ul class="heroes">
|
|
<li *ngFor="let hero of heroes">
|
|
<a (click)="edit(hero)">
|
|
<span class="badge">{{ hero.id || -1 }}</span>
|
|
<span *ngIf="hero!==editHero">{{hero.name}}</span>
|
|
<input *ngIf="hero===editHero" [(ngModel)]="hero.name"
|
|
(blur)="update()" (keyup.enter)="update()">
|
|
</a>
|
|
<button class="delete" title="delete hero"
|
|
(click)="delete(hero)">x</button>
|
|
</li>
|
|
</ul>
|
|
|
|
|
|
<!--
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
-->"><input type="hidden" name="files[src/app/messages/messages.component.html]" value="<div *ngIf="messageService.messages.length">
|
|
<h3>Messages</h3>
|
|
<button class="clear" (click)="messageService.clear()">clear</button>
|
|
<br>
|
|
<ol>
|
|
<li *ngFor='let message of messageService.messages'> {{message}} </li>
|
|
</ol>
|
|
</div>
|
|
|
|
|
|
<!--
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
-->"><input type="hidden" name="files[src/app/package-search/package-search.component.html]" value="<h3>Search Npm Packages</h3>
|
|
<p><i>Searches when typing stops. Caches for 30 seconds.</i></p>
|
|
<input (keyup)="search(getValue($event.target))" id="name" placeholder="Search"/>
|
|
<input type="checkbox" id="refresh" [checked]="withRefresh" (click)="toggleRefresh()">
|
|
<label for="refresh">with refresh</label>
|
|
|
|
<ul>
|
|
<li *ngFor="let package of packages$ | async">
|
|
<b>{{package.name}} v.{{package.version}}</b> -
|
|
<i>{{package.description}}</i>
|
|
</li>
|
|
</ul>
|
|
|
|
|
|
<!--
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
-->"><input type="hidden" name="files[src/app/uploader/uploader.component.html]" value="<h3>Upload file</h3>
|
|
<form enctype="multipart/form-data" method="post">
|
|
<div>
|
|
<label for="picked">Choose file to upload</label>
|
|
<div>
|
|
<input type="file" id="picked" #picked
|
|
(click)="message=''"
|
|
(change)="onPicked(picked)">
|
|
</div>
|
|
</div>
|
|
<p *ngIf="message">{{message}}</p>
|
|
</form>
|
|
|
|
|
|
<!--
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
-->"><input type="hidden" name="files[src/index.html]" value="<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>HttpClient Demo</title>
|
|
<base href="/">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
</head>
|
|
<body>
|
|
<app-root></app-root>
|
|
</body>
|
|
</html>
|
|
|
|
|
|
<!--
|
|
Copyright Google LLC. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at https://angular.io/license
|
|
-->"><input type="hidden" name="files[angular.json]" value="{
|
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
|
"version": 1,
|
|
"newProjectRoot": "projects",
|
|
"projects": {
|
|
"angular.io-example": {
|
|
"projectType": "application",
|
|
"schematics": {
|
|
"@schematics/angular:application": {
|
|
"strict": true
|
|
}
|
|
},
|
|
"root": "",
|
|
"sourceRoot": "src",
|
|
"prefix": "app",
|
|
"architect": {
|
|
"build": {
|
|
"builder": "@angular-devkit/build-angular:browser",
|
|
"options": {
|
|
"outputPath": "dist",
|
|
"index": "src/index.html",
|
|
"main": "src/main.ts",
|
|
"polyfills": "src/polyfills.ts",
|
|
"tsConfig": "tsconfig.app.json",
|
|
"aot": true,
|
|
"assets": [
|
|
"src/favicon.ico",
|
|
"src/assets"
|
|
],
|
|
"styles": [
|
|
"src/styles.css",
|
|
"src/test.css"
|
|
],
|
|
"scripts": []
|
|
},
|
|
"configurations": {
|
|
"production": {
|
|
"fileReplacements": [
|
|
{
|
|
"replace": "src/environments/environment.ts",
|
|
"with": "src/environments/environment.prod.ts"
|
|
}
|
|
],
|
|
"optimization": true,
|
|
"outputHashing": "all",
|
|
"sourceMap": false,
|
|
"namedChunks": false,
|
|
"extractLicenses": true,
|
|
"vendorChunk": false,
|
|
"buildOptimizer": true,
|
|
"budgets": [
|
|
{
|
|
"type": "initial",
|
|
"maximumWarning": "500kb",
|
|
"maximumError": "1mb"
|
|
},
|
|
{
|
|
"type": "anyComponentStyle",
|
|
"maximumWarning": "2kb",
|
|
"maximumError": "4kb"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"serve": {
|
|
"builder": "@angular-devkit/build-angular:dev-server",
|
|
"options": {
|
|
"browserTarget": "angular.io-example:build"
|
|
},
|
|
"configurations": {
|
|
"production": {
|
|
"browserTarget": "angular.io-example:build:production"
|
|
}
|
|
}
|
|
},
|
|
"extract-i18n": {
|
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
|
"options": {
|
|
"browserTarget": "angular.io-example:build"
|
|
}
|
|
},
|
|
"test": {
|
|
"builder": "@angular-devkit/build-angular:karma",
|
|
"options": {
|
|
"main": "src/test.ts",
|
|
"polyfills": "src/polyfills.ts",
|
|
"tsConfig": "tsconfig.spec.json",
|
|
"karmaConfig": "karma.conf.js",
|
|
"assets": [
|
|
"src/favicon.ico",
|
|
"src/assets"
|
|
],
|
|
"styles": [
|
|
"src/styles.css"
|
|
],
|
|
"scripts": []
|
|
}
|
|
},
|
|
"lint": {
|
|
"builder": "@angular-devkit/build-angular:tslint",
|
|
"options": {
|
|
"tsConfig": [
|
|
"tsconfig.app.json",
|
|
"tsconfig.spec.json",
|
|
"e2e/tsconfig.json"
|
|
],
|
|
"exclude": [
|
|
"**/node_modules/**"
|
|
]
|
|
}
|
|
},
|
|
"e2e": {
|
|
"builder": "@angular-devkit/build-angular:protractor",
|
|
"options": {
|
|
"protractorConfig": "e2e/protractor.conf.js",
|
|
"devServerTarget": "angular.io-example:serve"
|
|
},
|
|
"configurations": {
|
|
"production": {
|
|
"devServerTarget": "angular.io-example:serve:production"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"defaultProject": "angular.io-example"
|
|
}
|
|
"><input type="hidden" name="files[src/assets/config.json]" value="{
|
|
"heroesUrl": "api/heroes",
|
|
"textfile": "assets/textfile.txt",
|
|
"date": "2020-01-29"
|
|
}
|
|
"><input type="hidden" name="files[tsconfig.json]" value="{
|
|
"compileOnSave": false,
|
|
"compilerOptions": {
|
|
"baseUrl": "./",
|
|
"outDir": "./dist/out-tsc",
|
|
"forceConsistentCasingInFileNames": true,
|
|
"noImplicitReturns": true,
|
|
"noFallthroughCasesInSwitch": true,
|
|
"sourceMap": true,
|
|
"declaration": false,
|
|
"downlevelIteration": true,
|
|
"experimentalDecorators": true,
|
|
"moduleResolution": "node",
|
|
"importHelpers": true,
|
|
"target": "es2015",
|
|
"module": "es2020",
|
|
"lib": [
|
|
"es2018",
|
|
"dom"
|
|
]
|
|
},
|
|
"angularCompilerOptions": {
|
|
"strictInjectionParameters": true,
|
|
"strictInputAccessModifiers": true,
|
|
"strictTemplates": true,
|
|
"enableIvy": true
|
|
}
|
|
}"><input type="hidden" name="tags[0]" value="angular"><input type="hidden" name="tags[1]" value="example"><input type="hidden" name="tags[2]" value="http"><input type="hidden" name="description" value="Angular Example - Http"><input type="hidden" name="dependencies" value="{"@angular/animations":"~11.0.1","@angular/common":"~11.0.1","@angular/compiler":"~11.0.1","@angular/core":"~11.0.1","@angular/forms":"~11.0.1","@angular/platform-browser":"~11.0.1","@angular/platform-browser-dynamic":"~11.0.1","@angular/router":"~11.0.1","angular-in-memory-web-api":"~0.11.0","rxjs":"~6.6.0","tslib":"^2.0.0","zone.js":"~0.11.4","jasmine-core":"~3.6.0","jasmine-marbles":"~0.6.0"}"></form>
|
|
<script>
|
|
var embedded = 'ctl=1';
|
|
var isEmbedded = window.location.search.indexOf(embedded) > -1;
|
|
|
|
if (isEmbedded) {
|
|
var form = document.getElementById('mainForm');
|
|
var action = form.action;
|
|
var actionHasParams = action.indexOf('?') > -1;
|
|
var symbol = actionHasParams ? '&' : '?'
|
|
form.action = form.action + symbol + embedded;
|
|
}
|
|
document.getElementById("mainForm").submit();
|
|
</script>
|
|
</body></html> |