How to split HTML Page in A4 size in Angular 9

Viewed 4540

I am trying to create something like in Xing the CV maker -> https://lebenslauf.com/.

I do have different Arrays of Objects. But I am not able to create the A4 pages which will render the data and if the array it is larger than one page create new page A4 and add data there. The function needs to be so if the array is larger for one size, then create a new page a4 and put the data there. In the stackblitz I have added an array and some random text and designed an A4 letter. I referred to this question and answer, but didn't help me too much. CSS to set A4 paper size.
I tried to fake pagination and to create A4 sizes but didn't work.
I looked in this code with jquery there. It works, but I am unable to compile it in Angular. https://jsfiddle.net/tm637ysp/10/ Can someone help me here ?

I have created two projects in stackblitz. Maybe they will help.
https://stackblitz.com/edit/angular-ivy-fjhpdu.
https://stackblitz.com/edit/angular-ivy-uzmdwg

I wanted to reopen this question because the approved answer from @HirenParekh it doesn't work as I wanted. The problem it is now, If the text in one Object it is very large it wont add a new page in a realtime, but only if I reload the page. I think that the code will be for add new page or edit page will be rendered only in ngOnInit. The directive that is provided to do that job, I think it is not working as excepted. Here is the stackblitz what he tried to help me. https://stackblitz.com/edit/angular-ivy-zjf8rv

This is the code I am trying to show data.

    <div style="transition: transform 0.25s ease 0s;transform: scale(1.3);transform-origin: 50% 0px 0px;backface-visibility: hidden;perspective: 1000px;display: block;margin: 0px 11.5385%;font-size:10px;width: 76.9231%;-webkit-font-smoothing: antialiased;">
