How to export angular components into PDF? - angular5

I am working on html to PDF export. I have multiple components in my page which I need to export to PDF.
In first component I have html table from below code
<div id="contentToConvert">
<h3>Top Ten</h3>
<p *ngIf="!this.dataAvailable">No data available for now. Please use the filter panel.</p>
<ng-container *ngIf="this.dataAvailable">
<ng-container *ngFor="let key of newData">
<app-table [data]="this.data[key]"></app-table>
</ng-container>
</ng-container>
</div>
In another component, I have button
<!-- PDF EXPORT -->
<div class="col-xs-offset-6 col-xs-6 col-sm-offset-9 col-sm-3 ta-r mt-4">
<button type="button" (click)="downloadPDF()" class="uni-button def">Export to PDF</button>
</div>
I am using following code to export to PDF.
downloadPDF() {
let data = document.getElementById("contentToConvert");
html2canvas(data).then(canvas => {
const imgWidth = 180;
const imgHeight = 160;
const contentDataURL = canvas.toDataURL('image/png');
const doc = new jsPDF('p', 'mm', 'a4');
const position = 40;
doc.addImage(contentDataURL, 'PNG', 15, position, imgWidth, imgHeight);
doc.save('testFile.pdf');
})
Using above code, I can generate PDF for the content inside contentToConvert component only. But I have multiple component in my page. Lets say I have contentToConvert1, contentToConvert2, contentToConvert3 as well.
How can I export all these to single pdf file?

Related

Vue Composition API with "is" attribute for conditional rendering

I'm working on an onboarding process that will collect a users name, location, job , etc. It needs to be one question per page but as an SPA so I currently have around 20 components to conditionally render.
Same problem as this but I've been asked change to Composition API and now I can't get this to work.
Vue - Render new component based on page count
The solution in the above was to make an array with all the page titles, a for loop and use :is to render each page as needed.
My components are named in this format: MLandingPage.vue, MFirstName.vue, etc.
I also have buttons that add or minus 1 from onboardingStep to go forward or back a step.
I have tried this:
const onboardingPages = ref(["MLandingPage", "MFirstName", 'MWelcome', 'MLastName', 'MAge' ]);
const onboardingStep = ref(0);
<template v-for="(onboardingPage, index) in onboardingPages" :key="index">
<component :is="onboardingPage" v-if="index === onboardingStep"/>
</template>
This doesn't render anything on the page and when I inspect, it just has <mlandingpage></mlandingpage> with no content.
I tried this instead:
const onboardingPages = ref(["m-landing-page", "m-first-name", 'm-welcome', 'm-last-name', 'm-age' ]);
Still nothing and I get this when I inspect the page: <m-landing-page></m-landing-page>
As a test, if I just write <m-landing-page></m-landing-page> in the code, it works.
Totally new to Composition API and the "is" attribute so any help would be great.
Edit - added more code for context:
<script setup>
import MLandingPage from "~~/components/onboarding/MLandingPage.vue";
import MFirstName from "~/components/onboarding/MFirstName.vue";
import MLastName from "~/components/onboarding/MLastName.vue";
import MAge from "~/components/onboarding/MAge.vue";
import { ref } from "vue";
const onboardingPages = ref(["m-landing-page", "m-first-name", 'm-welcome', 'm-last-name', 'm-age' ]);
const onboardingStep = ref(0);
function prevStep() {
onboardingStep.value -= 1;
}
function nextStep() {
onboardingStep.value += 1;
}
</script>
<template>
<div class="flex items-center justify-end gap-2 md:gap-10 mt-16 px-5 md:px-20">
<h1>Example Page Title</h1>
<div class="mt-16 p-5 pr-8 md:px-20 md:mt-24">
<template v-for="(onboardingPage, index) in onboardingPages" :key="index">
<component :is="onboardingPage" v-if="index === onboardingStep"/>
</template>
</div>
<button #click="prevStep">Back</button>
<button #click="nextStep">Next</button>
</div>
</template>
Thanks!
Found a much simpler way around this
Key issue was that I had the page names as strings
const onboardingPages = ref([MLandingPage, MFirstName, MWelcome, MLastName, MAge]);
<div class="mt-16 p-5 pr-8 md:px-20 md:mt-24">
<transition name="fade">
<component :is="onboardingPages[onboardingStep]" />
</transition>
</div>

Reference variables between components in Vue

I have a sidebar component that onclick toggles between closed and expanded, using the following functions:
const is_expanded = ref(localStorage.getItem("is_expanded") === "true")
const ToggleMenu = () => {
is_expanded.value = !is_expanded.value
// #ts-ignore
localStorage.setItem("is_expanded", is_expanded.value)
}
The value of is_expanded determines which class the component uses:
<template>
<aside :class="`${is_expanded ? 'is-expanded' : ''}`">
<div class="logo">
<img :src="logoURL" alt="Vue" />
</div>
<div class="menu-toggle-wrap">
<button class="menu-toggle" #click="ToggleMenu">
<span class="material-icons">keyboard_double_arrow_right</span>
</button>
</div>
.........
In the middle of the screen I have an iframe component which currently has a fixed margin-left so that it starts where the expanded sidebar ends, however I would like it to be dynamic and re-size when the sidebar is closed.
Is there a way I can reference the "is_expanded" variable from the sidebar component within the iframe component?

Passing props to vue instance from .blade.php file using createElement and render function

EDIT: Some more information:
I have a main-page.blade.php file, with this:
//main-page.blade.php where vue instance will mount
<div id="main-page" class=" bg-uoi-green">
<main-page :data=['one'] ></main-page>
</div>
There is also a main.js file importing the app.js file of Vue, where the instance for main-page is defined ( I have installed vue-loader and template compiler).
The app.js for Vue is:
//app.js of Vue
import Vue from 'vue'
import './assets/tailwind.css'
//
// import MainPage from './components/MainPage.vue'
// const mainpage_inst=new Vue({
// el: '#main-page',
// components: {
// MainPage,
// },
// props: ['data'],
// });
Vue.config.productionTip = false
export default {
mainpage_inst,
}
Compiling for my application was giving me exactly what was needed. But then, if I try to also check the Vue directory using vue client, I get an empty page; this was to be expected since the vue-cli does not use the full-build. But even when I changed
// import Vue from 'vue'
to
import Vue from 'vue/dist/vue.js';
I was getting a blank page when visiting localhost:8080 from vue-cli.
I was able to fix it by using the render function of vue. So now, the instance became:
import MainPage from './components/MainPage.vue'
// Main page instance
const mainpage_inst = new Vue({
render: h => h(MainPage),
}).$mount('#main-page')
But, no matter how I try to pass props as before, my main application cannot see them, in contrast to the previous writing where it was able, but vue-cli was producing a blank page.
For clarification: I have a Vue-app inside a different app that is loaded with all the required( I think) modules to read and compile .vue files. But I would also like to be able to run the Vue-app using the vue-cli.
So, is my main-app missing a module and is not able to get the props? Or is there some other way to make 'understand'?
Thanks again.
In the past, I was using the following code to create the application instance :
// const mainpage_inst=new Vue({
// el: '#main-page',
// components: {
// MainPage,
// },
// props: ['data'],
// });
and I was calling the MainPage component from a .blade.php file by passing the props:
<main-page :data=" {{ $data_to_send_as_props }}" > </main-page>
Now I want to use the following code:
import MainPage from './components/MainPage.vue'
// Main page instance
const mainpage_inst = new Vue({
render: h => h(MainPage),
}).$mount('#main-page')
but if I try to define the props as before, the application does not recognizes them. What is the correct way to write the above excerpt of code, so that props will be passed ? In other words, how and were should I add the props definition on the instance above (note also that I should implement also the change mentioned in the answer of Daniel)
Edit:
This is a simplified version of the MainPage.vue component
<template>
<div class=" h-screen w-screen md:text-2xl text:2xl " id="main-page ">
<div class=" flex flex-shrink text-4xl md:text-9xl font-bold md:ml-20 md:my-5 md:py-0 py-10 title">
A Title
</div>
<div class="flex flex-col flex-grow h-1/3 ">
<div v-for="(item,index) in slicedArray " v-bind:key="item.id" >
<div class=" flex flex-col md:flex-row md:flex-grow w-full hover:bg-uoi-green px-10 border-black border-2 transition duration-500 cursor-pointer ease-in-out bg-opacity-70 transform hover:bg-opacity-95" :class="{active: isActive === item.id}">
<div class=" date-font h-full w-1/6 md:flex-grow px-2 py-4"> {{item.date}} — </div>
<div class=" h-full w-2/3 md:flex-grow px-2 py-4 text-left"> {{item.title}} </div>
<div class=" date-font h-full w-1/6 md:flex-grow px-2 py-4 text-right"> {{item.exp_date}} </div>
</div>
<div class=" bg-white py-10 px-20" v-if="isActive === item.id">
<div > {{item.text}} </div>
<div class='file'> <a href=''> Σχετικό αρχείο </a>
<!-- <img src="../assets/curved-up-arrow-.png" class="arrow"/> -->
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name:'MainPage',
props: ['data'],
data(){
return{
isActive: null,
newsToShow: 3,
totalnews: 0,
array:[],
slicedArray:[],
}
},
mounted(){
this.newsToShow=3;
this.array=this.data;
this.totalnews = this.array.length;
// console.log(this.array)
this.slicedArray=this.array.slice(0,this.newsToShow)
},
}
</script>
<style scoped>
</style>
One of the breaking changes in Vue 3 was that the Global Vue API was changed to use an application instance. To create a vue app, use the createApp function to create the instance.
a-new-global-api-createapp
The render function (h) is not provided by the render function, so you need to import it. docs
so your instantiation should look something like this:
import { h, createApp } from 'vue'
import MainPage from './components/MainPage.vue'
// Main page instance
const mainpage_inst = new createApp({
render: () => h(MainPage),
}).$mount('#main-page')

