How to generate documents from your app

noflamine

Mitry Potato

Posted on September 9, 2019

How to generate documents from your app

Document Builder is an open-source library for generating docs. It might be useful when you need to:

  • create tons of almost similar docs with small variations.
  • fabricate docs based on huge amount of data.
  • generate user data-based docs within your  web service.

It’s simple: you write js code using methods documented here, pass it to the builder which generates the docx, xlsx, pptx or PDF for you. Or doesn’t generate, if there’s an error in your script.

Let’s see how it works on a real example.

Do you remember services where users complete forms and then get to download their ready beautifully formatted resume? Let’s bring one of those into existence using Document Builder.

In other words, my plan is to create an app capable of generating docs based on a ready template and some user data. I am going to use Node.js (Express) for it.

Work scheme:

  1. The user  completes  the  form  in browser.
  2. The data from the form is sent to the server.
  3. The script based on user data is created on the Node.js server.
  4. Node.js sends the script to the builder.
  5. The builder creates a document using  the script.
  6. Node.js sends a link to the document to the user.

Form creation

The form to be filled out by our users will have 8 fields: "Full name", "Phone number", "Email", "Profile", "Degree", "University", "Location", "Year", "skill". The skill field can be cloned.

Let’s create index.html file and add the template code to it:

<div class="fill-name">
    <input type="text" id="fill-name" placeholder="full name">
</div>
<div class="phone-number">
    <input type="number" id="phone-number" placeholder="phone number">
</div>
<div class="email">
    <input type="text" id="email" placeholder="email">
</div>
<div class="profile">
    <textarea id="profile" placeholder="Insert a brief description of yourself"></textarea>
</div>
<div class="education">
    <input type="text" id="degree" placeholder="degree">
    <input type="text" id="university" placeholder="university">
    <input type="text" id="location" placeholder="location">
    <input type="date" id="year" placeholder="year">
</div>
<div class="skills">
    <div class="skill">
        <input type="text" id="new-skill" placeholder="skill" onkeyup="add_skill_by_enter(event)">
        <button onclick="add_skill()">+</button>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Here, we use two functions add_skill_by_enter (event) and add_skill () for creating new fields when user hits the + button or Enter. I will describe them later.

We also need the button to send the data from the completed form to the server:

<button onclick="sendForm()">Send</button>
Enter fullscreen mode Exit fullscreen mode

Now it’s time to write functions for working with the form.

The first function is add_skill ():

add_skill = () => {
    const newSkill = document.getElementById("new-skill");
    if (newSkill.value === '') {return; } //  we do nothing, if nothing was inserted into the field
    const div = document.createElement("div"); .// outer  div
    const span = document.createElement("span");  //  skill name
    const button = document.createElement("button"); //  skill deletion button
    span.innerText += newSkill.value; //  for adding the inserted text to the span
    newSkill.value = ''; // resets skill name field
    newSkill.focus(); // returning the focus to the skill name field
    button.innerText += "-";
    button.onclick = () => {  // adding an action for the delete button
    div.remove();
};
div.appendChild(span); // adding span to  div
div.appendChild(button); // adding the delete button
document.getElementsByClassName('skills')[0].appendChild(div); // adding object to he page
};
add_skill_by_enter()
  add_skill_by_enter = (event) => {
        if (event.code === "Enter") { // adding an element only when  enter was pressed
            add_skill();
        }
    };
Enter fullscreen mode Exit fullscreen mode

Now we add a simple function to collect data from the fields and send them to the server.

 get_skill_values = () => {
        const skills = [];
        if (document.getElementById('new-skill').value !== '') {
            skills.push(document.getElementById('new-skill').value);
        }
        Array.from(document.getElementsByClassName('skillfield')).forEach(current_element => {
            skills.push(current_element.innerHTML);
        });
        return skills;
    };
sendForm()
    sendForm = () => {
        fetch('/', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                userData: {
                    fillName: document.getElementById('fill-name').value,
                    phoneNumber: document.getElementById('phone-number').value,
                    email: document.getElementById('email').value,
                    profile: document.getElementById('profile').value,
                    education: {
                        degree: document.getElementById('degree').value,
                        university: document.getElementById('university').value,
                        location: document.getElementById('location').value,
                        year: document.getElementById('year').value,
                    },
                    skills: get_skill_values()
                }
            })
        }).then(res => res.json())
            .then(response => {
                location.replace('/' + response.filename); // downloading the file which will be available at this link
            })
            .catch(error => console.error('Error:', error));
    };
