Angular 2 download PDF from API and Display it in View - pdf

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

Related

Failed to send post request with Capacitor (works fine from regular webpage)

I'm working on an app using Ionic (Vue) and have already set a server side (Python with FastAPI).
While I've no issues sending a request when running my app on the browser (ionic serve), I fail to do so when running it on iOS simulator.
I've tried both axios and Capacitor HTTP library and the outcomes are the same.
Below are only the relevant parts from client:
<template>
<base-layout page-title="Choose Players" page-default-back-link="/home">
<ion-item v-for="contact in contacts" :key="contact.phoneNumbers[0]" #click="toggleContact(contact)">
<ion-icon v-if="isContactSelected(contact)" slot="end" :icon="checkmark"/>
{{ contact.displayName }}
</ion-item>
<ion-button expand="block" #click="inviteSelectedContacts">Invite Selected Contacts</ion-button>
</base-layout>
</template>
<script>
import { IonItem, IonIcon, IonButton } from "#ionic/vue";
import { checkmark } from 'ionicons/icons';
import { Contacts } from "#capacitor-community/contacts";
import { serverURL } from '#/components/Config';
// import axios from 'axios';
import { Http } from '#capacitor-community/http';
export default {
name: "AddPlayers",
data() {
return {
contacts: [],
chosenContacts: new Set()
};
},
methods: {
async inviteSelectedContacts() {
let data = {userId: 'test id', users: this.chosenContacts}
let url = `${serverURL}/inviteUsers`;
try {
let options = {url: url, data: data}
let res = await Http.post(options);
if (res.status === 200){
alert(res.status)
}
return res.data
}
catch (err) {
alert(err);
}
}
},
My conclusion is that it's somehow related to CORS not being defined properly for Capacitor only (as they're working just fine for the browser), but I couldn't understand where should I define those.
The server API url is http://fastapi.localhost:8008
Update:
I was able to log and view the error (via Xcode):
[log] - {"message":"Network Error","name":"Error","stack":"createError#\nhandleError#","config":{"transitional":{"silentJSONParsing":true,"forcedJSONParsing":true,"clarifyTimeoutError":false},"transformRequest":[null],"transformResponse":[null],"timeout":0,"xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","maxContentLength":-1,"maxBodyLength":-1,"headers":{"Accept":"application/json, text/plain, */*","Content-Type":"application/json"},"method":"post","url":"http://fastapi.localhost:8008/inviteUsers","data":"{\"userId\":\"test id\",\"users\":{}}"},"status":null}
Too much time wasted on this one...
It turns out that the solution was to change the development server URL to an IP based one: http://127.0.0.1:8008 (in my case).
I don't know why Capacitor doesn't accept DNS naming.
I was able to verify it by adding this dummy endpoint to my server:
from fastapi import FastAPI, Request
...
#app.post("/dummypath")
async def get_body(request: Request):
return await request.json()
so now from the client:
async inviteSelectedContacts() {
// let data = {userId: 'test id', users: this.chosenContacts}
let data = {}
let url = `${serverURL}/dummypath`;
try {
let res = await axios.post(url, data);
if (res.status === 200){
alert(res.status)
}
alert(res);
return res.data
}
catch (err) {
console.log(err);
alert(err);
}
}
where serverURL is http://127.0.0.1:8008

Nestjs - file upload with fastify multipart

