Part 2: Pixel perfect paginated reports

Paginated reports? Yeah!

Part 1 of this little series was an introduction to something that makes me really excited, pixel perfect reports.
After some time fighting against the way reports work on this platform (mainly via no-code GDocs/MSWord) I understanded that HTML was the way to go.
After that, I keep learning more and more about the huge benefits we get by using HTML/CSS instead of the common GDocs/MSWord template.
On this Part 2 I'm going to drive you through some of the core CSS tricks that will take your reports to a next level; I'm serious about this.
In order for this to work, I made a sample app that I posted as a resourse/tip/trick as it's own.

Book writting and publishing via AppSheet

It's rather simple app, the complex part resides on the HTML/CSS template that makes the final result, and that's the part we are going to concentrate here.
Here is a layout of the app's tables relationships:

  • Books
    • Sections
      • Chapters
        • Subthemes
          • Paragraphs

So, thanks to this basic layout, we can have multiple parent-child relationships to work with.

The goal

My goal will be to make a book/pdf where:

  • First page should have just the title and some data from it's author, as well as the date of the "printing"
  • Second page should be left blank
  • Third page should have an index
  • Then every Section is a whole page Title
  • Every Section should be on a right page
  • Every Chapter starts on a new page
  • Every subtheme is not a new page, just continues to be inside the chapter it belongs to
  • Left pages and right pages should have different margins (we want this to be printable)
  • Headers will show the section we are in (different for left and right pages)
  • Footers will show the chapter we are in as well as page number (different for left and right pages)

Some basics about HTML/CSS

HTML

Since html is on the majority of people's minds a language focused on web development, some people think that the usage of it for printed docs is crazy.
But actually it's kind of the opposite, it's at the heart of common formats like .EPUB and .MOBI, so we are not trying to use a tool for something that's not meant to be, in my mind it's a tool for digital and printed media.
The good thing for us -AppSheet users instead of web developers- is that with very basic knowledge of HTML we can make an awesome layout, since printed media is kinda simple.
We won't add forms, buttons, floating things, slides, flashing objects, etc. We just need to conquer layout stuff and images+text tags.
So, with that in mind, I'll make a list of the tags I've use to this point and that I think you need to learn to go any further. Take a look at them on https://www.w3schools.com/ for you to learn more because I won't add much info on this post about them.

  • <body>
  • <section>
  • <div>
  • <h1>, <h2>, <h3>, <h4>, <h5>, <h6> - although I don't use more than <h4>
  • <p>
  • <span>
  • <b>, <em>, <u>
  • <ul>, <ol>
    • <li>
  • <table>
    • <colgroup>
      • <col>
    • <thead>
    • <tbody>
    • <tfoot>
    • <caption>
    • <tr>
    • <th>
    • <td>
  • <img>

That's pretty much it.

CSS

This is the one that could complicate things a bit so it's also the one that I'll limit myself to say: study it.
Don't fear it, I've barely scratched the surface and I already made some good stuff. The important part is that you have to think of it as the one in charge of the styling, the colors, size, padding, margin, and basically anything that controls the looks of our template.
I'm going to repeat that, I won't give insights about this because it will just complicate things and most of it is very intuitive. Just learn as you go, try to learn my code and understand what each thing is doing. Use Goog.. Brave Search or DuckDuckGo if you have questions.

Now, to the fun part, the layout of the template

Coding the template

From your AppSheet experience you may know that a cool tool made by QREW, the AppSheet Toolbox aka QREW Tools, brings "Syntax highlighting" to the expression assistant. We need something similar here.
To code the template I use VSCode, you will need to download it. Although it's available via webapp under vscode.dev it doesn't support all of the tools we will need for this.
You need to install some extensions for syntax highlighting on your HTML and CSS code. You can search HTML on the extension tab (Ctrl+Shift+X) as well as CSS. Just install the ones with most downloads:
https://marketplace.visualstudio.com/items?itemName=abusaidm.html-snippets
https://marketplace.visualstudio.com/items?itemName=ecmel.vscode-html-css

That should be enough, let's go to the template part.

Wait, some config is needed here.

I've been talking about HTML/CSS as one thing, eventhough they are not. But you can have CSS inside of an HTML. To make things simple, just understand that we can have a .css file but we are going to make just the .html and place the CSS inside of it.

This is the basic part:

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Whatever</title>
    <style>    /* This is a comment. This is ignored but helps me to mention stuff inside
    of my code to help you understand it. Anything inside of the <style> tag
    will be considered as CSS code, as if it were a .CSS file */
    </style>
</head>
<body>
    <!-- This is another comment. This is different because this is the way comments are written on HTML.
    As you can see, we needed to use the CSS way of commenting on the style tag -->
</body>
</html>

 

We will concentrate on the <body> and <style> tags, so on future codeblocks any HTML code will be inside our <body> and any CSS code will be inside our <style> so that I can share smaller parts of the template and not the whole big mess.

The first part is a little cumbersome but it's needed to understand more later on the road. We will define inside of our code the size of the pdf, the orientation and the margins. Yes, forget about the margin config on the AppSheet task.

 

@Page {
    size: 8.5in 11in portrait;
    margin: 1in;
}

 

The size property controls (in this case): width height orientation. The margin property is giving a 1 inch to the four margins.
We also want to have different margins to left and right. Take a look:

 

@Page:left {
    margin-right: 1.5in;
}
@page:right {
    margin-left: 1.5in;
}

 

Side note: CSS is interpreted from top to bottom. The bottom-most configs overwrite the top ones. So if we have a "margin-right" property that's written after another "margin-right", the last one is the final one. In this case, we made all pages with a 1 inch margin on all sides but then we overwrite that the left page should have a right margin of 1.5 inches and the right one should have a left margin of the same size.

