I want to display a loader for every fetch done by my app, preventing the user from submiting the same request multiple times. For this I changed the HttpClient configuration to intercept the requests and responses during configuration in main.ts
let httpClient = new HttpClient();
httpClient.configure(config => {
config.withInterceptor({
request(request) {
// displayLoader();
return request;
},
response(response) {
// hideLoader();
return response;
},
});
});
If it was plain js I would do something like document.body.appendChild... but Aurelia doens't let me do that.
Is there a way to display a custom view dynamically, without changing the route or having to insert a new loading view on every component that is doing a request?
Also if you have a better approach for this problem I'm open to suggestions.
Here's an option that might work for you. In your main.js, have something that looks like this:
import {HttpClient} from 'aurelia-fetch-client';
import {EventAggregator} from 'aurelia-event-aggregator';
export function configure(aurelia) {
const container = aurelia.container;
const httpClient = container.get(HttpClient);
const ea = container.get(EventAggregator);
httpClient.configure(config => {
config.withInterceptor({
request(request) {
ea.publish('http-request', request);
return request;
},
response(response) {
ea.publish('http-response', response);
return response;
}
});
});
aurelia.use
.standardConfiguration()
.developmentLogging()
.singleton(HttpClient, httpClient);
aurelia.start().then(() => aurelia.setRoot());
}
Then, in app.js or whatever you are using for your app's root page. Have something like this:
import {inject} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
#inject(EventAggregator)
export class App {
displayLoader = false;
constructor(ea) {
this.ea = ea;
ea.subscribe('http-request', () => this.displayLoader = true);
ea.subscribe('http-response', () => this.displayLoader = false );
}
...
Then simply use databinding w/if.bind="displayLoader" or show.bind="displayLoader" in app.html.
Related
I have several components that I need to check if the user logged on/has valid access token
I currently do check this inside a Vue component method using the contents of isLoggedOut function below. I am thinking that I might need to create an external js file and import this js everywhere that I need to check of credentials. So js function will look sthg like below. However this function also resets the cookies in the component. see this.$cookies. I don't think this is possible due to scoping.
So how can I import functions (like from a utility js file) that also changes this objects? Or is there a better way of what avoiding code duplication in Vue/check for log out in multiple components using same code
import axios from "axios";
function isLoggedOut() {
axios.defaults.withCredentials = true;
const isLoggedOut = True;
const path = `/user_authentication/protected`;
axios
.get(path, { withCredentials: true })
.then((response) => {
message = response.data["user"];
isLoggedOut = false;
return isLoggedOut;
})
.catch((error) => {
console.error(error);
this.$cookies.remove("csrf_access_token");
isLoggedOut = true;
return isLoggedOut;
});
}
Create an index.ts file in a folder named utils and export the funtion isLoggedOut.
Pass the Vue app to the function isLoggedOut as a prop and call the vue methods.
import Vue from 'vue'
import axios from "axios";
export function isLoggedOut(app: Vue) {
axios.defaults.withCredentials = true;
const isLoggedOut = True;
const path = `/user_authentication/protected`;
axios
.get(path, { withCredentials: true })
.then((response) => {
message = response.data["user"];
isLoggedOut = false;
return isLoggedOut;
})
.catch((error) => {
console.error(error);
app.$cookies.remove("csrf_access_token");
isLoggedOut = true;
return isLoggedOut;
});
}
Component
import { isLoggedOut } from '~/utils'
export default {
methods: {
logOut() {
// Passing the Vue app
isLoggedOut(this)
}
}
}
I am working on a StencilJS project where I have to use MirageJS to make fake API data.
How to call server before StencilJS application loads.
In react we can call makeServer() in the index.ts file, but in the stencil, we don't have such a file.
How can we call this to start the mirage server, Please can someone suggest the correct way.
Below is my server.ts file
mirage/server.ts
import { createServer, Model } from 'miragejs';
import { auditFactory } from './factories';
import { processCollectionRequest } from './utils';
export const makeServer = async ({ environment = 'development' } = {}) => {
console.log('started server');
return createServer({
environment,
factories: {
people: auditFactory,
},
models: {
people: Model,
},
routes() {
this.namespace = '/api/v1';
this.get('/peoples', function (schema, request) {
let res = processCollectionRequest(schema, request, 'peoples', this);
// remove factory properties not in spec
res.items.forEach(e => ['associatedResourceId', 'associatedResourceName', 'associatedResourceType'].forEach(p => delete e[p]));
return res;
});
},
seeds(server) {
server.createList('audit', 20);
},
});
};
I'm not familiar with MirageJS so I might be off, but can you use globalScript (https://stenciljs.com/docs/config) and then run your Mirage server there?
I'm trying to implement a globally-accessible singular class in an Aurelia project. The purposes are to (a) store singulares/states like current user ID/name/permissions, (b) load and store common data like enum lists and key-value pairs for drop-down lists across the whole app, (c) store commonly-used functions like wrappers for Http-Fetch client, (d) configure and then update i18n locale, (e) global keyboard listener for hotkeys throughout the app. Here's what I have so far:
/src/resources/components/core.js:
import 'fetch';
import { HttpClient, json } from 'aurelia-fetch-client';
import { inject } from 'aurelia-framework';
import { EventAggregator } from 'aurelia-event-aggregator';
import { BindingSignaler } from 'aurelia-templating-resources';
import { I18N } from 'aurelia-i18n';
import * as store from 'store';
#inject(EventAggregator, BindingSignaler, I18N, HttpClient)
export class Core {
constructor(eventAggregator, bindingSignaler, i18n, httpClient) {
// store local handles
this.eventAggregator = eventAggregator;
this.bindingSignaler = bindingSignaler;
this.i18n = i18n;
// initialize singulars
this.UserID = 1;
this.lang = 'es';
this.yr = 78;
this.qtr = 1;
// set up httpClient
httpClient.configure(config => {
config
.withBaseUrl('http://localhost:8080/api/v1');
});
this.httpClient = httpClient;
// listen for Ctrl+S or Ctrl+Enter and publish event
window.addEventListener("keydown", (event) => {
if (event.ctrlKey || event.metaKey) { // Ctrl + ___
if ((event.keyCode == 83) || (event.keyCode == 115) || (event.keyCode == 10) || (event.keyCode == 13)) { // Ctrl+Enter or Ctrl+S
// Save button... publish new event
event.preventDefault();
this.eventAggregator.publish('ewKeyboardShortcutSave', true);
}
if ((event.keyCode == 77) || (event.keyCode == 109)) { // Ctrl+M
// New button... publish new event
event.preventDefault();
this.eventAggregator.publish('ewKeyboardShortcutNew', true);
}
}
});
// load enumData
$.getJSON("../../locales/" + this.lang + "/enum.json", (json) => { this.enum = json; });
this.getTableKeys();
this.getEnumCats();
}
getData(url) {
// Http Fetch Client to retreive data (GET)
return this.httpClient.fetch(url)
.then(response => response.json());
}
postData(url, data, use_method = 'post') {
// Http Fetch Client to send data (POST/PUT/DELETE)
return this.httpClient.fetch(url, {
method: use_method,
body: json(data)
}).then(response => {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
});
}
getTableKeys() {
// retrieve list of table keys from database API
this.getData('/keys').then(response => {
this.keys = response;
});
}
getEnumCats() {
// retrieve list of enum cats from database API
this.getData('/enums').then(response => {
this.cats = response;
});
}
setLang(lang) {
if (lang) {
this.lang = lang;
}
// set i18n locale
this.i18n.setLocale(this.lang);
// load enumData
$.getJSON("../../locales/" + this.lang + "/enum.json", (json) => {
this.enumData = json;
});
// publish new event
this.eventAggregator.publish('ewLang', lang);
this.bindingSignaler.signal('ewLang');
}
}
Here's the /src/resources/index.js for the resources feature:
export function configure(config) {
// value converters
config.globalResources([
'./value-converters/currency-format-value-converter',
'./value-converters/number-format-value-converter',
'./value-converters/date-format-value-converter',
'./value-converters/checkbox-value-converter',
'./value-converters/keys-value-converter',
'./value-converters/enum-value-converter',
'./value-converters/table-key-value-converter'
]);
// custom elements
config.globalResources([
'./elements/enum-list',
'./elements/modal-form'
]);
// common/core components
config.globalResources([
'./components/core'
]);
}
which is in turn activated in my main.js like this:
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.feature('resources')
// .plugin('aurelia-dialog') // not working
.plugin('aurelia-validation')
.plugin('aurelia-i18n', (instance) => {
// register backend plugin
instance.i18next.use(XHR);
instance.setup({
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
lng : 'en',
ns: ['translation'],
defaultNS: 'translation',
attributes : ['t'],
fallbackLng : 'en',
debug : false
});
});
aurelia.start().then(a => a.setRoot());
}
Questions:
It's not working. I get two errors: vendor-bundle.js:3777 Uncaught TypeError: h.load is not a function and Unhandled rejection Error: Load timeout for modules: template-registry-entry!resources/components/core.html,text!resources/components/core.html. Any idea why it's trying to find a core.html when I only need the core.js component?
Is it even possible to globally inject this type of class in a way that my viewmodels don't need to inject it but can still access the properties, or do I still need to inject this file everywhere?
Is the filename core.js and the class name Core acceptable naming conventions? Is the location inside /src/resources/components a good choice? I had to create the components subfolder.
Any other suggestions for better best practices?
Question 1
When you do this:
config.globalResources([
'./components/core'
]);
Aurelia will try to load a pair of view and view-model, respectively core.js and core.html, unless if the component is declared as a "view-model only component". Like this:
import { noView } from 'aurelia-framework';
#noView
#inject(EventAggregator, BindingSignaler, I18N, HttpClient)
export class Core {
}
In the above case Aurelia won't try to load "core.html" because the component is declared with noView.
Question 2
As far as I know, you have to inject or <require> it everywhere, but the latter doesn't apply in your case, so you have to inject. You could some trickery to avoid the injecting but I would not recommend.
Question 3
The file name core.js and the class name Core are not only acceptable but the correct aurelia-way of doing this. However, I don't think that "/resources/components" is a good a location because it's not a component, not even a "resource". I would move this to another folder.
In addition, remove these lines:
config.globalResources([
'./components/core'
]);
Resources were made to be used inside views, which is not you are case.
Question 4
The file core.js seems to be a very import piece of code of your application. I would leave it inside the root folder, next to main.js. (THIS IS MY OPINION)
Also, if you need to set some specific properties in your Core object, you can instantiate it inside the main.js. Something like this:
export function configure(aurelia) {
//...
Core core = new Core(); //<--- put necessary parameters
//some default configuration
aurelia.container.registerInstance(Core, core);
aurelia.start().then(a => a.setRoot());
}
Now, you can inject the core object using the #inject decorator and all classe will have the same instance of Core. More information at http://aurelia.io/hub.html#/doc/article/aurelia/dependency-injection/latest/dependency-injection-basics/1
Hope this helps!
I'm missing something when passing data from a component to another component. I use #Input to pass the data, that i get from an http.get request. The thing is, i get an error while trying to access an attribute from the passed input while the request hasn't been resolved.
//news.ts
import {Component} from 'angular2/core';
import {Http, HTTP_PROVIDERS} from 'angular2/http';
import {Pagination} from './pagination';
#Component({
selector: 'news',
templateUrl: 'app/news.html',
viewProviders: [HTTP_PROVIDERS],
directives: [Pagination]
})
export class News {
news = [];
pagination: Pagination;
constructor(http: Http) {
http.get('http://test.com/data')
.map(res => res.json())
.subscribe(news => this.news = news);
}
}
//pagination.ts
import {Component, Input} from 'angular2/core';
#Component({
selector: 'pagination',
templateUrl: 'app/pagination.html'
})
export class Pagination {
// page: int = 1;
#Input() config;
constructor() {
// this.page = this.config.number;
}
}
//Pagination.html
Page {{config.total}}
config.total generates an error on load. But doing {{config}} seems to work though.
Any ideas ?
Thanks
There are two solutions for this:
You can use Elvis operator in your pagination.html
Page {{config?.total}}
This is from Angular documentation:
The Elvis operator (?) means that the employer field is optional and if undefined, the rest of the expression should be ignored.
Second solution would be to use Async Pipe:
https://angular.io/docs/ts/latest/api/common/AsyncPipe-class.html
In this case you would need to rewrite your code.
Variable decorated with #Input() is not available in the constructor. You have to wait until Angular resolves the binding and access it later in component's lifecycle:
ngOnInit() {
this.page = this.config.number;
}
#Vlado Tesanovic I just tried the second solution because i might need to handle the data in the constructor.
What i did :
//news.ts
constructor(http: Http) {
// http.get('http://lechiffonbleu.com:1337/news/test')
// .map(res => res.json())
// .subscribe(news => this.news = news);
this.news = new Promise((resolve, reject) => {
resolve = http.get('http://lechiffonbleu.com:1337/news/test')
.map(res => res.json())
.subscribe(news => this.news = news);
console.log(this.news);
console.log(resolve);
});
}
Well i can't figure out how to resolve the promise correctly so it doesn't throw an error in the #input in pagination.ts
I'm learning Angular 2 Beta. I wonder how to download the PDF file from the API and display it in my view? I've tried to make a request using the following:
var headers = new Headers();
headers.append('Accept', 'application/pdf');
var options = new ResponseOptions({
headers: headers
});
var response = new Response(options);
this.http.get(this.setUrl(endpoint), response).map(res => res.arrayBuffer()).subscribe(r=>{
console.log(r);
})
Please note that I only use the console.log to see the value of r
But I always get the following exception message:
"arrayBuffer()" method not implemented on Response superclass
Is it because that method isn't ready yet in Angular 2 Beta? Or is there any mistake that I made?
Any help would be appreciated. Thank you very much.
In fact, this feature isn't implemented yet in the HTTP support.
As a workaround, you need to extend the BrowserXhr class of Angular2 as described below to set the responseType to blob on the underlying xhr object:
import {Injectable} from 'angular2/core';
import {BrowserXhr} from 'angular2/http';
#Injectable()
export class CustomBrowserXhr extends BrowserXhr {
constructor() {}
build(): any {
let xhr = super.build();
xhr.responseType = "blob";
return <any>(xhr);
}
}
Then you need to wrap the response payload into a Blob object and use the FileSaver library to open the download dialog:
downloadFile() {
this.http.get(
'https://mapapi.apispark.net/v1/images/Granizo.pdf').subscribe(
(response) => {
var mediaType = 'application/pdf';
var blob = new Blob([response._body], {type: mediaType});
var filename = 'test.pdf';
saveAs(blob, filename);
});
}
The FileSaver library must be included into your HTML file:
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2014-11-29/FileSaver.min.js"></script>
See this plunkr: http://plnkr.co/edit/tfpS9k2YOO1bMgXBky5Y?p=preview
Unfortunately this will set the responseType for all AJAX requests. To be able to set the value of this property, there are more updates to do in the XHRConnection and Http classes.
As references see these links:
Download pdf file using jquery ajax
Receive zip file, angularJs
Edit
After thinking a bit more, I think that you could leverage hierarchical injectors and configure this provider only at the level of the component that executes the download:
#Component({
selector: 'download',
template: '<div (click)="downloadFile() ">Download</div>'
, providers: [
provide(CustomBrowserXhr,
{ useClass: CustomBrowserXhr }
]
})
export class DownloadComponent {
#Input()
filename:string;
constructor(private http:Http) {
}
downloadFile() {
this.http.get(
'https://mapapi.apispark.net/v1/images/'+this.filename).subscribe(
(response) => {
var mediaType = 'application/pdf';
var blob = new Blob([response._body], {type: mediaType});
var filename = 'test.pdf';
saveAs(blob, filename);
});
}
}
This override would only applies for this component (don't forget to remove the corresponding provide when bootstrapping your application). The download component could be used like that:
#Component({
selector: 'somecomponent',
template: `
<download filename="'Granizo.pdf'"></download>
`
, directives: [ DownloadComponent ]
})
So here is how I managed to get it to work.
My situation: I needed to download a PDF from my API endpoint, and save the result as a PDF in the browser.
To support file-saving in all browsers, I used the FileSaver.js module.
I created a component that takes the ID of the file to download as parameter.
The component, , is called like this:
<pdf-downloader no="24234232"></pdf-downloader>
The component itself uses XHR to fetch/save the file with the number given in the no parameter. This way we can circumvent the fact that the Angular2 http module doesn't yet support binary result types.
And now, without further ado, the component code:
import {Component,Input } from 'angular2/core';
import {BrowserXhr} from 'angular2/http';
// Use Filesaver.js to save binary to file
// https://github.com/eligrey/FileSaver.js/
let fileSaver = require('filesaver.js');
#Component({
selector: 'pdf-downloader',
template: `
<button
class="btn btn-secondary-outline btn-sm "
(click)="download()">
<span class="fa fa-download" *ngIf="!pending"></span>
<span class="fa fa-refresh fa-spin" *ngIf="pending"></span>
</button>
`
})
export class PdfDownloader {
#Input() no: any;
public pending:boolean = false;
constructor() {}
public download() {
// Xhr creates new context so we need to create reference to this
let self = this;
// Status flag used in the template.
this.pending = true;
// Create the Xhr request object
let xhr = new XMLHttpRequest();
let url = `/api/pdf/iticket/${this.no}?lang=en`;
xhr.open('GET', url, true);
xhr.responseType = 'blob';
// Xhr callback when we get a result back
// We are not using arrow function because we need the 'this' context
xhr.onreadystatechange = function() {
// We use setTimeout to trigger change detection in Zones
setTimeout( () => { self.pending = false; }, 0);
// If we get an HTTP status OK (200), save the file using fileSaver
if(xhr.readyState === 4 && xhr.status === 200) {
var blob = new Blob([this.response], {type: 'application/pdf'});
fileSaver.saveAs(blob, 'Report.pdf');
}
};
// Start the Ajax request
xhr.send();
}
}
I've used Font Awesome for the fonts used in the template. I wanted the component to display a download button and a spinner while the pdf is fetched.
Also, notice I could use require to fetch the fileSaver.js module. This is because I'm using WebPack so I can require/import like I want. Your syntax might be different depending of your build tool.
I don't think all of these hacks are necessary. I just did a quick test with the standard http service in angular 2.0, and it worked as expected.
/* generic download mechanism */
public download(url: string, data: Object = null): Observable<Response> {
//if custom headers are required, add them here
let headers = new Headers();
//add search parameters, if any
let params = new URLSearchParams();
if (data) {
for (let key in data) {
params.set(key, data[key]);
}
}
//create an instance of requestOptions
let requestOptions = new RequestOptions({
headers: headers,
search: params
});
//any other requestOptions
requestOptions.method = RequestMethod.Get;
requestOptions.url = url;
requestOptions.responseType = ResponseContentType.Blob;
//create a generic request object with the above requestOptions
let request = new Request(requestOptions);
//get the file
return this.http.request(request)
.catch(err => {
/* handle errors */
});
}
/* downloads a csv report file generated on the server based on search criteria specified. Save using fileSaver.js. */
downloadSomethingSpecifc(searchCriteria: SearchCriteria): void {
download(this.url, searchCriteria)
.subscribe(
response => {
let file = response.blob();
console.log(file.size + " bytes file downloaded. File type: ", file.type);
saveAs(file, 'myCSV_Report.csv');
},
error => { /* handle errors */ }
);
}
Here is the simplest way to download a file from an API that I was able to come up with.
import { Injectable } from '#angular/core';
import { Http, ResponseContentType } from "#angular/http";
import * as FileSaver from 'file-saver';
#Injectable()
export class FileDownloadService {
constructor(private http: Http) { }
downloadFile(api: string, fileName: string) {
this.http.get(api, { responseType: 'blob' })
.subscribe((file: Blob) => {
FileSaver.saveAs(file, fileName);
});
}
}
Call the downloadFile(api,fileName) method from your component class.
To get FileSaver run the following commands in your terminal
npm install file-saver --save
npm install #types/file-saver --save
Hello, here is a working example. It is also suitable for PDF!
application/octet-stream - general type.
Controller:
public FileResult exportExcelTest()
{
var contentType = "application/octet-stream";
HttpContext.Response.ContentType = contentType;
RealisationsReportExcell reportExcell = new RealisationsReportExcell();
byte[] filedata = reportExcell.RunSample1();
FileContentResult result = new FileContentResult(filedata, contentType)
{
FileDownloadName = "report.xlsx"
};
return result;
}
Angular2:
Service xhr:
import { Injectable } from '#angular/core';
import { BrowserXhr } from '#angular/http';
#Injectable()
export class CustomBrowserXhr extends BrowserXhr {
constructor() {
super();
}
public build(): any {
let xhr = super.build();
xhr.responseType = "blob";
return <any>(xhr);
}
}
Install file-saver npm packages "file-saver": "^1.3.3", "#types/file-saver": "0.0.0" and include in vendor.ts import 'file-saver';
Component btn download.
import { Component, OnInit, Input } from "#angular/core";
import { Http, ResponseContentType } from '#angular/http';
import { CustomBrowserXhr } from '../services/customBrowserXhr.service';
import * as FileSaver from 'file-saver';
#Component({
selector: 'download-btn',
template: '<button type="button" (click)="downloadFile()">Download</button>',
providers: [
{ provide: CustomBrowserXhr, useClass: CustomBrowserXhr }
]
})
export class DownloadComponent {
#Input() api: string;
constructor(private http: Http) {
}
public downloadFile() {
return this.http.get(this.api, { responseType: ResponseContentType.Blob })
.subscribe(
(res: any) =>
{
let blob = res.blob();
let filename = 'report.xlsx';
FileSaver.saveAs(blob, filename);
}
);
}
}
Using
<download-btn api="api/realisations/realisationsExcel"></download-btn>
To get Filesaver working in Angular 5: Install
npm install file-saver --save
npm install #types/file-saver --save
In your component use import * as FileSaver from "file-saver";
and use FileSaver.default and not FileSaver.SaveAs
.subscribe(data => {
const blob = data.data;
const filename = "filename.txt";
FileSaver.default(blob, filename);
Here is the code that works for downloadign the API respone in IE and chrome/safari. Here response variable is API response.
Note: http call from client needs to support blob response.
let blob = new Blob([response], {type: 'application/pdf'});
let fileUrl = window.URL.createObjectURL(blob);
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(blob, fileUrl.split(':')[1] + '.pdf');
} else {
window.open(fileUrl);
}
Working solution with C# Web API loading PDF as a byte array:
C# loads PDF as a byte array and converts to Base64 encoded string
public HttpResponseMessage GetPdf(Guid id)
{
byte[] file = GetFile(id);
HttpResponseMessage result = Request.CreateResponse(HttpStatusCode.OK);
result.Content = new StringContent("data:application/pdf;base64," + Convert.ToBase64String(file));
return result;
}
Angular service gets PDF
getPdf(): Observable<string> {
return this.http.get(webApiRequest).pipe(
map(response => {
var anonymous = <any>response;
return anonymous._body;
})
);
}
Component view embeds the PDF via binding to service response
The pdfSource variable below is the returned value from the service.
<embed [src]="sanitizer.bypassSecurityTrustResourceUrl(pdfSource)" type="application/pdf" width="100%" height="300px" />
See the Angular DomSanitizer docs for more info.
http
.post(url, data, {
responseType: "blob",
observe: "response"
})
.pipe(
map(response => {
saveAs(response.body, "fileName.pdf");
})
);
Extending what #ThierryTemplier did (the accepted answer) for Angular 8.
HTML:
<button mat-raised-button color="accent" (click)="downloadFile()">Download</button>
TypeScript:
downloadFile() {
this.http.get(
'http://localhost:4200/assets/other/' + this.fileName, {responseType: 'blob'})
.pipe(tap( // Log the result or error
data => console.log(this.fileName, data),
error => console.log(this.fileName, error)
)).subscribe(results => {
saveAs(results, this.fileName);
});
}
Sources:
FileSaver
Angular Http Client