Enter fullscreen mode Exit fullscreen mode

The server side

I write the server part using express. The libraries connection, server configuration and description of get and post methods look like this:

const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
const app = express();

app.use(bodyParser.json());

app.get('/', (req, res) => {
    res.sendFile(path.join(__dirname + '/index.html'));
});

app.post('/', (req, res) => {
    //  later we will add file building function here
});

app.listen(3000, () => console.log(`Example app listening on port ${3000}!`));
Enter fullscreen mode Exit fullscreen mode

Now we run Node.js:

node main.js

and open the address in a browser:

http://localhost:3000

Now we see the newly created form and complete it:

Alt Text

We get the following json:

{"userData":{"fillName":"Uzumaki Naruto","phoneNumber":"09879879898","email":"uzumakinaruto@konohagaku","profile":"Hero of the Hidden Leaf\nSeventh Hokage","country":"Land of Fire","city":"Hidden Leaf Village","education":{"degree":"Ninja","university":"Ninja Academy","location":"Hidden Leaf Village","year":"2015"},"skills":["Shurikenjutsu","Shadow Clone Technique","Rasengan"]}};
Enter fullscreen mode Exit fullscreen mode

Script for builder

Now we need to write a script for the builder. I took this resume template:

Alt Text

There are several ways of doing this, and the simplest is to download the desktop version of ONLYOFFICE editors and write a macro that can generate a doc using the data.

After this, we’ll need to add file creation and file saving to the macro code. This is how you get a script for the builder. This will work because ONLYOFFICE js-based macros and the builder share the same API. See?

Alt Text

Let’ start with initializing the page object and adding the user data.

const Document = Api.GetDocument();
const data =  
{"userData":{"fillName":"Uzumaki Naruto","phoneNumber":"09879879898","email":"uzumakinaruto@konohagaku","profile":"Hero of the Hidden Leaf\nSeventh Hokage","country":"Land of Fire","city":"Hidden Leaf Village","education":{"degree":"Ninja","university":"Ninja Academy","location":"Hidden Leaf Village","year":"2015"},"skills":["Shurikenjutsu","Shadow Clone Technique","Rasengan"]}};
Enter fullscreen mode Exit fullscreen mode

Now we need to add the paragraph with the full user name. It is written in bold, and this paragraph has a 1.15 line spacing.

let paragraph = document.GetElement(0); //  docs always have the 1st paragraph
FullName_style = Document.CreateStyle("FullName"); // creating new style
FullName_style.GetTextPr().SetFontSize(28); // changing the font size
FullName_style.GetTextPr().SetBold(true); //  adding the bold parameter
paragraph.SetStyle(FullName_style); //  applying the newly created style to the paragraph
paragraph.SetSpacingLine(1.15 * 240, "auto"); // changing the line spacing
paragraph.AddText(data.userData.fillName);  // adding text to the paragraph
Enter fullscreen mode Exit fullscreen mode

The remaining paragraphs:

// Country and cityconst CountryCity_style = Document.CreateStyle("CountryCity");
CountryCity_style.GetTextPr().SetFontSize(20);
CountryCity_style.GetTextPr().SetCaps(true);
CountryCity_style.GetTextPr().SetBold(true);
paragraph = Api.CreateParagraph();
paragraph.AddText(data.userData.country + ', ' + data.userData.city);
paragraph.SetStyle(CountryCity_style);
paragraph.SetSpacingAfter(0);
Document.Push(paragraph);

// phone numberconst PhoneNumber_style = Document.CreateStyle("PhoneNumber");
PhoneNumber_style.GetTextPr().SetFontSize(20);
PhoneNumber_style.GetParaPr().SetSpacingAfter(0);
PhoneNumber_style.GetTextPr().SetBold(true);
paragraph = Api.CreateParagraph();
paragraph.AddText(data.userData.phoneNumber);
paragraph.SetStyle(PhoneNumber_style);
Document.Push(paragraph);

// emailconst Email_style = Document.CreateStyle("Email");
Email_style.GetTextPr().SetFontSize(18);
Email_style.GetParaPr().SetSpacingAfter(0);
Email_style.GetTextPr().SetBold(true);
paragraph = Api.CreateParagraph();
paragraph.AddText(data.userData.email);
paragraph.SetStyle(Email_style);
Document.Push(paragraph);