<app-paginated-view [pageSize]="'A4'" *ngIf="model" class="Grid-grid-column">
  <div pageContent class="row">
    <div class="col col-lg-7">
      <h4>{{currentUser?.firstName}} {{currentUser?.lastName}}</h4>
    </div>
    <div class="col text-right">
      <input type="file" accept="image/*" (change)="readUrl($event)">
      <img [src]="url" (change)="readUrl($event)" height="128" style="cursor:  pointer">

    </div>
  </div>

  <div pageContent class="Unit-unit-unitGroup"
   *ngFor="let personalData of model.personalData; let id = index">
   <div pageContent [ngClass]="{ 'isCatActive': selectedCategory === category.PersonalData}">

   <ng-container *ngIf="selectedCategory === category.PersonalData" clickOutside (clickOutside)="removeClick()">
    <ul>
      <li class="fa fa-plus addIconTop" (click)="openDialog()"></li>
      <li class="fa fa-plus addIconBottom" (click)="openDialog()"></li>
      <li class="fa fa-trash deleteIconRight" (click)="deleteCategory(index)"></li>
      <li class="fa fa-arrow-down moveIconDown"></li>
      <li class="fa fa-arrow-up moveIconTop"></li>
    </ul>
  </ng-container>

    <div pageContent class="col-md-12" (click)="setCategory(category.PersonalData)">
      <div class="row height">
      <div  class="col-md-4 col-sm-6 text-right tLine"></div>
      <h3 class="first-template-paragraphTitle Paragraph-paragraph-title height">
        <div class="Text-text-wrapper">
          <div class="Text-Text-text">{{'category.PersonalData' | translate}}</div>
        </div>
      </h3>
    </div>
    </div>
    <div pageContent class="container-fluid">
      <ng-container>
      <app-personal-data [personalData]="personalData" [model]="model" [id]="id">
    </app-personal-data>
  </ng-container>
  </div>
  </div>
  </div>

    <!-- Career Component -->
    <ng-container *ngFor="let careers of model.careers" class="Unit-unit-unitGroup">
      <div pageContent class="col-md-12">
        <div class="row height">
        <div  class="col-md-4 col-sm-6 text-right tLine"></div>
        <h3 class="first-template-paragraphTitle Paragraph-paragraph-title height">
          <div class="Text-text-wrapper">
            <div class="Text-Text-text">{{'category.Career' | translate}}</div>
          </div>
        </h3>
      </div>
      </div>
      <div class="container-fluid" pageContent>
      <ng-container *ngFor="let careerObj of careers.subCategories; let i = index">
      <app-career [careerObj]="careerObj" [id]="i" [career]="careers" [model]="model"></app-career>
      </ng-container>
      <ng-container *ngFor="let emptyObj of careers.emptySubContents; let iEmpty = index">
        <app-empty-object [emptyObj]="emptyObj" [iEmpty]="iEmpty" [model]="model" [isFromCareer]="true"></app-empty-object>
      </ng-container>
        </div>
    </ng-container>

    <!--Education Component-->
    <ng-container *ngFor="let education of model.education" class="Unit-unit-unitGroup">
      <div pageContent [ngClass]="{ 'isCatActive': selectedCategory === category.Education}">
        <ng-container *ngIf="selectedCategory === category.Education" clickOutside (clickOutside)="removeClick()">
          <ul>
            <li class="fa fa-plus addIconTop" (click)="openDialog()"></li>
            <li class="fa fa-plus addIconBottom" (click)="openDialog()"></li>
            <li class="fa fa-trash deleteIconRight" (click)="deleteCategory(index)"></li>
            <li class="fa fa-arrow-down moveIconDown"></li>
            <li class="fa fa-arrow-up moveIconTop"></li>
          </ul>
        </ng-container>
      <div pageContent class="col-md-12" (click)="setCategory(category.Education)">
        <div class="row height">
          <div class="col-md-4 col-sm-6 text-right tLine"></div>
          <h3 class="first-template-paragraphTitle Paragraph-paragraph-title height">
            <div class="Text-text-wrapper">
              <div class="Text-Text-text">{{'category.Education' | translate}}</div>
            </div>
          </h3>
      </div>
      </div>
      <div pageContent class="container-fluid">
      <ng-container *ngFor="let educationObj of education.subCategories; let i = index" class="col-md-12">
        <app-education [educationObj]="educationObj" [id]="i" [education]="education" [model]="model"></app-education>
      </ng-container>
      </div>
      </div>
    </ng-container>


  <!-- Skills Component-->
    <ng-container *ngFor="let skills of model.skills" class="Unit-unit-unitGroup">
    <div pageContent [ngClass]="{ 'isCatActive': selectedCategory === category.Skills}">
    <ng-container clickOutside *ngIf="selectedCategory === category.Skills" (clickOutside)="removeClick()">
      <ul>
        <li class="fa fa-plus addIconTop" (click)="openDialog()"></li>
        <li class="fa fa-plus addIconBottom" (click)="openDialog()"></li>
        <li class="fa fa-trash deleteIconRight" (click)="deleteCategory(index)"></li>
        <li class="fa fa-arrow-down moveIconDown"></li>
        <li class="fa fa-arrow-up moveIconTop"></li>
      </ul>
    </ng-container>
    <div pageContent class="col-md-12" (click)="setCategory(category.Skills)">
      <div class="row height">
        <div class="col-md-4 col-sm-6 text-right tLine"></div>
        <h3 class="first-template-paragraphTitle Paragraph-paragraph-title height">
          <div class="Text-text-wrapper">
            <div class="Text-Text-text">{{'category.Skills' | translate}}</div>
          </div>
        </h3>
    </div>
    </div>
          <div pageContent class="container-fluid">
      <ng-container *ngFor="let skillObj of skills.subCategories; let i = index" class="col-md-12">
        <app-skills [skillObj]="skillObj" [id]="i" [skills]="skills" [model]="model"></app-skills>
      </ng-container>
      </div>
    </div>
  </ng-container>


</app-paginated-view>
</div>

This is the CSS

  .A4 {
  width: 595px;
  height: 842px;
  padding: 25px 25px;
  position: relative;
}

 

This is the json

