Generating PDF for reports, forms, invoices, and other data is a common use case for any web application. In a web application, We can generate pdf using various approaches:
- Using browser print function: It is an easy option when you want to print a complete web page as a pdf. You can also customize pdf up to some limits. limitation of this approach is we don’t have strong control on format and design of pdf.
- Generating PDF using Backend application or third-party reporting tools and download it on client-side: You have more control over pdf formatting and design and you can process large amounts of data. Though this type of pdf generation approach required a separate API call for generating the pdf.
We can solve the limitations of both ways by generating pdf at client side. We can format and design pdf as per our requirement without calling separate API.
Following are the two popular open-source javascript libraries available for client-side pdf generation.
In this article, I will show you how to export a pdf file in angular 8 using pdfmake.
Further, Read related PDFMake Articles
- Client-Side PDF Generation in Angular 13 with PDFMake
- Loading External Libraries from CDN in Angular Application (Load PDFMake from CDN)
- Insert Image from URL in PDF using PDFMake
We will cover the following topics in this tutorial
- ✔️ Introduction
- ✔️ Environment Setup
- ✔️ Online Resume Builder using Angular and PDFMake
- 📌 Final Code Review
- 🔗 Git Repository
- 🚀 Live Application
So let’s start with PDFMake introduction,
PDFMake is very popular client-side and server-side pdf generation javascript library. It has 100,000+ weekly downloads from npm. And 7K+ GitHub stars.
It is easy to use and provides all required features for pdf design and formatting with some extraordinary features like QR Code, Table of contents and Helper methods for Opening pdf, download pdf, and pdf printing.
We will create an Online resume builder application in angular 8 and PDFMake. Here we will get the personal details, educational details and experience details and generate pdf at client side. We will provide different options for print pdf and download pdf.
I have published this application on netlify you can check it here https://online-resume-builder.netlify.com/
Create a Angular Project
Use below command to create a new Angular project with Angular CLI.
ng new online-resume-builder
Install PDFMake Library
npm install --save pdfmake
Import pdfmake and vfs_fonts
To begin in browser with the default configuration, we should include two files Pdfmake.js
and vfs_fonts.js
. When you install Pdfmake
from npm it comes with the both file.
Now to use this files in angular component or service add below import statement on top of component/service
import pdfMake from 'pdfmake/build/pdfmake'; import pdfFonts from 'pdfmake/build/vfs_fonts'; pdfMake.vfs = pdfFonts.pdfMake.vfs;
Generate single line text pdf for testing our environment setup
PDFMake follows a declarative approach. It basically means, you’ll never have to calculate positions manually or use commands like
writeText(text, x, y)
,moveDown
etc…, as you would with a lot of other libraries. The most fundamental concept to be mastered is thedocument-definition-object
which can be as simple as:
All the pdf formatting and design configuration are written in document-definition-object. As shown below :
export class AppComponent { generatePdf(){ const documentDefinition = { content: 'This is an sample PDF printed with pdfMake' }; pdfmake.createPdf(documentDefinition).open(); } }
<button (click)="generatePdf()">Open PDF</button>
Add Open PDF
button on app.component.html
and call generatePdf()
.
Serve your application and test. This will show pdf as below :
If you can see above pdf on click of Open PDF button then environment setup is done. Now we can start with our resume builder application
PDFMake comes with inbuilt methods :
- Download the PDF :
pdfMake.createPdf(docDefinition).download();
- Open the PDF in new window :
pdfMake.createPdf(docDefinition).open();
- Open PDF in same window :
pdfMake.createPdf(docDefinition).open({}, window);
- Print the PDF:
pdfMake.createPdf(docDefinition).print();
PDFMake also provides methods for :
- Put the PDF into your own page as URL data
- Get the PDF as base64 data
- Get the PDF as a buffer
- Get the PDF as Blob
Refer here for more details.
Same as PDF, You can generate excel at client side. Refer Export to Excel in Angular blog for client-side excel generation in Angular.
Now for an online resume builder, I have designed a resume form to get personal details, skills, experience details, education details, and other details. As shown below :
Here I have used the template-driven form and mapped it to resume
object with it using two-way data binding.
Now to open pdf in a browser, download the pdf and print the pdf I have included three action buttons. One more action button I have added to reset form.
User will also have the option to upload his/her picture.
I have stored the resume object in sessionStorage
so that if the user refreshes the window, entered details doesn’t lose.
So our initial app setup will be as below source code.
<nav class="navbar navbar-expand navbar-light bg-primary d-flex justify-content-between"> <div class="navbar-brand mb-0 h1 text-white">ONLINE RESUME BUILDER</div> </nav> <div class="container-fluid"> <form #resumeForm="ngForm"> <div class="row"> <div class="col-md-8"> <div class="shadow-sm card"> <div class="card-body"> <h4 class="card-title d-flex align-items-center"> <i class="material-icons"> account_circle </i> Personal Details</h4> <div class="row"> <div class="col-md-6"> <div class="form-group"> <input type="text" class="form-control" [(ngModel)]="resume.name" name="name" placeholder="Name" #Name="ngModel" required [ngClass]="{'is-invalid': Name.invalid && (Name.touched || resumeForm.submitted) }"> </div> <div class="form-group"> <div class="form-group"> <textarea class="form-control" [(ngModel)]="resume.address" name="address" rows="3" placeholder="Address" #Address="ngModel" required [ngClass]="{'is-invalid': Address.invalid && (Address.touched || resumeForm.submitted) }"></textarea> </div> </div> </div> <div class="col-md-6"> <div class="form-group"> <input type="text" class="form-control" [(ngModel)]="resume.contactNo" name="contactNo" placeholder="Contact No." #ContactNo="ngModel" required [ngClass]="{'is-invalid': ContactNo.invalid && (ContactNo.touched || resumeForm.submitted) }"> </div> <div class="form-group"> <input type="text" class="form-control" [(ngModel)]="resume.email" name="emailId" placeholder="Email ID" #Email="ngModel" required [ngClass]="{'is-invalid': Email.invalid && (Email.touched || resumeForm.submitted) }"> </div> <div class="form-group"> <input type="text" class="form-control" [(ngModel)]="resume.socialProfile" name="socialProfile" placeholder="Social Profile "> </div> </div> </div> </div> </div> <!-- Skills --> <div class="shadow-sm card"> <div class="card-body"> <div class="d-flex justify-content-between card-title"> <h4 class="d-flex align-items-center"> <i class="material-icons"> timeline </i> Skills</h4> <button class="btn btn-primary" (click)="addSkill()">+</button> </div> <div class="row"> <div class="col-md-4" *ngFor="let s of resume.skills; let i=index"> <div class="form-group"> <input type="text" class="form-control" name="skill{{i}}" [(ngModel)]="s.value" placeholder="e.g. Java / Angular / .Net " #Skill="ngModel" required [ngClass]="{'is-invalid': Skill.invalid && (Skill.touched || resumeForm.submitted) }"> </div> </div> </div> </div> </div> <!-- Experience --> <div class="shadow-sm card"> <div class="card-body"> <div class="d-flex justify-content-between card-title"> <h4 class="d-flex align-items-center"> <i class="material-icons"> timeline </i> Experience</h4> <button class="btn btn-primary" (click)="addExperience()">+</button> </div> <div class="row" *ngFor="let ex of resume.experiences; let i=index"> <div class="col-md-6"> <div class="form-group"> <input type="text" class="form-control" name="employer{{i}}" [(ngModel)]="ex.employer" placeholder="Employer" #Employer="ngModel" required [ngClass]="{'is-invalid': Employer.invalid && (Employer.touched || resumeForm.submitted) }"> </div> <div class="form-group"> <div class="form-group"> <textarea class="form-control" name="jobDescription{{i}}" [(ngModel)]="ex.jobDescription" rows="3" placeholder="Job Description"></textarea> </div> </div> </div> <div class="col-md-6"> <div class="form-group"> <input type="text" class="form-control" name="jobTitle{{i}}" [(ngModel)]="ex.jobTitle" placeholder="Job Title" #JobTitle="ngModel" required [ngClass]="{'is-invalid': JobTitle.invalid && (JobTitle.touched || resumeForm.submitted) }"> </div> <div class="form-group"> <input type="number" class="form-control" name="exInMonths{{i}}" [(ngModel)]="ex.experience" placeholder="Experience in months" #Experience="ngModel" required [ngClass]="{'is-invalid': Experience.invalid && (Experience.touched || resumeForm.submitted) }"> </div> </div> </div> </div> </div> <!-- Education --> <div class="shadow-sm card"> <div class="card-body"> <div class="d-flex justify-content-between card-title"> <h4 class="d-flex align-items-center"> <i class="material-icons"> school </i> Education</h4> <button class="btn btn-primary" (click)="addEducation()">+</button> </div> <div class="row" *ngFor="let ed of resume.educations; let i=index"> <div class="col-md-3"> <div class="form-group"> <select class="form-control" placeholder="Degree" name="degree{{i}}" [(ngModel)]="ed.degree" #Degree="ngModel" required [ngClass]="{'is-invalid': Degree.invalid && (Degree.touched || resumeForm.submitted) }"> <option [value]="d" *ngFor="let d of degrees">{{d}}</option> </select> </div> </div> <div class="col-md-3"> <div class="form-group"> <input type="text" class="form-control" name="college{{i}}" [(ngModel)]="ed.college" placeholder="School/College" #College="ngModel" required [ngClass]="{'is-invalid': College.invalid && (College.touched || resumeForm.submitted) }"> </div> </div> <div class="col-md-3"> <div class="form-group"> <input type="number" class="form-control" name="passingYear{{i}}" [(ngModel)]="ed.passingYear" placeholder="Passing Year" #PassingYear="ngModel" required [ngClass]="{'is-invalid': PassingYear.invalid && (PassingYear.touched || resumeForm.submitted) }"> </div> </div> <div class="col-md-3"> <div class="form-group"> <input type="number" class="form-control" name="result{{i}}" [(ngModel)]="ed.percentage" placeholder="Percentage" #Percentage="ngModel" required [ngClass]="{'is-invalid': Percentage.invalid && (Percentage.touched || resumeForm.submitted) }"> </div> </div> </div> </div> </div> <!-- Other Details --> <div class="shadow-sm card"> <div class="card-body"> <h4 class="card-title d-flex align-items-center"> <i class="material-icons"> list </i>Other Details</h4> <div class="row"> <div class="col-md-12"> <div class="form-group"> <textarea type="text" class="form-control" [(ngModel)]="resume.otherDetails" name="otherDetails" rows="4"></textarea> </div> </div> </div> </div> </div> </div> <div class="col-md-4"> <div class="shadow-sm card action-buttons"> <button (click)="resumeForm.valid ? generatePdf('open') : ''" class="btn btn-primary d-flex align-items-center justify-content-center"> <i class="material-icons"> picture_as_pdf </i> <span>Open PDF</span></button> <button (click)="resumeForm.valid ? generatePdf('download') : ''" class="btn btn-primary d-flex align-items-center justify-content-center"> <i class="material-icons"> cloud_download </i><span>Download PDF</span></button> <button (click)="resumeForm.valid ? generatePdf('print') : ''" class="btn btn-primary d-flex align-items-center justify-content-center"> <i class="material-icons"> print </i><span>Print PDF</span></button> <button type='reset' (click)="resetForm()" class="btn btn-primary d-flex align-items-center justify-content-center"> <i class="material-icons"> clear </i><span>Reset</span></button> </div> <div class="card p-4"> <div class="form-group"> <label class="h4 mb-3" for="">Show your picture in Resume</label> <input type="file" class="form-control-file" (change)="fileChanged($event)" aria-describedby="fileHelpId"> </div> <img *ngIf="resume.profilePic" [src]="resume.profilePic" class="img-thumbnail"> </div> </div> </div> </form> </div>
import { Component } from '@angular/core'; import pdfMake from 'pdfmake/build/pdfmake'; import pdfFonts from 'pdfmake/build/vfs_fonts'; import { Resume, Experience, Education, Skill } from './resume'; pdfMake.vfs = pdfFonts.pdfMake.vfs; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { resume = new Resume(); degrees = ['B.E.', 'M.E.', 'B.Com', 'M.Com']; constructor() { this.resume = JSON.parse(sessionStorage.getItem('resume')) || new Resume(); if (!this.resume.experiences || this.resume.experiences.length === 0) { this.resume.experiences = []; this.resume.experiences.push(new Experience()); } if (!this.resume.educations || this.resume.educations.length === 0) { this.resume.educations = []; this.resume.educations.push(new Education()); } if (!this.resume.skills || this.resume.skills.length === 0) { this.resume.skills = []; this.resume.skills.push(new Skill()); } } addExperience() { this.resume.experiences.push(new Experience()); } addEducation() { this.resume.educations.push(new Education()); } generatePdf(action = 'open') { const documentDefinition = this.getDocumentDefinition(); switch (action) { case 'open': pdfMake.createPdf(documentDefinition).open(); break; case 'print': pdfMake.createPdf(documentDefinition).print(); break; case 'download': pdfMake.createPdf(documentDefinition).download(); break; default: pdfMake.createPdf(documentDefinition).open(); break; } } resetForm() { this.resume = new Resume(); } getDocumentDefinition() { sessionStorage.setItem('resume', JSON.stringify(this.resume)); return { content: 'This is a sample PDF' }; } fileChanged(e) { const file = e.target.files[0]; this.getBase64(file); } getBase64(file) { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { console.log(reader.result); this.resume.profilePic = reader.result as string; }; reader.onerror = (error) => { console.log('Error: ', error); }; } addSkill() { this.resume.skills.push(new Skill()); } }
export class Resume { profilePic: string; name: string; address: string; contactNo: number; email: string; socialProfile: string; experiences: Experience[] = []; educations: Education[] = []; otherDetails: string; skills: Skill[] = []; constructor() { this.experiences.push(new Experience()); this.educations.push(new Education()); this.skills.push(new Skill()); } } export class Experience { employer: string; jobTitle: string; jobDescription: string; startDate: string; experience: number; } export class Education { degree: string; college: string; passingYear: string; percentage: number; } export class Skill { value: string; }
.row{ padding: 10px; } .card{ margin: 10px; } .material-icons{ font-size: 30px; } button span{ font-size: 22px; } .action-buttons button{ margin : 5px; }
We will write pdf design and configuration in the getDocumentDefinition()
method. Let’s start with adding resume title and personal details.
getDocumentDefinition() { sessionStorage.setItem('resume', JSON.stringify(this.resume));
return { content: [ { text: 'RESUME', bold: true, fontSize: 20, alignment: 'center', margin: [0, 0, 0, 20] }, { columns: [ [{ text: this.resume.name, style: 'name' }, { text: this.resume.address }, { text: 'Email : ' + this.resume.email, }, { text: 'Contant No : ' + this.resume.contactNo, }, { text: 'GitHub: ' + this.resume.socialProfile, link: this.resume.socialProfile, color: 'blue', } ], [ // Document definition for Profile pic ] ] }], styles: { name: { fontSize: 16, bold: true } } }; }
In the first line, we will show RESUME as the title. we have written styling directly in first-line object.
By default, paragraphs are rendered as a vertical stack of elements (one below another). It is possible however to divide available space into columns.
Second-line we will display with two columns as shown above, In the first column, we will display personal details, and in the second column, we will show a picture.
For name
, we have added styles in styles dictionary.
It is easy to add an image in pdf using PDFMake. you just need to use the { image: '...' }
node type. PDFMake supports JPEG and PNG image formats.
You can specify an image in the form of base64 data URI image or in the virtual file system you can specify image path.
I have created a separate method getProfilePicObject()
for getting profile picture image configuration and called it in document definition so that we add configuration only when the user has uploaded an image
getDocumentDefinition() { sessionStorage.setItem('resume', JSON.stringify(this.resume)); return { content: [ { text: 'RESUME', bold: true, fontSize: 20, alignment: 'center', margin: [0, 0, 0, 20] }, { columns: [ [{ text: this.resume.name, style: 'name' }, { text: this.resume.address }, { text: 'Email : ' + this.resume.email, }, { text: 'Contant No : ' + this.resume.contactNo, }, { text: 'GitHub: ' + this.resume.socialProfile, link: this.resume.socialProfile, color: 'blue', } ], [ this.getProfilePicObject() ] ] }], styles: { name: { fontSize: 16, bold: true } } }; } getProfilePicObject() { if (this.resume.profilePic) { return { image: this.resume.profilePic , width: 75, alignment : 'right' }; } return null; }
Personal details we have displayed in two columns, the same way we will display the skills list in three columns list.
pdfmake supports both numbered and bulleted lists.
let docDefinition = { content: [ 'Bulleted list example:', { // to treat a paragraph as a bulleted list, set an array of items under the ul key ul: [ 'Item 1', 'Item 2', 'Item 3', { text: 'Item 4', bold: true }, ] }, 'Numbered list example:', { // for numbered lists set the ol key ol: [ 'Item 1', 'Item 2', 'Item 3' ] } ] };
So now, our document definition object will look like as.
getDocumentDefinition() { // ... return { content: [ //... { text: 'Skills', style: 'header' }, { columns : [ { ul : [ ...this.resume.skills.filter((value, index) => index % 3 === 0).map(s => s.value) ] }, { ul : [ ...this.resume.skills.filter((value, index) => index % 3 === 1).map(s => s.value) ] }, { ul : [ ...this.resume.skills.filter((value, index) => index % 3 === 2).map(s => s.value) ] } ] }, ], styles: { header: { fontSize: 18, bold: true, margin: [0, 20, 0, 10], decoration: 'underline' }, name: { fontSize: 16, bold: true } } }; }
Tables are similar to columns. They can however have headers, borders and cells spanning over multiple columns/rows as shown below :
var docDefinition = { content: [ { table: { // headers are automatically repeated if the table spans over multiple pages // you can declare how many rows should be treated as headers headerRows: 1, widths: [ '*', 'auto', 100, '*' ], body: [ [ 'First', 'Second', 'Third', 'The last one' ], [ 'Value 1', 'Value 2', 'Value 3', 'Value 4' ], [ { text: 'Bold value', bold: true }, 'Val 2', 'Val 3', 'Val 4' ] ] } } ] };
- Tables support three width definitions: star, auto and fixed value
- You can specify no. of header rows using
headerRows
. - You can specify spanning using
colSpan
androwSpan
. - Table supports three layouts : noBorders, headerLineOnly, lightHorizontalLines
We will generate experience table dynamically with getExperienceObject()
method and education table with getEducationObject()
method. as below
getDocumentDefinition() { // ... return { content: [ // ... { text: 'Experience', style: 'header' }, this.getExperienceObject(this.resume.experiences), { text: 'Education', style: 'header' }, this.getEducationObject(this.resume.educations), ], styles: { header: { fontSize: 18, bold: true, margin: [0, 20, 0, 10], decoration: 'underline' }, name: { fontSize: 16, bold: true }, jobTitle: { fontSize: 14, bold: true, italics: true }, tableHeader: { bold: true, } } }; } getExperienceObject(experiences: Experience[]) { const exs = []; experiences.forEach(experience => { exs.push( [{ columns: [ [{ text: experience.jobTitle, style: 'jobTitle' }, { text: experience.employer, }, { text: experience.jobDescription, }], { text: 'Experience : ' + experience.experience + ' Months', alignment: 'right' } ] }] ); }); return { table: { widths: ['*'], body: [ ...exs ] } }; } getEducationObject(educations: Education[]) { return { table: { widths: ['*', '*', '*', '*'], body: [ [{ text: 'Degree', style: 'tableHeader' }, { text: 'College', style: 'tableHeader' }, { text: 'Passing Year', style: 'tableHeader' }, { text: 'Result', style: 'tableHeader' }, ], ...educations.map(ed => { return [ed.degree, ed.college, ed.passingYear, ed.percentage]; }) ] } }; }
We can easily create QR codes in PDFMake using {qr : 'text to shown'}
. as below
let docDefinition = { content: [ // basic usage { qr: 'text in QR' }, // colored QR { qr: 'text in QR', foreground: 'red', background: 'yellow' }, // resized QR { qr: 'text in QR', fit: '500' }, ] }
We will show name and mobile no. in the form of QR code so when anyone scans QR code, the scanner will get name and mobile no.
getDocumentDefinition() { // ... return { content: [ // ... { text: 'Other Details', style: 'header' }, { text: this.resume.otherDetails }, { text: 'Signature', style: 'sign' }, { columns : [ { qr: this.resume.name + ', Contact No : ' + this.resume.contactNo, fit : 100 }, { text: `(${this.resume.name})`, alignment: 'right', } ] } ], styles: { // ... sign: { margin: [0, 50, 0, 10], alignment: 'right', italics: true }, } }; }
PDF documents can have various metadata associated with them, such as the title, or author of the document. You can add that information by adding it to the document definition.
let docDefinition = { info: { title: 'awesome Document', author: 'john doe', subject: 'subject of document', keywords: 'keywords for document', }, content: 'This is an sample PDF printed with pdfMake' }
Standard properties:
- title – the title of the document
- author – the name of the author
- subject – the subject of the document
- keywords – keywords associated with the document
- creator – the creator of the document (default is ‘pdfmake’)
- producer – the producer of the document (default is ‘pdfmake’)
- creationDate – the date the document was created (added automatically by pdfmake)
- modDate – the date the document was last modified
- trapped – the trapped flag in a PDF document indicates whether the document has been “trapped”, i.e. corrected for slight color misregistrations
So our final document definition object will be as below :
getDocumentDefinition() { // ... return { content: [ // content definition ], info: { title: this.resume.name + '_RESUME', author: this.resume.name, subject: 'RESUME', keywords: 'RESUME, ONLINE RESUME', }, // style definition }; }
<nav class="navbar navbar-expand navbar-light bg-primary d-flex justify-content-between"> <div class="navbar-brand mb-0 h1 text-white">ONLINE RESUME BUILDER</div> </nav> <div class="container-fluid"> <form #resumeForm="ngForm"> <div class="row"> <div class="col-md-8"> <div class="shadow-sm card"> <div class="card-body"> <h4 class="card-title d-flex align-items-center"> <i class="material-icons"> account_circle </i> Personal Details</h4> <div class="row"> <div class="col-md-6"> <div class="form-group"> <input type="text" class="form-control" [(ngModel)]="resume.name" name="name" placeholder="Name" #Name="ngModel" required [ngClass]="{'is-invalid': Name.invalid && (Name.touched || resumeForm.submitted) }"> </div> <div class="form-group"> <div class="form-group"> <textarea class="form-control" [(ngModel)]="resume.address" name="address" rows="3" placeholder="Address" #Address="ngModel" required [ngClass]="{'is-invalid': Address.invalid && (Address.touched || resumeForm.submitted) }"></textarea> </div> </div> </div> <div class="col-md-6"> <div class="form-group"> <input type="text" class="form-control" [(ngModel)]="resume.contactNo" name="contactNo" placeholder="Contact No." #ContactNo="ngModel" required [ngClass]="{'is-invalid': ContactNo.invalid && (ContactNo.touched || resumeForm.submitted) }"> </div> <div class="form-group"> <input type="text" class="form-control" [(ngModel)]="resume.email" name="emailId" placeholder="Email ID" #Email="ngModel" required [ngClass]="{'is-invalid': Email.invalid && (Email.touched || resumeForm.submitted) }"> </div> <div class="form-group"> <input type="text" class="form-control" [(ngModel)]="resume.socialProfile" name="socialProfile" placeholder="Social Profile "> </div> </div> </div> </div> </div> <!-- Skills --> <div class="shadow-sm card"> <div class="card-body"> <div class="d-flex justify-content-between card-title"> <h4 class="d-flex align-items-center"> <i class="material-icons"> timeline </i> Skills</h4> <button class="btn btn-primary" (click)="addSkill()">+</button> </div> <div class="row"> <div class="col-md-4" *ngFor="let s of resume.skills; let i=index"> <div class="form-group"> <input type="text" class="form-control" name="skill{{i}}" [(ngModel)]="s.value" placeholder="e.g. Java / Angular / .Net " #Skill="ngModel" required [ngClass]="{'is-invalid': Skill.invalid && (Skill.touched || resumeForm.submitted) }"> </div> </div> </div> </div> </div> <!-- Experience --> <div class="shadow-sm card"> <div class="card-body"> <div class="d-flex justify-content-between card-title"> <h4 class="d-flex align-items-center"> <i class="material-icons"> timeline </i> Experience</h4> <button class="btn btn-primary" (click)="addExperience()">+</button> </div> <div class="row" *ngFor="let ex of resume.experiences; let i=index"> <div class="col-md-6"> <div class="form-group"> <input type="text" class="form-control" name="employer{{i}}" [(ngModel)]="ex.employer" placeholder="Employer" #Employer="ngModel" required [ngClass]="{'is-invalid': Employer.invalid && (Employer.touched || resumeForm.submitted) }"> </div> <div class="form-group"> <div class="form-group"> <textarea class="form-control" name="jobDescription{{i}}" [(ngModel)]="ex.jobDescription" rows="3" placeholder="Job Description"></textarea> </div> </div> </div> <div class="col-md-6"> <div class="form-group"> <input type="text" class="form-control" name="jobTitle{{i}}" [(ngModel)]="ex.jobTitle" placeholder="Job Title" #JobTitle="ngModel" required [ngClass]="{'is-invalid': JobTitle.invalid && (JobTitle.touched || resumeForm.submitted) }"> </div> <div class="form-group"> <input type="number" class="form-control" name="exInMonths{{i}}" [(ngModel)]="ex.experience" placeholder="Experience in months" #Experience="ngModel" required [ngClass]="{'is-invalid': Experience.invalid && (Experience.touched || resumeForm.submitted) }"> </div> </div> </div> </div> </div> <!-- Education --> <div class="shadow-sm card"> <div class="card-body"> <div class="d-flex justify-content-between card-title"> <h4 class="d-flex align-items-center"> <i class="material-icons"> school </i> Education</h4> <button class="btn btn-primary" (click)="addEducation()">+</button> </div> <div class="row" *ngFor="let ed of resume.educations; let i=index"> <div class="col-md-3"> <div class="form-group"> <select class="form-control" placeholder="Degree" name="degree{{i}}" [(ngModel)]="ed.degree" #Degree="ngModel" required [ngClass]="{'is-invalid': Degree.invalid && (Degree.touched || resumeForm.submitted) }"> <option [value]="d" *ngFor="let d of degrees">{{d}}</option> </select> </div> </div> <div class="col-md-3"> <div class="form-group"> <input type="text" class="form-control" name="college{{i}}" [(ngModel)]="ed.college" placeholder="School/College" #College="ngModel" required [ngClass]="{'is-invalid': College.invalid && (College.touched || resumeForm.submitted) }"> </div> </div> <div class="col-md-3"> <div class="form-group"> <input type="number" class="form-control" name="passingYear{{i}}" [(ngModel)]="ed.passingYear" placeholder="Passing Year" #PassingYear="ngModel" required [ngClass]="{'is-invalid': PassingYear.invalid && (PassingYear.touched || resumeForm.submitted) }"> </div> </div> <div class="col-md-3"> <div class="form-group"> <input type="number" class="form-control" name="result{{i}}" [(ngModel)]="ed.percentage" placeholder="Percentage" #Percentage="ngModel" required [ngClass]="{'is-invalid': Percentage.invalid && (Percentage.touched || resumeForm.submitted) }"> </div> </div> </div> </div> </div> <!-- Other Details --> <div class="shadow-sm card"> <div class="card-body"> <h4 class="card-title d-flex align-items-center"> <i class="material-icons"> list </i>Other Details</h4> <div class="row"> <div class="col-md-12"> <div class="form-group"> <textarea type="text" class="form-control" [(ngModel)]="resume.otherDetails" name="otherDetails" rows="4"></textarea> </div> </div> </div> </div> </div> </div> <div class="col-md-4"> <div class="shadow-sm card action-buttons"> <button (click)="resumeForm.valid ? generatePdf('open') : ''" class="btn btn-primary d-flex align-items-center justify-content-center"> <i class="material-icons"> picture_as_pdf </i> <span>Open PDF</span></button> <button (click)="resumeForm.valid ? generatePdf('download') : ''" class="btn btn-primary d-flex align-items-center justify-content-center"> <i class="material-icons"> cloud_download </i><span>Download PDF</span></button> <button (click)="resumeForm.valid ? generatePdf('print') : ''" class="btn btn-primary d-flex align-items-center justify-content-center"> <i class="material-icons"> print </i><span>Print PDF</span></button> <button type='reset' (click)="resetForm()" class="btn btn-primary d-flex align-items-center justify-content-center"> <i class="material-icons"> clear </i><span>Reset</span></button> </div> <div class="card p-4"> <div class="form-group"> <label class="h4 mb-3" for="">Show your picture in Resume</label> <input type="file" class="form-control-file" (change)="fileChanged($event)" aria-describedby="fileHelpId"> </div> <img *ngIf="resume.profilePic" [src]="resume.profilePic" class="img-thumbnail"> </div> </div> </div> </form> </div>
import { Component } from '@angular/core'; import pdfMake from 'pdfmake/build/pdfmake'; import pdfFonts from 'pdfmake/build/vfs_fonts'; import { Resume, Experience, Education, Skill } from './resume'; pdfMake.vfs = pdfFonts.pdfMake.vfs; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { resume = new Resume(); degrees = ['B.E.', 'M.E.', 'B.Com', 'M.Com']; constructor() { this.resume = JSON.parse(sessionStorage.getItem('resume')) || new Resume(); if (!this.resume.experiences || this.resume.experiences.length === 0) { this.resume.experiences = []; this.resume.experiences.push(new Experience()); } if (!this.resume.educations || this.resume.educations.length === 0) { this.resume.educations = []; this.resume.educations.push(new Education()); } if (!this.resume.skills || this.resume.skills.length === 0) { this.resume.skills = []; this.resume.skills.push(new Skill()); } } addExperience() { this.resume.experiences.push(new Experience()); } addEducation() { this.resume.educations.push(new Education()); } generatePdf(action = 'open') { const documentDefinition = this.getDocumentDefinition(); switch (action) { case 'open': pdfMake.createPdf(documentDefinition).open(); break; case 'print': pdfMake.createPdf(documentDefinition).print(); break; case 'download': pdfMake.createPdf(documentDefinition).download(); break; default: pdfMake.createPdf(documentDefinition).open(); break; } } resetForm() { this.resume = new Resume(); } getDocumentDefinition() { sessionStorage.setItem('resume', JSON.stringify(this.resume)); return { content: [ { text: 'RESUME', bold: true, fontSize: 20, alignment: 'center', margin: [0, 0, 0, 20] }, { columns: [ [{ text: this.resume.name, style: 'name' }, { text: this.resume.address }, { text: 'Email : ' + this.resume.email, }, { text: 'Contant No : ' + this.resume.contactNo, }, { text: 'GitHub: ' + this.resume.socialProfile, link: this.resume.socialProfile, color: 'blue', } ], [ this.getProfilePicObject() ] ] }, { text: 'Skills', style: 'header' }, { columns : [ { ul : [ ...this.resume.skills.filter((value, index) => index % 3 === 0).map(s => s.value) ] }, { ul : [ ...this.resume.skills.filter((value, index) => index % 3 === 1).map(s => s.value) ] }, { ul : [ ...this.resume.skills.filter((value, index) => index % 3 === 2).map(s => s.value) ] } ] }, { text: 'Experience', style: 'header' }, this.getExperienceObject(this.resume.experiences), { text: 'Education', style: 'header' }, this.getEducationObject(this.resume.educations), { text: 'Other Details', style: 'header' }, { text: this.resume.otherDetails }, { text: 'Signature', style: 'sign' }, { columns : [ { qr: this.resume.name + ', Contact No : ' + this.resume.contactNo, fit : 100 }, { text: `(${this.resume.name})`, alignment: 'right', } ] } ], info: { title: this.resume.name + '_RESUME', author: this.resume.name, subject: 'RESUME', keywords: 'RESUME, ONLINE RESUME', }, styles: { header: { fontSize: 18, bold: true, margin: [0, 20, 0, 10], decoration: 'underline' }, name: { fontSize: 16, bold: true }, jobTitle: { fontSize: 14, bold: true, italics: true }, sign: { margin: [0, 50, 0, 10], alignment: 'right', italics: true }, tableHeader: { bold: true, } } }; } getExperienceObject(experiences: Experience[]) { const exs = []; experiences.forEach(experience => { exs.push( [{ columns: [ [{ text: experience.jobTitle, style: 'jobTitle' }, { text: experience.employer, }, { text: experience.jobDescription, }], { text: 'Experience : ' + experience.experience + ' Months', alignment: 'right' } ] }] ); }); return { table: { widths: ['*'], body: [ ...exs ] } }; } getEducationObject(educations: Education[]) { return { table: { widths: ['*', '*', '*', '*'], body: [ [{ text: 'Degree', style: 'tableHeader' }, { text: 'College', style: 'tableHeader' }, { text: 'Passing Year', style: 'tableHeader' }, { text: 'Result', style: 'tableHeader' }, ], ...educations.map(ed => { return [ed.degree, ed.college, ed.passingYear, ed.percentage]; }) ] } }; } getProfilePicObject() { if (this.resume.profilePic) { return { image: this.resume.profilePic , width: 75, alignment : 'right' }; } return null; } fileChanged(e) { const file = e.target.files[0]; this.getBase64(file); } getBase64(file) { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { console.log(reader.result); this.resume.profilePic = reader.result as string; }; reader.onerror = (error) => { console.log('Error: ', error); }; } addSkill() { this.resume.skills.push(new Skill()); } }
.row{ padding: 10px; } .card{ margin: 10px; } .material-icons{ font-size: 30px; } button span{ font-size: 22px; } .action-buttons button{ margin : 5px; }
export class Resume { profilePic: string; name: string; address: string; contactNo: number; email: string; socialProfile: string; experiences: Experience[] = []; educations: Education[] = []; otherDetails: string; skills: Skill[] = []; constructor() { this.experiences.push(new Experience()); this.educations.push(new Education()); this.skills.push(new Skill()); } } export class Experience { employer: string; jobTitle: string; jobDescription: string; startDate: string; experience: number; } export class Education { degree: string; college: string; passingYear: string; percentage: number; } export class Skill { value: string; }