Also, I'll put this here now. This code is at the top of everything else (right under the @Page ones)

 

h1 {
    break-before: page;
    text-align: center;
    font-size: 2rem;
}

 

What this does is that every h1 tag is going to have a page break before it, it's going to be aligned at the center and the size of it is 2rem (this is just one of the measures that exist on CSS, generaly 1rem = 16px) https://www.sitepoint.com/understanding-and-using-rem-units-in-css/

Now -at last- let's go step by step with this thing to meet our goals mentioned earlier.

The first page

We want to cover the whole page with a centered title (right in the middle) and some data after it.
We will add a section on our body and the content of the first page will be on it, like this:

 

<section class="first-page">
    <h1 class="book-title">&lt;&lt;[BookTitle]&gt;&gt;</h1>
    <p class="book-author">&lt;&lt;[BookAuthor]&gt;&gt;</p>
    <p class="printed-at">&lt;&lt;TEXT(TODAY(), "MMMM DD, YYYY")&gt;&gt;</p>
</section>

 

Notice the &lt; and &gt; code. This is equivalent to < and > which you should be familiar with from the traditional templates. So the h1 is basically <<[BookTitle]>> writen on a way that's understandable to HTML. You can see this on the docs Using HTML templates | AppSheet Help Center

Also, you can see that on each openning tag we have an atribute called class and I add a description there. This is the way I style that specific tag using CSS. Take a look now at the CSS part to understand it better:  

 

section.first-page {
    display: flex;
    flex-direction: column;
    justify-content: center;
    height: 98vh;
    text-align: center;
    page-break-after: always;
}
h1.book-title {
    font-size: 3rem;
}

 

Don't try to understand all of it, again, Brave Search or DuckDuckGo is your friend

The second page

Remember when I told you I would left the second page blank? Well, first page is always a right one, and the index is going to be force to be on a right page as well, so a blank page at the left is added automatically.

The third one, the index

This is going to be a little bit more complicated, so we'll dig into it latter with more detail

 

<section class="index">
    <h1>Index</h1>
    <ul class="index-list">
    <li><a href="&lt;&lt;[ID]&gt;&gt;"></a></li>
    </ul>
</section>
section.index {
    break-before: right;
}
ul a::after {
    content: leader('.') target-counter(attr(href), page);
}
ul li a {
    color: black;
    text-decoration: none;
}

 

When the section of class index is added, there is a page break that makes sure it starts on a right page.

Sections

Sections (book sections, not the tags called section) will be added as a whole page divider. What I mean by this is that It'll be the section alone.
For this I'll apply a similar layout compared to the first page.

 

<section class="sections">
    <h1 class="section-title">&lt;&lt;[SectionTitle]&gt;&gt;</h1>
</section>
section.sections {
    display: flex;
    flex-direction: column;
    justify-content: center;
    height: 98vh;
    break-before: right;
}
h1.section-title {
    text-align: left;
}

 

The difference is that the text will be left aligned and there is a page break that makes sure the content is also on a right page, similar to the index one, that's it. Later we will add headers and footers and those will also be shown here.

Chapters

These ones are easy.
We already added a rule that all the h1 shoud have a page break before, so:

 

<section class="chapter">
    <h1 class="chapter-title">&lt;&lt;[ChapterTitle]&gt;&gt;</h1>
</section>

 

That's it.
Notice that I keep adding classes to my main tags, I do this so that I can change it's style after, and that style can be applied to all of the tags of the same class. This will make sense later since we are going to add some Start: expressions.

From now on, since we are talking about parent-child relationships, the code for subthemes and paragraphs will be inside of the section tag whose class is "chapter". Take a look at the following:

Subthemes

We are going to add the subthemes as h2 after the title. Your code should look like this:

 

<section class="chapter">
    <h1 class="chapter-title">&lt;&lt;[ChapterTitle]&gt;&gt;</h1>
    <!-- This h2 is on the same section as the chapter -->
    <h2 class="subtheme-title">&lt;&lt;[SubthemeTitle]&gt;&gt;</h2>
</section>

 

I kept the previous code for you to understand where the subthemes reside.

Paragraphs

These ones also are on the same section tag:

 

<section class="chapter">
    <h1 class="chapter-title">&lt;&lt;[ChapterTitle]&gt;&gt;</h1>
    <!-- This h2 is on the same section as the chapter -->
    <h2 class="subtheme-title">&lt;&lt;[SubthemeTitle]&gt;&gt;</h2>
    <!-- This p are on the same section also since they belong to this chapter -->
    <p class="paragraphs">&lt;&lt;[Paragraph]&gt;&gt;</p>
</section>

 

And we won't change it's style neither, default works fine for this purpose.

Headers, footers and beyond

This is where the most fun part comes in. All of the things mentioned above (except the blank pages added when we config page breaks as right and the index, we will customize that one latter) can be done with the traditional GDoc/MSWord template.
Sadly, what comes next is something that's not supported on the Skia backend that AppSheet uses to take our HTML and make a PDF with it.
What can we do? Well, first, we could make a feature request asking AppSheet to use PrinceXML (a service that supports full CSS) instead of Skia but, since we know how much attention those are getting lately, we should take another route and use PrinceXML directly.
This is NOT straighfoward.
Yes, I'm sorry but it's not going to be that easy and it's not our fault, as a lot of AppSheet flaws, we need to make a workaround.
At the end of this post I'm going to explain two of the easiest ones and I'm going to leave you with an idea how to implement a more "automatic" approach.

At this point, I'm going to be a little bit more technical and explain this with some detail.

Headers and footers live on something called "margin boxes"
These margin boxes exist on printed html/css and can be seen here:

1-image-margin-boxes-large-opt

There are 16 margin boxes and they behave on a very special way. We are going to use just:

  • top-left
  • top-right
  • bottom-left
  • bottom-right
  • bottom-center

An interesting thing about margin boxes is that if at the bottom there is just one box being styled, it'll take the space left by the other ones.
For example, if I'm just styling bottom-right while the bottom-left and bottom-center are not used, bottom-right will take the horizontal space of the three; corners are always intact.
This is explained this way:
Left-* and Right-* are a fixed width and variable height, while Top-* and Bottom-* are a fixed height with a variable width; except the corners.

Other things that we need to understand are "counter()" and "string()". This is how we can have a page number and also some text (string) that will change throughout the book.

The strings are the ones that will allow us to add the section title and chapter title at the header and footer, respectively. So I'll have strings for section title and chapter title, just that.

To add counters, we need to "reset" them. This just make them exist on our file. It's like an initialization.
Now comes the question, where should I reset the counter? Well, it depends.
We are going to have a section number, so this one should be initialized just once, then it'll be adding +1 everytime there is a section -book section, not section tag- (more on that later).
The way I like to do it is by reseting them before I need them. In this case, what's right before our first section? Our index. So I'll initialize our section number there. Also we are going to initialize the page counter. By default counters are reseted with a value of 0, but if we want to make our index page the first one, we are going to initialize it with 1

 

section.index {
    counter-reset: page 1 sectionnum;
}

 

Notice that after the counter "page" I added the number 1 while sectionnum has no integer after it so it's content will be 0.
If we use these counters on this page, page is 1 and sectionnum is 0.
To add +1 everytime, we need to set a "counter-increment". The page counter works by default adding 1 each page, but our sectionnum will need to add 1 everytime a section (book section) appears so we add the counter-increment there:

 

section.sections {
    display: flex;
    flex-direction: column;
    justify-content: center;
    height: 98vh;
    break-before: page;
    counter-increment: sectionnum; /* Here it is */
}

 

The same for the rest of the counters.

I'll reset the chapter counter on every section tag that holds the book section and I'll increment the chapter counter everytime a chapter appears:

 

section.sections {
    display: flex;
    flex-direction: column;
    justify-content: center;
    height: 98vh;
    break-before: page;
    counter-increment: sectionnum; /* This ones adds +1 everytime a section tag of class="sections" appears */
    counter-reset: chapternum; /* This one reset to 0 the chapter number */
}
section.chapter {
    counter-increment: chapternum; /* This one adds +1 everytime a section tag of class="chapter" appears */
}

 

Finally, I'll reset the subtheme counter on every chapter and increment it on every subtheme:

 

section.chapter {
    counter-increment: chapternum; /* This one adds +1 everytime a section tag of class="chapter" appears */
    counter-reset: subthemenum; /* This ones reset to 0 the subtheme number */
}
h2.subtheme-title {
    counter-increment: subthemenum; /* This one adds +1 everytime a header tag h2 of class="subtheme-title" appears */
}

 

We are done with the counters...


The strings.
These ones work on a similar fashion, we initialize them. The difference is that they are seted instead of reseted or incremented.
Think about them as post-it notes. We can have a post-it on a certain place that will be replaced with a different note on the same place in the future.
We are going to have two of them. One is going to have the section and the other the chapter.
Strings need content, so we are going to set them on the h1s

 

h1.section-title {
    text-align: left;
    string-set: sectiontitle content(); /* This sets the sectiontitle string with the content of the current tag */
}
h1.chapter-title {
    string-set: chaptertitle content(); /* This sets the chaptertitle string with the content of the current tag */
}

 

I added a string-set on each section that resets the chaptertitle to nothing, blank, so that it doesn't show the chapter from the previous section on it:

 

section.sections {
    display: flex;
    flex-direction: column;
    justify-content: center;
    height: 98vh;
    break-before: right;
    counter-increment: sectionnum; /* This ones adds +1 everytime a section tag of class="sections" appears */
    counter-reset: chapternum; /* This one reset to 0 the chapter number */
    string-set: chaptertitle normal;
}

 


We have counters and strings, we need to use them.

I already said that I want to put counters at the beginning of the headers and this is the way to do it:

 

h1.section-title::before {
 content: "Section " counter(sectionnum) ": ";
}

 

So, before the h1 that has the section title there is going to be a string that, on AppSheet lingo, is equivalent to:

 

CONCATENATE(
    "Section ",
    [sectionnum],
    ": "
)

 

The title comes after that to complete "Section 1: Blabla"

We do the same thing with the rest, but mixing the counters to get the 1.1 and 1.1.1:

 

h1.chapter-title::before {
    content: counter(sectionnum) "." counter(chapternum) ".  ";
}
h2.subtheme-title::before {
    content: counter(sectionnum) "." counter(chapternum) "." counter(subthemenum) ". ";
}

 

Notice that the page counter is not used yet, it's because we will add it to a margin. Strings will also go into margins, so we will add both things now.
The page counter will be on different places depending on the page. Right pages will have a page number on the right bottom and left pages the other way around.
To make different rules for different pages we already added @Page:right and @Page:left. Inside of them we need to add the margin box name this way:

 

@Page:left {
    margin-right: 1.5in;
    @bottom-left {
        content: counter(page);
    }
}
@page:right {
    margin-left: 1.5in;
    @bottom-right {
        content: counter(page);
    }
}

 

Page counter is done, now the rest:

 

@Page:left {
    margin-right: 1.5in;
    @bottom-left {
        content: counter(page); /* This one prints the page number at the bottom left */
    }
    @top-left {
        content: string(sectiontitle);
    }
    @bottom-right {
        content: string(chaptertitle);
    }
}
@page:right {
    margin-left: 1.5in;
    @bottom-right {
        content: counter(page); /* This one prints the page number at the bottom right */
    }
    @top-right {
        content: string(sectiontitle);
    }
    @bottom-left {
        content: string(chaptertitle);
    }
}

 