{
    "personalData": [
        {
  
            "firstName": "Max",
            "lastName": "Muster",
            "email": "max.musterman@outlook.com",
            "birthday": "2020-09-25T00:00:00.000Z",
            "telephone": "0123456789",
            "job": "Freelancer",
            "country": "Germany",
            "postalCode": 12345,
            "city": "None",
            "title": 2,
            "gender": 0,
            "street": "Musterman 12",
            "state": "",
            "status": 1,
            "showBirthday": true
        }
    ],
    "skills": [
        {
            "subCategories": [
                {
                    "languages": [
                        {
                            "name": "languages.de",
                            "rate": 5
                        },
                        {
                            "name": "languages.al",
                            "rate": 1
                        },
                        {
                            "name": "languages.en",
                            "rate": 5
                        },
                        {
                            "name": "languages.fr",
                            "rate": 4
                        },
                        {
                            "name": "languages.it",
                            "rate": 4
                        }
                    ],
                    "pcKnowledge": [
                        {
                            "_id": "5f5ca07e4dba443f786ea7ae",
                            "name": "Word"
                        },
                        {
                            "_id": "5f5ca07e4dba443f786ea7af",
                            "name": "Adobe Photoshop"
                        },
                        {
                            "_id": "5f5fd46bb21df2444c39f317",
                            "name": "Test"
                        },
                        {
                            "_id": "5f5fd46bb21df2444c39f318",
                            "name": "Excel"
                        },
                        {
                            "_id": "5f5fd46bb21df2444c39f319",
                            "name": "Ja"
                        },
                        {
                            "_id": "5f72339552009b4244391972",
                            "name": "Powerpoint"
                        }
                    ],
                    "skillsOffer": [
                        {
                            "_id": "5f4a4e2d718d33092df2c327",
                            "name": "Angular"
                        },
                        {
                            "_id": "5f4a4e2d718d33092df2c327",
                            "name": "Java"
                        },
                        {
                            "_id": "5f4a4e2d718d33092df2c327",
                            "name": "Typescript"
                        },
                        {
                            "_id": "5f4a4e2d718d33092df2c327",
                            "name": "html"
                        },
                        {
                            "name": "Javascript"
                        }
                    ],
                    "driveLicenses": [
                        {
                            "_id": "5f5ca07e4dba443f786ea7ac",
                            "name": "B"
                        },
                        {
                            "_id": "5f5ca07e4dba443f786ea7ad",
                            "name": "C"
                        },
                        {
                            "_id": "5f5f204faa5d0205180bd581",
                            "name": "B"
                        }
                    ],
                    "name": "",
                    "qualifications": ""
                }
            ]
        }
    ]
}

And this is the Paginated view class for adding new page and splitting pages

<!-- display: none style will any child that does not have #pageContent local variable defined -->
<div class="content-wrapper" #contentWrapper style="display: block">
</div>
<div class="paginated-view" #paginatedView>

</div>

export class PaginatedViewComponent implements AfterViewInit {
  @Input() pageSize: "A3" | "A4" = "A4";

  @ViewChild("paginatedView") paginatedView: ElementRef<HTMLDivElement>;

  @ViewChild("contentWrapper") contentWrapper: ElementRef<HTMLDivElement>;

  @ContentChildren(PageContentDirective, { read: ElementRef })
  elements: QueryList<ElementRef>;

  constructor(private changeDetector: ChangeDetectorRef ) {}

  ngAfterViewInit(): void {
    this.updatePages();

    // when ever childs updated call the updatePagesfunction
    this.elements.changes.subscribe((el) => {
      this.updatePages();
    });
  }


  updatePages(): void {
    // clear paginated view
    this.paginatedView.nativeElement.innerHTML = "";

    // get a new page and add it to the paginated view
    let page = this.getNewPage();
    this.paginatedView.nativeElement.appendChild(page);

    let lastEl: HTMLElement;
    // add content childrens to the page one by one
    this.elements.forEach((elRef) => {
      const el = elRef.nativeElement;

      // if the content child height is larger than the size of the page
      // then do not add it to the page

      if (el.clientHeight > page.clientHeight) {
        return;
      }
      // add the child to the page
      page.appendChild(el);

      // after adding the child if the page scroll hight becomes larger than the page height
      // then get a new page and append the child to the  new page
      if (page.scrollHeight > page.clientHeight) {
        page = this.getNewPage();
        this.paginatedView.nativeElement.appendChild(page);
        page.appendChild(el);
      }
      lastEl = el;
    });
    this.changeDetector.detectChanges();

    // bring the element in to view port
   // lastEl.scrollIntoView({ behavior: "smooth", block: "nearest" });
  }