Using vuejs with existing html

I have a list of elements, rendered server-side.
Each item has a button that changes its status.
<div class="list">
<div class="list-item" data-is-published="1" data-item-id="11">
<div class="item-link">Item 1</div>
<form method="post" class="list-item-form">
<div class="vue-mount">
<input type="submit" name="changeStatus" class="button-unpublish" value="Unpublish">
<!-- some hidden fields -->
</div>
</form>
</div>
<div class="list-item" data-is-published="1" data-item-id="12">
<div class="item-link">Item 2</div>
<form method="post" class="list-item-form">
<div class="vue-mount">
<input type="submit" name="changeStatus" class="button-unpublish" value="Unpublish">
<!-- some hidden fields -->
</div>
</form>
</div>
</div>
I'm using the following code to replace each item with a vue component
var ms = document.querySelectorAll('.vue-mount')
for (var i = 0; i < ms.length; i++) {
new Vue({
render: h => h(SubscriptionButton)
}).$mount(ms[i])
}
And on the component i get the values of data-is-published and data-item-id and set them on the component
<template>
<input
type="submit"
#click.prevent="changeStatus()"
:value="isPublished === 1 ? 'Unpublish' : 'Publish'">
</template>
<script>
export default {
....
created: function () {
const el = this.$root.$el.parentNode.parentNode
const status = el.dataset.isPublished
this.isPublished = parseInt(status)
this.itemID = el.dataset.itemID
}
}
I'm doing it this way to ensure that it works even if javascript is disabled, but the part this.$root.$el.parentNode.parentNode doesn't feel right.
Is the way I'm doing it ok? Are there better ways to achieve the same?
EDIT
I can put the data attributes on the element that vue will mount to and access them with this.$root.$el.dataset.
What I'm not sure about is how compliant to the best practices the use of this.$root.$el is.
You could use Element.closest (with a polyfill if IE support is required)
const el = this.$root.$el.closest("*[data-is-published]");
That avoids having to know the hierarchy depth of the DOM.
Info on MDN
Edited to add:
The appropriateness of using this.$root.$el depends on how you plan to structure the code. If the element you're trying to find is guaranteed to be a parent of the Vue application, then starting the search from $root is fine. If that bothers you, though, starting the search from this.$el instead would work.
The less conventional part of your approach is that it effectively creates a separate Vue application for each .vue-mount