The last rule will be to make sure that blank pages doesn't show headers and footers, instead just a "This page is intentionally left blank" will be shown at the center of the footer. This blank pages we are talking about are the ones that were added to make sure sections are at the right.

 

@Page:blank {
    @top-right {
        content: normal;
    }
    @top-left {
        content: normal;
    }
    @bottom-left {
        content: normal;
    }
    @bottom-right {
        content: normal;
    }
    @bottom-center {
        content: "This page is intentionally left blank";
    }
}

 

Let's add content

We are basically done with the template part, but we need to add real content on it. I referenced some columns on the template as you could see, but we need to add a dynamic amount of sections, chapters, subthemes and paragraphs. This is were this particular sample app comes to play, with it's parent-child we can make use of Template Start: expressions to complete it.
The way I see "Template Start:" is as a copy-n'-paste machine. It copies whatever it's between the <<Start:>> and <<End>> and replicates it X amount of times depending on the amount of rows found inside of the expression after the :
It's quite a good thing, I think I would need to have some Javascript knowledge to replicate it's behaviour or something similar.
Another thing to notice, if you are not familiar with Start:, is that between them you can reference fields of the table you are getting the rows from. This means we also can make another Start: expression inside of the previous one.
Let's do it!

Side note: Phil told us that we need to wrap the Start:, End, If: and EndIf with a <p> tag. Since we can change the style of it, I like to hide those <p> on my template. It's something I like to do, but you could do the oposite and make them red, big, or something like that. It doesn't matter, they will go away once the pdf is made.
In my case, take into account that my <p> tag will have a class="startifend" and I made a CSS to hide it:

 

p.startifend {
    display: none;
}

 

First, we are going to wrap almost all of our template with a Start: that returns a list of sections. This is easily done with the [Related...] column:

 

<p class="startifend">&lt;&lt;Start:[Related section]&gt;&gt;</p>
<section class="sections">
    <h1 class="section-title">&lt;&lt;[SectionTitle]&gt;&gt;</h1>
</section>
<section class="chapter">
    <h1 class="chapter-title">&lt;&lt;[ChapterTitle]&gt;&gt;</h1>
    <!-- This h2 is on the same section as the chapter -->
    <h2 class="subtheme-title">&lt;&lt;[SubthemeTitle]&gt;&gt;</h2>
    <!-- This p are on the same section also since they belong to this chapter -->
    <p class="paragraphs">&lt;&lt;[Paragraph]&gt;&gt;</p>
</section>
<p class="startifend">&lt;&lt;End&gt;&gt;</p>

 

The [SectionTitle] is the only one that's on the Sections table, we need to keep adding more Start: to make it work:

 

<p class="startifend">&lt;&lt;Start:[Related sections]&gt;&gt;</p> <!-- First Start: -->
<section class="sections">
    <h1 class="section-title">&lt;&lt;[SectionTitle]&gt;&gt;</h1>
</section>
<p class="startifend">&lt;&lt;Start:[Related chapters]&gt;&gt;</p> <!-- Second Start: -->
<section class="chapter">
    <h1 class="chapter-title">&lt;&lt;[ChapterTitle]&gt;&gt;</h1>
    <!-- This h2 is on the same section as the chapter -->
    <p class="startifend">&lt;&lt;Start:[Related subthemes]&gt;&gt;</p> <!-- Third Start: -->
    <h2 class="subtheme-title">&lt;&lt;[SubthemeTitle]&gt;&gt;</h2>
    <!-- This p are on the same section also since they belong to this chapter -->
    <p class="startifend">&lt;&lt;Start:[Related paragraphs]&gt;&gt;</p> <!-- Fourth Start: -->
    <p class="paragraphs">&lt;&lt;[Paragraph]&gt;&gt;</p>
    <p class="startifend">&lt;&lt;End&gt;&gt;</p> <!-- Fourth End -->
</section>
<p class="startifend">&lt;&lt;End&gt;&gt;</p> <!-- Third End -->
<p class="startifend">&lt;&lt;End&gt;&gt;</p> <!-- Second End -->
<p class="startifend">&lt;&lt;End&gt;&gt;</p> <!-- First End -->

 

Voilá, 4 nested Start: expressions to add content to our template. It's impressive that a couple lines of code can translate to a paginated document.

The index stuff and some more

I though a lot about this. "How can I add a dynamic list of items that links to the content?" This was an actual challenge for me that leads to another question, "How can I make sure that the Start: expressions add the content in the right order?".
Well, let's diggest this thing:

  • First, to have a dynamic list we can use the same thing we did to have dynamic content, "Start:". The only difference is that we won't reference the same columns. There is a column called TagId which is used to sort the content on the app as well as make each tag unique in order to reference them in the index.
  • Second, how can we make sure that the content is sorted correctly? Well, we need to change our Start: expressions a bit to make them ordered by the TagId column by wraping the [Related] with Orderby()

The Index

I said we would need to dig into the index html code, now it's the time.
This was what I presented to you at the beginning:

 

<section class="index">
    <h1>Index</h1>
    <ul class="index-list">
    <li><a href="&lt;&lt;[ID]&gt;&gt;"></a></li>
    </ul>
</section>

 

This is actually not that useful, it's just a unordered list (ul) with one list item on it (li) which is a link to an ID (a -anchor-) and with no visual content. It was there temporarily, now to the right one:
I will share the whole code and explain later

 