I am trying to upload multiple files with nestjs using the fastify adapter. I can do so following the tutorial in this link -article on upload
Now this does the job of file upload using fastify-multipart, but I couldnt make use of the request validations before uploading,
for example, here is my rule-file-models (which later I wanted to save to postgre)
import {IsUUID, Length, IsEnum, IsString, Matches, IsOptional} from "class-validator";
import { FileExtEnum } from "./enums/file-ext.enum";
import { Updatable } from "./updatable.model";
import {Expose, Type} from "class-transformer";
export class RuleFile {
#Expose()
#IsUUID("4", { always: true })
id: string;
#Expose()
#Length(2, 50, {
always: true,
each: true,
context: {
errorCode: "REQ-000",
message: `Filename shouldbe within 2 and can reach a max of 50 characters`,
},
})
fileNames: string[];
#Expose()
#IsEnum(FileExtEnum, { always: true, each: true })
fileExts: string[];
#IsOptional({each: true, message: 'File is corrupated'})
#Type(() => Buffer)
file: Buffer;
}
export class RuleFileDetail extends RuleFile implements Updatable {
#IsString()
#Matches(/[aA]{1}[\w]{6}/)
recUpdUser: string;
}
And I wanted to validate the multipart request and see if these are set properly.
I cannot make it to work with event subscription based approach. Here are a few things I tried - adding the interceptor, to check for the request
#Injectable()
export class FileUploadValidationInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req: FastifyRequest = context.switchToHttp().getRequest();
console.log('inside interceptor', req.body);
// content type cmes with multipart/form-data;boundary----. we dont need to valdidate the boundary
// TODO: handle split errors based on semicolon
const contentType = req.headers['content-type'].split(APP_CONSTANTS.CHAR.SEMI_COLON)[0];
console.log(APP_CONSTANTS.REGEX.MULTIPART_CONTENT_TYPE.test(contentType));
const isHeaderMultipart = contentType != null?
this.headerValidation(contentType): this.throwError(contentType);
**// CANNOT check fir req.file() inside this, as it throws undefined**
return next.handle();
}
headerValidation(contentType) {
return APP_CONSTANTS.REGEX.MULTIPART_CONTENT_TYPE.test(contentType) ? true : this.throwError(contentType);
}
throwError(contentType: string) {
throw AppConfigService.getCustomError('FID-HEADERS', `Request header does not contain multipart type:
Provided incorrect type - ${contentType}`);
}
}
I wasnt able to check req.file() in the above interceptor. It throws as undefined. I tried to follow the fastify-multipart
But I wasnt able to get the request data in a prehandler as provided in the documentation for fastify-multipart
fastify.post('/', async function (req, reply) {
// process a single file
// also, consider that if you allow to upload multiple files
// you must consume all files othwise the promise will never fulfill
const data = await req.file()
data.file // stream
data.fields // other parsed parts
data.fieldname
data.filename
data.encoding
data.mimetype
// to accumulate the file in memory! Be careful!
//
// await data.toBuffer() // Buffer
//
// or
await pump(data.file, fs.createWriteStream(data.filename))
I tried getting via by registering a prehandler hook of my own like this (executed as iife)
(async function bootstrap() {
const appConfig = AppConfigService.getAppCommonConfig();
const fastifyInstance = SERVERADAPTERINSTANCE.configureFastifyServer();
// #ts-ignore
const fastifyAdapter = new FastifyAdapter(fastifyInstance);
app = await NestFactory.create<NestFastifyApplication>(
AppModule,
fastifyAdapter
).catch((err) => {
console.log("err in creating adapter", err);
process.exit(1);
});
.....
app.useGlobalPipes(
new ValidationPipe({
errorHttpStatusCode: 500,
transform: true,
validationError: {
target: true,
value: true,
},
exceptionFactory: (errors: ValidationError[]) => {
// send it to the global exception filter\
AppConfigService.validationExceptionFactory(errors);
},
}),
);
app.register(require('fastify-multipart'), {
limits: {
fieldNameSize: 100, // Max field name size in bytes
fieldSize: 1000000, // Max field value size in bytes
fields: 10, // Max number of non-file fields
fileSize: 100000000000, // For multipart forms, the max file size
files: 3, // Max number of file fields
headerPairs: 2000, // Max number of header key=>value pairs
},
});
(app.getHttpAdapter().getInstance() as FastifyInstance).addHook('onRoute', (routeOptions) => {
console.log('all urls:', routeOptions.url);
if(routeOptions.url.includes('upload')) {
// The registration actually works, but I cant use the req.file() in the prehandler
console.log('###########################');
app.getHttpAdapter().getInstance().addHook('preHandler', FilePrehandlerService.fileHandler);
}
});
SERVERADAPTERINSTANCE.configureSecurity(app);
//Connect to database
await SERVERADAPTERINSTANCE.configureDbConn(app);
app.useStaticAssets({
root: join(__dirname, "..", "public"),
prefix: "/public/",
});
app.setViewEngine({
engine: {
handlebars: require("handlebars"),
},
templates: join(__dirname, "..", "views"),
});
await app.listen(appConfig.port, appConfig.host, () => {
console.log(`Server listening on port - ${appConfig.port}`);
});
})();
Here is the prehandler,
export class FilePrehandlerService {
constructor() {}
static fileHandler = async (req, reply) => {
console.log('coming inside prehandler');
console.log('req is a multipart req',await req.file);
const data = await req.file();
console.log('data received -filename:', data.filename);
console.log('data received- fieldname:', data.fieldname);
console.log('data received- fields:', data.fields);
return;
};
}
This pattern of registring and gettin the file using preHandler works in bare fastify application. I tried it
Bare fastify server:
export class FileController {
constructor() {}
async testHandler(req: FastifyRequest, reply: FastifyReply) {
reply.send('test reading dne');
}
async fileReadHandler(req, reply: FastifyReply) {
const data = await req.file();
console.log('field val:', data.fields);
console.log('field filename:', data.filename);
console.log('field fieldname:', data.fieldname);
reply.send('done');
}
}
export const FILE_CONTROLLER_INSTANCE = new FileController();
This is my route file
const testRoute: RouteOptions<Server, IncomingMessage, ServerResponse, RouteGenericInterface, unknown> = {
method: 'GET',
url: '/test',
handler: TESTCONTROLLER_INSTANCE.testMethodRouteHandler,
};
const fileRoute: RouteOptions = {
method: 'GET',
url: '/fileTest',
preHandler: fileInterceptor,
handler: FILE_CONTROLLER_INSTANCE.testHandler,
};
const fileUploadRoute: RouteOptions = {
method: 'POST',
url: '/fileUpload',
preHandler: fileInterceptor,
handler: FILE_CONTROLLER_INSTANCE.fileReadHandler,
};
const apiRoutes = [testRoute, fileRoute, fileUploadRoute];
export default apiRoutes;
Could someone let me know the right the way to get the fieldnames , validate them befr the service being called in Nestjs
Well, I have done something like this and It works great for me. Maybe it can work for you too.
// main.ts
import multipart from "fastify-multipart";
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
app.register(multipart);
// upload.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
BadRequestException,
} from "#nestjs/common";
import { FastifyRequest } from "fastify";
#Injectable()
export class UploadGuard implements CanActivate {
public async canActivate(ctx: ExecutionContext): Promise<boolean> {
const req = ctx.switchToHttp().getRequest() as FastifyRequest;
const isMultipart = req.isMultipart();
if (!isMultipart)
throw new BadRequestException("multipart/form-data expected.");
const file = await req.file();
if (!file) throw new BadRequestException("file expected");
req.incomingFile = file;
return true;
}
}
// file.decorator.ts
import { createParamDecorator, ExecutionContext } from "#nestjs/common";
import { FastifyRequest } from "fastify";
export const File = createParamDecorator(
(_data: unknown, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest() as FastifyRequest;
const file = req.incomingFile;
return file
},
);
// post controller
#Post("upload")
#UseGuards(UploadGuard)
uploadFile(#File() file: Storage.MultipartFile) {
console.log(file); // logs MultipartFile from "fastify-multipart"
return "File uploaded"
}
and finally my typing file
declare global {
namespace Storage {
interface MultipartFile {
toBuffer: () => Promise<Buffer>;
file: NodeJS.ReadableStream;
filepath: string;
fieldname: string;
filename: string;
encoding: string;
mimetype: string;
fields: import("fastify-multipart").MultipartFields;
}
}
}
declare module "fastify" {
interface FastifyRequest {
incomingFile: Storage.MultipartFile;
}
}
So I found a simpler alternative. I started using fastify-multer. I used it along with this awesome lib - which made me use the multer for fastify - #webundsoehne/nest-fastify-file-upload
These are the changes I made. I registered the multer content process.
app.register(multer( {dest:path.join(process.cwd()+'/upload'),
limits:{
fields: 5, //Number of non-file fields allowed
files: 1,
fileSize: 2097152,// 2 MB,
}}).contentParser);
Then in the controller - I use it as the nestjs doc says . This actually makes fasitfy work with multer
#UseInterceptors(FileUploadValidationInterceptor, FileInterceptor('file'))
#Post('/multerSample')
async multerUploadFiles(#UploadedFile() file, #Body() ruleFileCreate: RuleFileCreate) {
console.log('data sent', ruleFileCreate);
console.log(file);
// getting the original name of the file - no matter what
ruleFileCreate.originalName = file.originalname;
return await this.fileService.fileUpload(file.buffer, ruleFileCreate);
}
BONUS - storing the file in local and storing it in DB - Please refer
github link

React-Admin <ImageInput> to upload images to rails api

I am trying to upload images from react-admin to rails api backend using active storage.
In the documentation of react-admin it says: "Note that the image upload returns a File object. It is your responsibility to handle it depending on your API behavior. You can for instance encode it in base64, or send it as a multi-part form data" I am trying to send it as a multi-part form.
I have been reading here and there but I can not find what I want, at least a roadmap of how I should proceed.
You can actually find an example in the dataProvider section of the documentation.
You have to decorate your dataProvider to enable the data upload. Here is the example of transforming the images into base64 strings before posting the resource:
// in addUploadFeature.js
/**
* Convert a `File` object returned by the upload input into a base 64 string.
* That's not the most optimized way to store images in production, but it's
* enough to illustrate the idea of data provider decoration.
*/
const convertFileToBase64 = file => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file.rawFile);
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
});
/**
* For posts update only, convert uploaded image in base 64 and attach it to
* the `picture` sent property, with `src` and `title` attributes.
*/
const addUploadFeature = requestHandler => (type, resource, params) => {
if (type === 'UPDATE' && resource === 'posts') {
// notice that following condition can be true only when `<ImageInput source="pictures" />` component has parameter `multiple={true}`
// if parameter `multiple` is false, then data.pictures is not an array, but single object
if (params.data.pictures && params.data.pictures.length) {
// only freshly dropped pictures are instance of File
const formerPictures = params.data.pictures.filter(p => !(p.rawFile instanceof File));
const newPictures = params.data.pictures.filter(p => p.rawFile instanceof File);
return Promise.all(newPictures.map(convertFileToBase64))
.then(base64Pictures => base64Pictures.map((picture64, index) => ({
src: picture64,
title: `${newPictures[index].title}`,
})))
.then(transformedNewPictures => requestHandler(type, resource, {
...params,
data: {
...params.data,
pictures: [...transformedNewPictures, ...formerPictures],
},
}));
}
}
// for other request types and resources, fall back to the default request handler
return requestHandler(type, resource, params);
};
export default addUploadFeature;
You can then apply this on your dataProvider:
// in dataProvider.js
import simpleRestProvider from 'ra-data-simple-rest';
import addUploadFeature from './addUploadFeature';
const dataProvider = simpleRestProvider('http://path.to.my.api/');
const uploadCapableDataProvider = addUploadFeature(dataProvider);
export default uploadCapableDataProvider;
Finally, you can use it in your admin as usual:
// in App.js
import { Admin, Resource } from 'react-admin';
import dataProvider from './dataProvider';
import PostList from './posts/PostList';
const App = () => (
<Admin dataProvider={uploadCapableDataProvider}>
<Resource name="posts" list={PostList} />
</Admin>
);
When using files, use a multi-part form in the react front-end and for example multer in your API backend.
In react-admin you should create a custom dataProvider and extend either the default or built a custom one. Per implementation you should handle the file/files upload. For uploading a file or files from your custom dataprovider in react-admin:
// dataProvider.js
// this is only the implementation for a create
case "CREATE":
const formData = new FormData();
for ( const param in params.data ) {
// 1 file
if (param === 'file') {
formData.append('file', params.data[param].rawFile);
continue
}
// when using multiple files
if (param === 'files') {
params.data[param].forEach(file => {
formData.append('files', file.rawFile);
});
continue
}
formData.append(param, params.data[param]);
}
return httpClient(`myendpoint.com/upload`, {
method: "POST",
body: formData,
}).then(({ json }) => ({ data: json });
From there you pick it up in your API using multer, that supports multi-part forms out-of-the-box. When using nestjs that could look like:
import {
Controller,
Post,
Header,
UseInterceptors,
UploadedFile,
} from "#nestjs/common";
import { FileInterceptor } from '#nestjs/platform-express'
#Controller("upload")
export class UploadController {
#Post()
#Header("Content-Type", "application/json")
// multer extracts file from the request body
#UseInterceptors(FileInterceptor('file'))
async uploadFile(
#UploadedFile() file : Record<any, any>
) {
console.log({ file })
}
}

How To Use FormData() For Uploading File In Vue

So I had asked a question previously, and got a little bit of help as far as logging the results however my results are not making sense.
So I have a input
<input type="file" name="import_file" v-on:change="selectedFile($event)">
The v-on:change binds the selected file to my data object this.file
selectedFile(event) {
this.file = event.target.files[0]
},
and then I submit the file with this method
uploadTodos() {
let formData = new FormData();
formData.append('file', this.file);
for(var pair of formData.entries()) {
console.log(pair[0]+ ', '+ pair[1]);
}
this.$store.dispatch('uploadTodos', formData);
}
However when I submit it seems there is no data attached to formData because my logged result is this
file, [object File]
shouldn't I have my actual data appended to the formData object??
I have referenced other articles on how to post but I am not getting the desired results.
article 1
article2
uploadTodos(context, file) {
console.log(file)
axios.post('/import', file,{ headers: {
'Content-Type': 'multipart/form-data'
}})
.then(response => {
console.log(response.data)
context.commit('importTodos', response.data)
})
.catch(error => {
console.log(error.response.data)
})
}
when I console.log(file) the formData object is empty
Backend Question
So my issue with Laravel on the backend is with the maatwebsite package. From what I have seen is the 3.0 version does not yet support imports. And the only work around suggested is to install version 2.0? Is this still the only workaround? Here is the controller method
public function importExcel(Request $request)
{
if (empty($request->file('file')->getRealPath())) {
return back()->with('success','No file selected');
}
else {
$path = $request->file('file')->getRealPath();
$inserts = [];
Excel::load($path,function($reader) use (&$inserts)
{
foreach ($reader->toArray() as $rows){
foreach($rows as $row){
$inserts[] = ['user_id' => $row['user_id'], 'todo' => $row['todo']];
};
}
});
if (!empty($inserts)) {
DB::table('todos')->insert($inserts);
return back()->with('success','Inserted Record successfully');
}
return back();
}
}
The line not suppported by version 3.0 is this
Excel::load($path,function($reader) use (&$inserts)
I have reproduced your code and it seems to be working fine
when I console.log(file) the formData object is empty
Yeah the output should be an empty object when you console, that's the way javascript works.
after casting the output to an array i get the output in the image below:
const store = new Vuex.Store({
actions: {
uploadTodos(context, file) {
console.log([...file])
axios.post('/import', file,{ headers: {
'Content-Type': 'multipart/form-data'
}})
.then(response => {
console.log(response.data)
context.commit('importTodos', response.data)
})
.catch(error => {
console.log(error.response.data)
})
}
}
})
const app = new Vue({
store,
data: {
file: null
},
methods: {
selectedFile(event) {
console.log(event);
this.file = event.target.files[0]
},
uploadTodos() {
let formData = new FormData();
formData.append('file', this.file);
for(var pair of formData.entries()) {
console.log(pair[0]+ ', '+ pair[1]);
}
this.$store.dispatch('uploadTodos', formData);
}
},
el: '#app'
})
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vuex"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<div id="app">
<input type="file" name="import_file" #change="selectedFile($event)">
<button #click="uploadTodos">
Submit
</button>
</div>
This post answers the second part of the question. At first from what I read maatwebsite/excel version 3.0 does not support import. However I am using version 3.1.0 which does support imports. However the method for importing still does not suppport Excel::load(). You should instead use Excel::import() and follow the given rules for passing in parameters. Which of course can be modified to suit your needs. But anyways here is a simple example of how I am using it for anyone interested.
First create import file for whatever model it is. For me it is Todos.
<?php
namespace App\Imports;
use App\Todo;
use Maatwebsite\Excel\Concerns\ToModel;
class TodoImport implements ToModel
{
/**
* #param array $row
*
* #return \Illuminate\Database\Eloquent\Model|null
*/
public function model(array $row)
{
return new Todo([
'user_id' => $row[0],
'todo' => $row[1],
]);
}
}
next you have your controller handling the file, and passing it to the todosimport file
public function importExcel(Request $request)
{
if (empty($request->file('file')->getRealPath())) {
return back()->with('success','No file selected');
}
else {
Excel::import(new TodoImport, $request->file('file'));
return response('Import Succesful, Please Refresh Page');
}
}
notice the Excel::import(). I pass in the new Todo model and the file received.
of course for me since I am doing it by ajax I use this route to ping the method
Route::post('/import', 'TodosController#importExcel');

Aurelia common class/model

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!