  getNewPage(): HTMLDivElement {
    const page = document.createElement("div");
    page.classList.add("page");
    page.classList.add(this.pageSize);
    return page;
  }
}
    @Directive({
  // tslint:disable-next-line: directive-selector
  selector: "[pageContent]"
})
export class PageContentDirective {

}
7 Answers

It is all about dividing the given content to fit into the give page size.

We can create a component that will handle the dividing functionality for us. here is a StackBlitz Demo.

And here is a brief explanation.

Use the ContentChildren decorator to observe the change in content. every time the content changes we will run the page creation logic.

import {
  AfterContentInit,
  AfterViewInit,
  Component,
  ContentChildren,
  ElementRef,
  Input,
  OnInit,
  QueryList,
  ViewChild
} from "@angular/core";

@Component({
  selector: "app-paginated-view",
  templateUrl: "paginated-view.component.html",
  styleUrls: ["paginated-view.component.scss"]
})
export class PaginatedViewComponent implements AfterViewInit {
  @Input() pageSize: "A3" | "A4" = "A4";

  @ViewChild("paginatedView") paginatedView: ElementRef<HTMLDivElement>;

  @ViewChild("contentWrapper") contentWrapper: ElementRef<HTMLDivElement>;

  @ContentChildren("pageContent", { read: ElementRef }) elements: QueryList<
    ElementRef
  >;

  constructor() {}

  ngAfterViewInit(): void {
    this.updatePages();

    // when ever childs updated call the updatePagesfunction
    this.elements.changes.subscribe(el => {
      this.updatePages();
    });
  }

  updatePages(): void {
    // clear paginated view
    this.paginatedView.nativeElement.innerHTML = "";

    // get a new page and add it to the paginated view
    let page = this.getNewPage();
    this.paginatedView.nativeElement.appendChild(page);

    let lastEl: HTMLElement;
    // add content childrens to the page one by one
    this.elements.forEach(elRef => {
      const el = elRef.nativeElement;

      // if the content child height is larger than the size of the page
      // then do not add it to the page
      if (el.clientHeight > page.clientHeight) {
        return;
      }
      // add the child to the page
      page.appendChild(el);

      // after adding the child if the page scroll hight becomes larger than the page height
      // then get a new page and append the child to the  new page
      if (page.scrollHeight > page.clientHeight) {
        page = this.getNewPage();
        this.paginatedView.nativeElement.appendChild(page);
        page.appendChild(el);
      }
      lastEl = el;
    });

    //bring the element in to view port
    lastEl.scrollIntoView({ behavior: "smooth", block: "nearest" });
  }

  getNewPage(): HTMLDivElement {
    const page = document.createElement("div");
    page.classList.add("page");
    page.classList.add(this.pageSize);
    return page;
  }
}

We can use this component in an application like this.

<app-paginated-view [pageSize]="'A4'">
    <h1 #pageContent>Hello World!!</h1>
    <p #pageContent>This content will be displayed in an A4 size page</p>
</app-paginated-view>

We have to provide the template variable #pageContent so that we can select them using @ContentChildren in our PaginatedViewComponent.

Note that we are using native dom APIs here to change the dom structure. it will only move the dom node from one place to another so if you have any event listener added or have any property binding to the content children they will work as it is.

Edit: I have also updated your stackblitz https://stackblitz.com/edit/angular-ivy-zjf8rv

enter image description here In app.component.html file:

<div class="page" *ngFor="let page of pages; index as i"
  [style.height]="sizePage.height + 'cm'"
  [style.width]="sizePage.width + 'cm'"
  (click)="clickPage(i)">
  <div class="content" 
    [style.paddingTop]="paddingPage.top + 'cm'"
    [style.paddingRight]="paddingPage.right + 'cm'"
    [style.paddingBottom]="paddingPage.bottom + 'cm'"
    [style.paddingLeft]="paddingPage.left + 'cm'"  
    [id]="'content-' + i" contenteditable="true"
    (input)="inputContent($event['data'], i)">
  </div>
</div>

In app.component.css file:

.page {
    background: white;
    display: block;
    margin: 40px auto;
    box-shadow: 0 0 0.5cm rgba(0, 0, 0, 0.5);
    box-sizing: border-box;
}
.page .content {
    overflow: auto;
    outline: 0;
}