<section class="index">
    <h1>Index</h1>
    <ol class="index-list">
        <p class="startifend">&lt;&lt;Start:ORDERBY([Related sections], [TagId])&gt;&gt;</p> <!-- First Start: -->
        <li><a href="#&lt;&lt;[TagId]&gt;&gt;">&lt;&lt;[SectionTitle]&gt;&gt;</a>
            <ol>
                <p class="startifend">&lt;&lt;Start:ORDERBY([Related chapters], [TagId])&gt;&gt;</p> <!-- Second Start: -->
                <li><a href="#&lt;&lt;[TagId]&gt;&gt;">&lt;&lt;[ChapterTitle]&gt;&gt;</a>
                    <ol>
                        <p class="startifend">&lt;&lt;Start:ORDERBY([Related subthemes], [TagId])&gt;&gt;</p> <!-- Third Start: -->
                        <li><a href="#&lt;&lt;[TagId]&gt;&gt;">&lt;&lt;[SubthemeTitle]&gt;&gt;</a></li>
                        <p class="startifend">&lt;&lt;End&gt;&gt;</p> <!-- Third End -->
                    </ol>
                </li>
                <p class="startifend">&lt;&lt;End&gt;&gt;</p> <!-- Second End -->
            </ol>
        </li>
        <p class="startifend">&lt;&lt;End&gt;&gt;</p> <!-- First End -->
    </ol>