What is the proper way to use multiple heavy same VueJS components on a page?

I am trying to create custom input as VueJS component. It will have <input type="text"> field and button. This component must implement such behavior: you can type text with autocomplete or press button which opens modal dialog with records from database and then select one. Something like this:
<div class="input-group">
<input type="text" class="form-control" placeholder="Search for...">
<span class="input-group-btn">
<button class="btn btn-default" type="button" #click="openModal">Choose</button>
</span>
</div>
The modal dialog will contain complicated logic and a lot of HTML code. I think I will put modal dialog in other component.
After all my custom input component will be used on page in table rows like:
<tr v-for="item in items">
<td><input-component :item="item"><input-component></td>
</tr>
The table may contain 10-30 rows and that is a question - should I exclude heavy modal dialog code from my custom input component or it is fine in VueJS to have such many tens duplications in DOM?
What variant should I choose:
1) exclude modal dialog, place it once in top of page and call it from custom input components
<body>
<modal-component></modal-component>
<table><tbody>
<tr v-for="item in items">
<td><input-component :item="item"><input-component></td>
</tr>
</tbody></table>
</body>
2) include modal dialog and have tens of its duplicated code in DOM
<body>
<table><tbody>
<tr v-for="item in items">
<td><input-component :item="item"><input-component></td><!--now contains <modal-component></modal-component>-->
</tr>
</tbody></table>
</body>
Use a dynamic component and bind the component type to a reactive property.
Vue.component('customer-dialog', {
template: '<p>Customer Dialog</p>'
})
Vue.component('supplier-dialog', {
template: '<p>Supplier Dialog</p>'
})
var app = new Vue({
el: '#app',
data: {
dialog: null // initial dialog to use
},
methods: {
showDialog: function(d) {
this.dialog = d
// additional dialog initialization code
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.js"></script>
<div id="app">
<component :is="dialog"></component>
<button #click="showDialog('customer-dialog')">Customers</button>
<button #click="showDialog('supplier-dialog')">Suppliers</button>
</div>
If you want to keep the switched-out dialogs in memory so that you can preserve their state or avoid re-rendering, you can wrap the dynamic component in a element.
<keep-alive><component :is="dialog"></component></keep-alive>
Just use one modal dialog for the whole page.
Fill in the dialog with relevant data from an array that will look like this
var dialogs = [
{ name: 'john', surname: 'snow' },
{ name: undefined, surname: undefined },
...
]
var currentDialog = 4
var dialogData = dialogs[currentDialog]