In app.component.ts file:

import { AfterViewChecked, Component } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent implements AfterViewChecked {
  
  sizePage = {
    width: 21, //cm
    height: 29.7 //cm
  }
  paddingPage = {
    top: 2, //cm
    right: 2, //cm
    bottom: 2, //cm
    left: 2 //cm
  }
  pages = [
    {
      htmlContent: null,
      full: false
    },
  ]
  currentPage = 0;
  currentChar = null;

  runAfterViewChecked = false;
  
  clickPage(i) {
    this.currentPage = i;
  }

  inputContent(char, i) {
    var element = document.getElementById('content-' + i)
    var heightContent = element.offsetHeight * 2.54 / 96; // Convert pixels to cm
    this.pages[i].htmlContent = element.innerHTML;
    console.log(this.pages);
    if (Number(heightContent.toFixed(1)) > this.sizePage.height) { 
      this.currentChar = char;
      this.pages[i].full = true;
      if (!this.pages[i + 1]) {
        this.pages.push({
          htmlContent: null,
          full: false
        })
      }
      this.currentPage = i + 1;
      this.runAfterViewChecked = true;
    }
  }

  ngAfterViewChecked() {
    document.getElementById('content-' + this.currentPage).focus();
    if (this.runAfterViewChecked) {
      if (this.currentChar) {
        var str = this.pages[this.currentPage-1].htmlContent;
        var indexLastCloseDiv = str.lastIndexOf("</div>");
        var indexLastBr = str.lastIndexOf("<br>");
        var lastChar = str[indexLastCloseDiv-1];
        if (indexLastBr != -1 && (indexLastBr + 4) == indexLastCloseDiv)
          lastChar = ' ';

        if (indexLastCloseDiv != -1)
          str = str.slice(0, indexLastCloseDiv-1) + str.slice(indexLastCloseDiv);
        else 
          str = str.slice(0, str.length - 1);
        this.pages[this.currentPage-1].htmlContent = str;

        if (this.pages[this.currentPage].htmlContent)
          this.pages[this.currentPage].htmlContent = lastChar + this.pages[this.currentPage].htmlContent;
        else
          this.pages[this.currentPage].htmlContent = lastChar;
      }

      var element = null;
      for (let i = 0; i < this.pages.length; i++) {
        element = document.getElementById('content-' + i);
        element.innerHTML = this.pages[i].htmlContent;
      }
      this.runAfterViewChecked = false;
    }
  }
}

Link to Stackblitz

Here is a simple example. There are a few mistakes, please give your suggestions for further development.

Some functions such as Backspace, Delete, Scale Page, ... have not been processed.

If you want to creat html A4 like office word A4 you must use these size:

body{
 width: 21cm ;
 height: 29.7cm;
 margin:30mm 45mm 30mm 45mm;}

Actually I cant see any js or css by you that attempts to solve the issue.

But looking at the provided example from xing, they are using static pixel width/height

width: 595px;
height: 842px;

this matches a PPI (pixel per inch) of 72 for the given Format A4 thats the only important thing to take in mind.

Knowing that, you can just check if that height is exceeded while editing and manipulate the DOM accordingly ("create new Page", "split/move elements or parts", etc. ...) You should have everything now to solve yourself. But expect that there is a lot of time going into this, especially regarding keeping font sizes / rendering/printring matching and not to talk about the mobile version ;)

Good luck

It would be fairly easy to force the web browser to display the page with the same pixel dimensions as A4. However, there may be a few quirks when things are rendered.

Assuming your monitors display 72 dpi, you could add something like this:

<!DOCTYPE html>
<html>
  <head>
    <style>
    body {
        height: 842px;
        width: 595px;
        /* to centre page on screen*/
        margin-left: auto;
        margin-right: auto;
    }
    </style>
  </head>
  <body>
  </body>
</html>

Here an example in your code with printable A4 size: https://stackblitz.com/edit/angular-ivy-fjhpdu?embed=1&file=src/app/app.component.html

enter image description here

In app.component.html file:

<div class="container" id="container"></div>
<button type="button" class="buttonAdd" (click)="addBlock()">Add Block</button>

In app.component.scss file:

.buttonAdd {
    position: fixed;
    display: inline-block;
    font-weight: 400;
    text-align: center;
    white-space: nowrap;
    vertical-align: middle;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    border: 1px solid transparent;
    transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
    color: #fff;
    background-color: #007bff;
    border-color: #007bff;
    padding: .25rem .5rem;
    font-size: .875rem;
    line-height: 1.5;
    border-radius: .2rem;
    top: 0;
}
.container {
    height: 100%;
    width: 100%;
}
.page {
    // background: white;
    display: block;
    margin: 40px auto;
    box-shadow: 0 0 0.5cm rgba(0, 0, 0, 0.5);
    box-sizing: border-box;
    .content {
        overflow: auto;
        outline: 0;
        .block {
            border: 1px solid rgba(0,0,0,0);
            padding: .25rem;
            cursor: default;
            &:hover {
                border: 1px solid #dee2e6;
                border-radius: .25rem;
            }
            .title {
                font-weight: bold;
            }
            .value {
                cursor: text;
            }
        }
    }
}

In app.component.ts file:

import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'], 
  encapsulation: ViewEncapsulation.None,
})
export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
  sizePage = {
    width: 21, //cm
    height: 29.7 //cm
  }
  paddingPage = {
    top: 2, //cm
    right: 2, //cm
    bottom: 2, //cm
    left: 2 //cm
  }

  data = [
    {
      title: "Name_1",
      value: "Thomas K.Wilson"
    }, {
      title: "Email_1",
      value: "thomas.k.wilson@gmail.com"
    }, {
      title: "Telephone_1",
      value: "0123 456 789"
    }, {
      title: "Job_1",
      value: "Teacher"
    }, {
      title: "Name_2",
      value: "Thomas K.Wilson"
    }, {
      title: "Email_2",
      value: "thomas.k.wilson@gmail.com"
    }, {
      title: "Telephone_2",
      value: "0123 456 789"
    }, {
      title: "Job_2",
      value: "Teacher"
    }, {
      title: "Name_3",
      value: "Thomas K.Wilson"
    }, {
      title: "Email_3",
      value: "thomas.k.wilson@gmail.com"
    }, {
      title: "Telephone_3",
      value: "0123 456 789"
    }, {
      title: "Job_3",
      value: "Teacher"
    }, {
      title: "Name_4",
      value: "Thomas K.Wilson"
    }, {
      title: "Email_4",
      value: "thomas.k.wilson@gmail.com"
    }, {
      title: "Telephone_4",
      value: "0123 456 789"
    }, {
      title: "Job_4",
      value: "Teacher"
    }, {
      title: "Name_5",
      value: "Thomas K.Wilson"
    }, {
      title: "Email_5",
      value: "thomas.k.wilson@gmail.com"
    }, {
      title: "Telephone_5",
      value: "0123 456 789"
    }, {
      title: "Job_5",
      value: "Teacher"
    }, {
      title: "Name_6",
      value: "Thomas K.Wilson"
    }, {
      title: "Email_6",
      value: "thomas.k.wilson@gmail.com"
    }, {
      title: "Telephone_6",
      value: "0123 456 789"
    }, {
      title: "Job_6",
      value: "Teacher"
    }, {
      title: "Name_7",
      value: "Thomas K.Wilson"
    }, {
      title: "Email_7",
      value: "thomas.k.wilson@gmail.com"
    }, {
      title: "Telephone_7",
      value: "0123 456 789"
    }, {
      title: "Job_7",
      value: "Teacher"
    }, {
      title: "Name_8",
      value: "Thomas K.Wilson"
    }, {
      title: "Email_8",
      value: "thomas.k.wilson@gmail.com"
    }, {
      title: "Telephone_8",
      value: "0123 456 789"
    }, {
      title: "Job_8",
      value: "Teacher"
    }, {
      title: "Name_9",
      value: "Thomas K.Wilson"
    }, {
      title: "Email_9",
      value: "thomas.k.wilson@gmail.com"
    }, {
      title: "Telephone_9",
      value: "0123 456 789"
    }, {
      title: "Job_9",
      value: "Teacher"
    }, {
      title: "Name_10",
      value: "Thomas K.Wilson"
    }, {
      title: "Email_10",
      value: "thomas.k.wilson@gmail.com"
    }, {
      title: "Telephone_10",
      value: "0123 456 789"
    }, {
      title: "Job_10",
      value: "Teacher"
    }
  ]

  heightPageWithoutPadding = this.convertCmtoPx(this.sizePage.height - (this.paddingPage.top + this.paddingPage.bottom));
  elContainer;
  anchorsBlockValue;
  pageContent = [[]]; // Ex: [[0, 1, 2, 3], [4, 5]]

  constructor (private elementRef: ElementRef){

  }
  
  ngOnInit() {
    
  }

  ngAfterViewInit() {
    this.elContainer = document.getElementById('container');
    this.elContainer.innerHTML += this.createHTMLPage(0);
    this.insertListData();
    this.anchorsBlockValue = this.elementRef.nativeElement.querySelectorAll('.block .value');
    this.anchorsBlockValue.forEach((anchor: HTMLAnchorElement) => {
      anchor.addEventListener('input', this.handleAnchorBlockValue)
    });
  }

  insertListData() {
    var html_ListBlock = "";
    var html_Block = "";
    var iPage = 0;
    var iBlock = 0;
    var elPageContent = document.getElementById('page-' + iPage + '-content');
    for (let i = 0; i < this.data.length; i++) {
      
      html_Block = this.createHTMLBlock(iPage, iBlock, this.data[i]);
      elPageContent.innerHTML = html_ListBlock + html_Block; 

      if (elPageContent.offsetHeight > this.heightPageWithoutPadding) {
        elPageContent.innerHTML = html_ListBlock;

        iPage += 1;
        this.elContainer.innerHTML += this.createHTMLPage(iPage);
        elPageContent = document.getElementById('page-' + iPage + '-content'); 
        
        this.pageContent[iPage] = [];
        this.pageContent[iPage].push(iBlock);

        html_Block = this.createHTMLBlock(iPage, iBlock, this.data[i]);
        html_ListBlock = html_Block;
        elPageContent.innerHTML = html_ListBlock;
      } else {
        this.pageContent[iPage].push(iBlock);
        
        html_ListBlock += html_Block;
        elPageContent.innerHTML = html_ListBlock;
      }
      iBlock += 1;
    }
    // nodes += `<button type="button" class="buttonAdd" (click)="addBlock()">Add Block</button>`;
  }

  handleAnchorBlockValue = (event: Event) => {
    // Prevent opening anchors the default way
    event.preventDefault();
    const anchor = event.target as HTMLAnchorElement;
    const id_anchorParentEl = anchor.parentElement.getAttribute('id'); // page-iPage-content-block-iBlock
    var iPage = Number(id_anchorParentEl.slice(id_anchorParentEl.indexOf("page-") + ("page-").length, id_anchorParentEl.indexOf("-content")));
    var iBlock = Number(id_anchorParentEl.slice(id_anchorParentEl.indexOf("block-") + ("block-").length, id_anchorParentEl.length));
    var elPageContent = anchor.parentElement.parentElement;
    if (elPageContent.offsetHeight > this.heightPageWithoutPadding) {
      if (!this.pageContent[iPage + 1]) {
        this.elContainer.innerHTML += this.createHTMLPage(iPage + 1);
        this.pageContent[iPage + 1] = [];
      }
      if (this.pageContent[iPage].length == 1) {
        // This is (Height Block == Height Content) > Height Page
        alert("To be continue ...");
      } else {
        while (iPage < this.pageContent.length) {

          var elPageContent = document.getElementById('page-' + iPage + '-content'); 
          var iLastBlock_PageContent = this.pageContent[iPage][this.pageContent[iPage].length - 1];
          var elLastBlock_PageContent = document.getElementById('page-' + iPage + '-content-block-' + iLastBlock_PageContent); 
          elLastBlock_PageContent.remove();
          this.pageContent[iPage].pop();
    
          if (!this.pageContent[iPage + 1]) {
            this.elContainer.innerHTML += this.createHTMLPage(iPage + 1);
            this.pageContent[iPage + 1] = [];
          }

          elLastBlock_PageContent.setAttribute('id', 'page-' + (iPage + 1) + '-content-block-' + iLastBlock_PageContent); 
          var elNextPageContent = document.getElementById('page-' + (iPage + 1) + '-content'); 
          elNextPageContent.innerHTML = elLastBlock_PageContent.outerHTML + elNextPageContent.innerHTML;
          this.pageContent[iPage + 1].unshift(iLastBlock_PageContent);
    
          if (elPageContent.offsetHeight <= this.heightPageWithoutPadding) {
            if (elNextPageContent.offsetHeight <= this.heightPageWithoutPadding) {
              break;
            } else {
              iPage += 1;
            }
          }
        }
      }
      this.anchorsBlockValue = this.elementRef.nativeElement.querySelectorAll('.block .value');
      this.anchorsBlockValue.forEach((anchor: HTMLAnchorElement) => {
        anchor.addEventListener('input', this.handleAnchorBlockValue)
      });
    }
  }

  addBlock() {
    alert("To be continue ...");
  }

  convertPxToCm(px) {
    return Math.round(px * 2.54/96 * 100) / 100;
  }

  convertCmtoPx(cm) {
    return Math.round(cm * 96/2.54);
  }

  createHTMLBlock(iPage, iBlock, data) {
    return `<div class="block" id="page-${iPage}-content-block-${iBlock}">
              <div class="title">${data.title}</div>
              <div class="value" contenteditable>${data.value}</div>
            </div>`;
  }

  createHTMLPage(iPage) {
    return `<div class="page" id="page-${iPage}" 
              style="
                height: ${this.sizePage.height}cm;
                width: ${this.sizePage.width}cm;
                padding-top: ${this.paddingPage.top}cm;
                padding-right: ${this.paddingPage.right}cm;
                padding-bottom: ${this.paddingPage.bottom}cm;
                padding-left: ${this.paddingPage.left}cm;
              ">
              <div class="content" id="page-${iPage}-content">
              </div>
            </div>`;
  }

  ngOnDestroy() {
    // Cleanup by removing the event listeners on destroy
    this.anchorsBlockValue.forEach((anchor: HTMLAnchorElement) => {
      anchor.removeEventListener('input', this.handleAnchorBlockValue)
    })
  }
}