</section>

 

  1. We get rid of the ul, and everything is a ol, this means that instead of dots or similar, we are going to have numbers.
  2. We make new ordered lists (<ol>) inside of the list items (<li>). This is the thing that allows us to have more ordered lists inside of a list item that's part of an ordered list, so we end with this result:

  1. Sections
    1. Chapters
      1. Subthemes

  1. We add Start: expressions to do the copy-n'-paste stuff I said before, but in this context it'll add just the following:
    1. A link to the content, this is done thanks to the anchor (<a> and it's href= property) where we will point to the same TagId we are going to add as an id= property to each of the headers.
    2. A content, which is between the <a> and </a>, in this case the titles/headers

Finally, we just need to add the same [TagId] column as and id= on the headers:

 

<p class="startifend">&lt;&lt;Start:ORDERBY([Related sections], [TagId])&gt;&gt;</p> <!-- First Start: -->
<section class="sections">
    <h1 id="&lt;&lt;[TagId]&gt;&gt;" class="section-title">&lt;&lt;[SectionTitle]&gt;&gt;</h1>
</section>
<p class="startifend">&lt;&lt;Start:ORDERBY([Related chapters], [TagId])&gt;&gt;</p> <!-- Second Start: -->
<section class="chapter">
    <h1 id="&lt;&lt;[TagId]&gt;&gt;" class="chapter-title">&lt;&lt;[ChapterTitle]&gt;&gt;</h1>
    <!-- This h2 is on the same section as the chapter -->
    <p class="startifend">&lt;&lt;Start:ORDERBY([Related subthemes], [TagId])&gt;&gt;</p> <!-- Third Start: -->
    <h2 id="&lt;&lt;[TagId]&gt;&gt;" class="subtheme-title">&lt;&lt;[SubthemeTitle]&gt;&gt;</h2>
    <!-- This p are on the same section also since they belong to this chapter -->
    <p class="startifend">&lt;&lt;Start:ORDERBY([Related paragraphs], [TagId])&gt;&gt;</p> <!-- Fourth Start: -->
    <p class="paragraphs">&lt;&lt;[Paragraph]&gt;&gt;</p>
    <p class="startifend">&lt;&lt;End&gt;&gt;</p> <!-- Fourth End -->
</section>
<p class="startifend">&lt;&lt;End&gt;&gt;</p> <!-- Third End -->
<p class="startifend">&lt;&lt;End&gt;&gt;</p> <!-- Second End -->
<p class="startifend">&lt;&lt;End&gt;&gt;</p> <!-- First End -->

 

Little side note: We actually don't need the [TagId] column. I did it just to show you the relation between the anchor on the index and each title clearlier. We could use the [_RowNumber] as our orderby column and the Key column as the index link target and the title's id. It's actually a perfect ID in this case.
Also, the platform we will use for the pdf rendering supports javascript and we could use that to make our index or table of contents, but that's not the purpose of this topic.

There it is, a working index with dynamic content, dynamic hyperlinks and a little bit more.
About that last sentence, lets break the CSS code I provided at the beginning:

 

ul a::after {
    content: leader('.') target-counter(attr(href), page);
}
ul li a {
    color: black;
    text-decoration: none;
}

 

I changed it a bit to:

 

ol a::after {
    content: leader('.') target-counter(attr(href), page);
}
ol li a {
    color: black;
    text-decoration: none;
}

 

This is because we now have ordered lists instead of unordered lists. Now, to the code:

  1. After "a" tags that are inside an ordered list, there is going to be a "leader", which just think about it as a way to repeat certain content as much as we can, spaned across our page, and, after that leader, there is going to be a target-counter that adds the page number of the href, in this case, the header we added as a link. This adds something like "...............7" after each list on our index.
  2. The "a" tags that are inside our list items "li" that's also inside another "ol" (this is to make sure we style just those specific "a" tags) will have a black font color without any text decoration. Thanks to this, it won't look like this

We are done! Almost...

I said that most of the cool stuff made through CSS is lost on the default Skia engine that AppSheet uses, and it's true, obviously.
We have two alternatives to do this, both related to PrinceXML.

1. PrinceXML on a local machine

This can start to get even more tricky so just go to this link and try it yourself:
https://www.princexml.com/download/

You can use PrinceXML on a cli for free, but don't use it for commercial purposes.
Once installed, the syntax is something like this: 

prince DOC.html -o DOC.pdf

2. PrinceXML on a server through API aka DocRaptor

DocRaptor is awesome, it's the only one I know that brings PrinceXML to an API level.
Take a look at their webpage:
https://docraptor.com/

I added this second method inside of our template. They provide an API Key for testing purposes (yeah!).
Just open the .html file from the bot with your browser, you will see it inmediatly between the book author and time stamp. Surprise!
I won't explain it, just take a look at the code and see if you can find it

Show More
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <title>Whatever</title>
    <link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Open Sans"/>
    <style>
      /* This is a comment. This is ignored but helps me to mention stuff inside of my code
        Anything inside of the style tag will be considered as CSS code, as if it were a .CSS file */
      @Page {
        size: 8.5in 11in portrait;
        margin: 1in;
      }
      @Media print {
        #pdf-button {
          display: none;
        }
      }
      @Page:left {
        margin-right: 1.5in;
        @bottom-left {
          content: counter(page); /* This one prints the page number at the bottom left */
        }
        @top-left {
          content: string(sectiontitle);
        }
        @bottom-right {
          content: string(chaptertitle);
        }
      }
      @Page:right {
        margin-left: 1.5in;
        @bottom-right {
          content: counter(page); /* This one prints the page number at the bottom right */
        }
        @top-right {
          content: string(sectiontitle);
        }
        @bottom-left {
          content: string(chaptertitle);
        }
      }
      @Page:first {
        @top-right {
          content: normal;
        }
        @top-left {
          content: normal;
        }
        @bottom-left {
          content: normal;
        }
        @bottom-right {
          content: normal;
        }
      }
      @Page:blank {
        @top-right {
          content: normal;
        }
        @top-left {
          content: normal;
        }
        @bottom-left {
          content: normal;
        }
        @bottom-right {
          content: normal;
        }
        @bottom-center {
          content: "This page is intentionally left blank";
        }
      }
      body {
        font-family: "Open Sans";
      }
      #pdf-button {
        border: 0;
        margin-top: 5px;
        padding: 10px 20px;
        font-size: 15pt;
        text-align: center;
        color: black;
        text-shadow: 1px 1px 1px black;
        border-radius: 10px;
      }
      #pdf-button:hover {
        border: 0;
        margin-top: 5px;
        padding: 10px 20px;
        font-size: 18pt;
        text-align: center;
        color: red;
        text-shadow: 1px 1px 1px black;
        border-radius: 10px;
      }
      ol a::after {
        content: leader(".") target-counter(attr(href), page);
      }
      ol li a {
        color: black;
        text-decoration: none;
      }
      h1 {
        break-before: page;
        text-align: center;
        font-size: 2rem;
      }
      section.first-page {
        display: flex;
        flex-direction: column;
        justify-content: center;
        height: 98vh;
        text-align: center;
        page-break-after: always;
      }
      h1.book-title {
        font-size: 3rem;
        page-break-before: avoid;
      }
      section.second-page {
        height: 98vh;
      }
      section.sections {
        display: flex;
        flex-direction: column;
        justify-content: center;
        height: 98vh;
        break-before: right;
        counter-increment: sectionnum; /* This ones adds +1 everytime a section tag of class="sections" appears */
        counter-reset: chapternum; /* This one reset to 0 the chapter number */
        string-set: chaptertitle normal;
      }
      h1.section-title {
        text-align: left;
        string-set: sectiontitle content(); /* This sets the sectiontitle string with the content of the current tag */
      }
      h1.section-title::before {
        content: "Section " counter(sectionnum) ": ";
      }
      section.index {
        counter-reset: page 1 sectionnum;
        break-before: right;
      }
      section.chapter {
        counter-increment: chapternum; /* This one adds +1 everytime a section tag of class="chapter" appears */
        counter-reset: subthemenum; /* This ones reset to 0 the subtheme number */
      }
      h1.chapter-title {
        string-set: chaptertitle content(); /* This sets the chaptertitle string with the content of the current tag */
      }
      h1.chapter-title::before {
        content: counter(sectionnum) "." counter(chapternum) ". ";
      }
      h2.subtheme-title {
        counter-increment: subthemenum; /* This one adds +1 everytime a header tag h2 of class="subtheme-title" appears */
      }
      h2.subtheme-title::before {
        content: counter(sectionnum) "." counter(chapternum) "."
          counter(subthemenum) ". ";
      }
      p {
        text-align: justify;
      }
      p.book-author,
      p.printed-at {
        text-align: center;
      }
      p.startifend {
        display: none;
      }
    </style>
    <script src="https://docraptor.com/docraptor-1.0.0.js"></script>
    <script>
      var downloadPDF = function () {
        DocRaptor.createAndDownloadDoc("YOUR_API_KEY_HERE", {
          test: true, // test documents are free, but watermarked
          type: "pdf",
          document_content: document.querySelector("html").innerHTML,
        });
      };
    </script>
  </head>
  <body>
    <!-- This is another comment. This is different because this is the way comments are written on HTML.
    As you can see, we needed to use the CSS way of commenting on the <style> tag -->
    <section class="first-page">
      <h1 class="book-title">&lt;&lt;[BookTitle]&gt;&gt;</h1>
      <p class="book-author">&lt;&lt;[BookAuthor]&gt;&gt;</p>
      <input
        id="pdf-button"
        type="button"
        value="Generate a cool PDF"
        onclick="downloadPDF()"
      />
      <p class="printed-at">&lt;&lt;TEXT(TODAY(), "MMMM DD, YYYY")&gt;&gt;</p>
    </section>
    <!--<section class="second-page"></section> -->
    <section class="index">
      <h1>Index</h1>
      <ol class="index-list">
        <p class="startifend">
          &lt;&lt;Start:ORDERBY([Related sections], [TagId])&gt;&gt;
        </p>
        <!-- First Start: -->
        <li>
          <a href="#&lt;&lt;[TagId]&gt;&gt;">&lt;&lt;[SectionTitle]&gt;&gt;</a>
          <ol>
            <p class="startifend">
              &lt;&lt;Start:ORDERBY([Related chapters], [TagId])&gt;&gt;
            </p>
            <!-- Second Start: -->
            <li>
              <a href="#&lt;&lt;[TagId]&gt;&gt;"
                >&lt;&lt;[ChapterTitle]&gt;&gt;</a
              >
              <ol>
                <p class="startifend">
                  &lt;&lt;Start:ORDERBY([Related subthemes], [TagId])&gt;&gt;
                </p>
                <!-- Third Start: -->
                <li>
                  <a href="#&lt;&lt;[TagId]&gt;&gt;"
                    >&lt;&lt;[SubthemeTitle]&gt;&gt;</a
                  >
                </li>
                <p class="startifend">&lt;&lt;End&gt;&gt;</p>
                <!-- Third End -->
              </ol>
            </li>
            <p class="startifend">&lt;&lt;End&gt;&gt;</p>
            <!-- Second End -->
          </ol>
        </li>
        <p class="startifend">&lt;&lt;End&gt;&gt;</p>
        <!-- First End -->
      </ol>
    </section>
    <p class="startifend">
      &lt;&lt;Start:ORDERBY([Related sections], [TagId])&gt;&gt;
    </p>
    <!-- First Start: -->
    <section class="sections">
      <h1 id="&lt;&lt;[TagId]&gt;&gt;" class="section-title">
        &lt;&lt;[SectionTitle]&gt;&gt;
      </h1>
    </section>
    <p class="startifend">
      &lt;&lt;Start:ORDERBY([Related chapters], [TagId])&gt;&gt;
    </p>
    <!-- Second Start: -->
    <section class="chapter">
      <h1 id="&lt;&lt;[TagId]&gt;&gt;" class="chapter-title">
        &lt;&lt;[ChapterTitle]&gt;&gt;
      </h1>
      <!-- This h2 is on the same section as the chapter -->
      <p class="startifend">
        &lt;&lt;Start:ORDERBY([Related subthemes], [TagId])&gt;&gt;
      </p>
      <!-- Third Start: -->
      <h2 id="&lt;&lt;[TagId]&gt;&gt;" class="subtheme-title">
        &lt;&lt;[SubthemeTitle]&gt;&gt;
      </h2>
      <!-- This p are on the same section also since they belong to this chapter -->
      <p class="startifend">
        &lt;&lt;Start:ORDERBY([Related paragraphs], [TagId])&gt;&gt;
      </p>
      <!-- Fourth Start: -->
      <p class="paragraphs">&lt;&lt;[Paragraph]&gt;&gt;</p>
      <p class="startifend">&lt;&lt;End&gt;&gt;</p>
      <!-- Fourth End -->
    </section>
    <p class="startifend">&lt;&lt;End&gt;&gt;</p>
    <!-- Third End -->
    <p class="startifend">&lt;&lt;End&gt;&gt;</p>
    <!-- Second End -->
    <p class="startifend">&lt;&lt;End&gt;&gt;</p>
    <!-- First End -->
  </body>