// SectionHeader styleconst SectionHeader = Document.CreateStyle("SectionHeader");
SectionHeader.GetTextPr().SetBold(true);
SectionHeader.GetTextPr().SetColor(247, 93, 93, false);
SectionHeader.GetTextPr().SetFontSize(28);
SectionHeader.GetParaPr().SetSpacingBefore(1.33 * 240);
SectionHeader.GetParaPr().SetSpacingLine(1 * 240, "auto");

// add header Profile:
paragraph = Api.CreateParagraph();
paragraph.AddText("Profile:")
paragraph.SetStyle(SectionHeader);
Document.Push(paragraph);

// add profile text:
paragraph = Api.CreateParagraph();
paragraph.AddText(data.userData.profile)
Document.Push(paragraph);

// add header Education:
paragraph = Api.CreateParagraph();
paragraph.AddText("Education:")
paragraph.SetStyle(SectionHeader);
Document.Push(paragraph);

// add education year:const EducationYear_style = Document.CreateStyle("EducationYear");
EducationYear_style.GetTextPr().SetColor(102, 102, 102);
EducationYear_style.GetTextPr().SetFontSize(18);
EducationYear_style.GetParaPr().SetSpacingAfter(0);
paragraph = Api.CreateParagraph();
paragraph.SetStyle(EducationYear_style);
paragraph.AddText(data.userData.education.year)
Document.Push(paragraph);

// add education university:
paragraph = Api.CreateParagraph();

run = Api.CreateRun();
run.AddText(data.userData.education.university)
run.AddText(', ')
run.AddText(data.userData.education.location)
run.SetBold(true);
paragraph.AddElement(run);
run = Api.CreateRun();
run.AddText(' – ' + data.userData.education.degree)
paragraph.AddElement(run);
Document.Push(paragraph);

// add header Skills:
paragraph = Api.CreateParagraph();
paragraph.AddText("Skills:")
paragraph.SetStyle(SectionHeader);
Document.Push(paragraph);

// add skills text:
paragraph = Api.CreateParagraph();
const skills = data.userData.skills.map(x => ' ' + x).toString();
paragraph.AddText(skills)
Document.Push(paragraph);
Enter fullscreen mode Exit fullscreen mode

By executing this script, we get the following document:

Alt Text

Now it's time to add functions for writing the script code into a file and generating a document.

The algorithm:

The script is generated -> the script is written to the file -> the file is sent to DocBuilder -> DocBuilder returns the file link to the user.

We add a connection of add-ons for working with files and running commands using Node.js, and also create a “public” folder and make it public:

const {exec} = require('child_process');
const fs = require('fs');
app.use(express.static('public'));
Enter fullscreen mode Exit fullscreen mode

The text generation function will be very simple - it will return the line with all the code for the builder + user data. It is important to add a line break at the end of each line, otherwise nothing will work.

generate_script = (data) => {
    let first_template = 'builder.CreateFile("docx");\n' +
        'const Document = Api.GetDocument();\n';
    first_template += 'const data = ' + JSON.stringify(data) + ';\n';
    first_template += 'let paragraph = Document.GetElement(0);\n' +
        'FullName_style = Document.CreateStyle("FullName");\n' +
 .... the rest of the code
    return first_template;
};
Enter fullscreen mode Exit fullscreen mode

Now we get the script to the file and send it to DocBuilder. We just need to execute documentbuilder path / script.js command using Node.js.

Let’s write a build function for this:

build = (data, res) => {
    const filename = Math.random().toString(36).substring(7) + '.docx'; // random file name 
    let script = generate_script(data);
    script += 'builder.SaveFile("docx", "' + __dirname + '/public/' + filename + '");\n' + 'builder.CloseFile();';
    fs.writeFile('public/' + filename + 'js', script, () => {
        exec('documentbuilder ' + 'public/' + filename + 'js', () => { res.send({'filename': filename }); });
    });
};
Enter fullscreen mode Exit fullscreen mode

Let’s also add build(req.body, res) method call for post request:

app.post('/', (req, res) => {
    build(req.body, res);
});
Enter fullscreen mode Exit fullscreen mode

Pronto! Here’s how you integrate DocBuilder with your app. The full source code of the example is available here.

I have some ideas on how to widen the spectrum of the problems that can be solved by implementing this tool, and I’d also appreciate you sharing the cases when document generation is needed from your experience.

Thanks!

💖 💪 🙅 🚩
noflamine
Mitry Potato

Posted on September 9, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related