In tsconfig.json file:

Add

"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true

It looks like this

{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "module": "esnext",
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "es2015",
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "es2018",
      "dom"
    ],
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true
  },
  "angularCompilerOptions": {
    "enableIvy": true,
    "fullTemplateTypeCheck": true,
    "strictInjectionParameters": true
  }
}

Link to Stackblitz

I did that after looking at Xing's CV page.

This Block and Data Type design makes it easy to separate columns for the page and add content with arbitrary headings.

You have to convert your data to my data type by adding the dataConvert function in ngOnInit ()

Because you are using the Text data type. If it were more diverse then the data type would be:

data = [
    {
      type: "Text",
      title: "Full Name",
      value: "Thomas K.Wilson"
    }, {
      type: "Image",
      title: "My Avatar",
      value: "linkImage.com"
    }
]

If you think you can do it with jquery there is no restriction to use jquery in angular

you would need to add following packages

jQuery (For jquery ofcourse) https://www.npmjs.com/package/jquery

@types/jquery (for support in typescript) https://www.npmjs.com/package/@types/jquery

Following is the how you can use in your ts file (just for example)

import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
declare var $: any;

@Component({
  selector: 'app-a4',
  templateUrl: './a4.component.html',
  styleUrls: ['./a4.component.css']
})
export class A4Component implements OnInit {
  max_pages = 5;
  page_count = 0;

  constructor() { }

  ngOnInit(): void {
    this.snipMe();
  }

   snipMe() {
    this.page_count++;
    if (this.page_count > this.max_pages) {
      return;
    }
    var long = $(this)[0].scrollHeight - Math.ceil($(this).innerHeight());
    var children = $(this).children().toArray();
    var removed = [];
    while (long > 0 && children.length > 0) {
      var child = children.pop();
      $(child).detach();
      removed.unshift(child);
      long = $(this)[0].scrollHeight - Math.ceil($(this).innerHeight());
    }
    if (removed.length > 0) {
      var a4 = $('<div class="A4"></div>');
      a4.append(removed);
      $(this).after(a4);
      this.snipMe.call(a4[0]);
    }
  }
}
Related