</html>

This is getting to it's end... this took me a couple of months to learn and a couple of days to write and I hope you were able to read it on a coupe of minutes and learned something.
I suggest you to bookmark this post, I'll do it myself since I'll need to check again some of the points I explained here.
Also, I'm not done yet with this HTML/CSS thing. I'm going to make a final Part 3 with some layout tricks and the usage of tables and similar, things that are available on the Skia engine so you can make use of them right now.

Finally, I said at some point of this post that I was going to leave you with an idea so that you can try to implement it:

Since DocRaptor is an API, I'd love if someone comes with a way to integrate it through some kind of no-code or low-code tool like Make (Integromat). I tried a lot so that you could have that but I failed. For some reason the body of the JSON POST made through Make didn't work. I tried document_content and document_url with no luck, although I was close. Experiment and tell me if you have success!

I'll keep practicing and learning. This is a whole new skill to develop so I find it funny and useful at the same time.
I'm not near the point I want to be with this, for example I don't know yet how to make sure that each section of the book is on a right page so that if it lands on a left page it should add a page break automatically before it, but time will help I guess.

If you have any ideas, share them here!

20 25 5,264
25 REPLIES 25


@SkrOYC wrote:

I'm not near the point I want to be with this, for example I don't know yet how to make sure that each section of the book is on a right page so that if it lands on a left page it should add a page break automatically before it, but time will help I guess.

If you have any ideas, share them here!


Haha, I learned it in the process of making this post and forgot to remove that last paragraph, the index/toc and sections start on a Right page

Bookmarked to study carefully later. But just wanted to drop you a BIG THANK YOU!! message. Very kind of you to compile such detailed tutorial Oscar. Appreciated!

Thanks @Joseph_Seddik

I though a lot about posting such a big topic.

Is hard to left on the table something that took you a lot of work to understand and make.

But at the same time I enjoy the Free and Open Source Software way of thinking and I though "Why not? I learned it free, I should help others free of charge also".

So, here it is, for anyone to use it

Awesome, congrats

Oscar my friend, you are truly:

one-in-a-million-steve-irwin.gif

 

A thorough post with detailed explanation for each part and useful pointers on HTML/CSS.

Will definitely take time to thoroughly understand the whole concept but the sheer book compilation idea, execution and explanation is very impressive. Thank you very much @SkrOYC .

 

Thanks @Suvrutt_Gurjar, that means a lot coming from someone like yourself.

Prepare for Part 3!

Aurelien
Google Developer Expert
Google Developer Expert

This is how I felt at reading your post:

firework.gif

I will need time to digest it, but it definitely worth it.

Huge thanks @SkrOYC !!!!!!

Thanks @Aurelien, I knew you would like this topic.

The only thing that would make this better is PrinceXML support directly from AppSheet, maybe similar to how the Scandit SDK is optional for a premium to upgrade barcode scanning.

Skia/Chromium is not suitable for this kind of docs, it's basically the same you would get by Ctrl/CMD+P on a Chromium Browser, and I don't think that they will provide support anytime soon because some of the things used in this topic have been around for more than a decade and Google has no interest or need to put them on Skia.

On Part 3 I'll add references to videos and articles for anyone to learn about HTML/CSS for printed media.

I'm also open to ideas about what to make! Part 3 will be about layout, docs like invoices and others that preferaly use one page.

If you have any need or you already have a template for something like that, let me know!

I recommend for part 3 that something about Flex and Grid be mentioned, it is a little more advanced in CSS but very useful and aesthetic, especially GRID for creating tables, I have been testing and it works when making static tables, I am experimenting with its use for when new tables need to be generated.

 

Afaik, Grids are not supported, while Flex are.

On Part 3 I'm making use of some basic Flex. It's almost done but I have to write the last part.
It's quite a large post, maybe more than this one

Have anybody managed to make a pdf from the emails' attachment?

If you haven't, here is the result.

HTML:

BookMaker HTML result after AppSheet automation process

PDF:

BookMaker PDF result after DocRaptor engine

Thank you so much @SkrOYC !

I'm pretty sure I have uses cases, but I need to understand how this all works so that I can start thinking of the possibilities and then I'll be able to post examples/questions.

Anyhow, thanks again! Definitely looking forward to more info about layout, docs like invoices and others that preferably use one page.

Thanks for such a detailed work around for producing professional attachments @SkrOYC .  You did a great job and I have to learn every part of what you made.

For starters who want to start with HTML/CSS it could be a help to go to https://www.khanacademy.org/computing/computer-programming/html-css

It is an expensive solution for 1 customer (US3,800 Dollar PrinceXML) with the extra connector for DocRaptor (US 75 Dollar/mo). When your Apps are used by a company, you have to pay the price!

If you also want to get rid of the <noreply@appsheet.com>  with Zapier it keeps adding up. 

Hope to see your part 3 that digs on deeper into the layout of an invoice.

Hi @Peter_Bernsen 

Thanks for your comments, I'm glad you liked it.

Also, the Khan Academy link is an awesome resource, thanks for that. I'll add it to the end of Part 3 and give credits to you for bringing it to our attention.

About the costs, PrinceXML is not cheap if you buy it to use it on your company's servers. Docraptor on the other hand is quite affordable.

I don't know where you find the "DocRaptor extra connector" but DocRaptor is a service that charges based on the amount of documents made through it. Also it's important to notice that they also provide document hosting and xls/xlsx support, appart from pdf:

PlanDocumentsPricing
Free5 mo$0 mo
Basic125 mo$15 mo
Professional325 mo$29 mo
Premium1,250 mo$75 mo
Max5,000 mo$149 mo
Bronze15,000 mo$399 mo
Silver40,000 mo$1000 mo
EnterpriseContact us!

leluyen1205_0-1658459164273.png

Tôi đã chạy code của bạn và thêm

background-color thì thấy các lề khi in bị như này? @SkrOYC 

Where did you add the background color? In the body tag or html one?

Holaa! Increíble el post!  solo que no entiendo, ¿Dónde entra xml en todo esto?


@datijoaquin wrote:

xml


No recuerdo haber mencionado xml en el post, quizá te refieres a PrinceXML? Es un servicio

Buen día, vengo con una consulta, cuando hago la maqueta desde la web se ve con todos los estilos pero cuando la agrego en appsheet mucho de estos estilos desaparecen. Sabrían decirme que es lo que no esta soportando appsheet de mi código CSS? Cuando lo subo y reviso tanto el monitor como el test del bot no me figura ningún error.

Dejo el repo abajo.

Desde ya muchas gracias!

https://github.com/Joaquindati/Reportesappsheet/blob/main/Worjorder

pd: Se que las funciones están comentadas, ahora me estoy centrando mas en los estilos.

Mi estilo:

Consulta.png

Estilos cuando lo subo a appsheet:

Sin título2.png

  


@datijoaquin wrote:

Estilos cuando lo subo a appsheet:


Estás usando la opción "Preview"?
Intenta generar un reporte propiamente tal para concluir si es que los estilos se pierden o no


Por otro lado, asegúrate que el archivo sea .html; no estoy seguro si esto generará o no problemas, pero puedes probarlo

Sin cuando lo visualizo con "Preview" me aparece sin estilos (2da imagen), pero me di cuenta que es que estaba agregando el archivo de una manera que me lo tomaba como un .doc 

Muchas gracias por responder! 

@SkrOYC , I was doing some research and a found the jsPDF library to generate PDF from HTML,  Did you ever try this one before instead of PrinceXML or DOCRaptor ? I am building a report but it needs pagination and header in all pages, even following the current article, I didn't get what I need.

You can read about it here.

To put it in simple terms, jsPDF is a different animal and afaik cannot be compatible with what we do here in AppSheet for the time being. jsPDF is made for the client side, not the server side.

Your best bet (the thing I'm currently working) is to send the required data from AppSheet in a JSON object to a middle service (maybe an AppScript) to then send the call to DocRaptor in order to produce the final PDF in one go.

The offline version is to produce the HTML through AppSheet and use prince, although remember that you cannot use PriceXML for free if it's for commercial purposes

Top Labels in this Space