Save Class Instance to Vue Data - javascript

I am trying to use PDFtron in one of my Vue components. I create the pdf viewer iFrame instance in the mounted hook so that upon loading the Vue, the PDF frame shows up ready to load PDFs:
mounted() {
const viewerElement = document.getElementById('viewer');
this.viewer = new PDFTron.WebViewer({
path: 'https://www.pdftron.com/4.0/lib',
l: 'apikey'
}, viewerElement);
}
I would like to save this instance so that I can call it again in a method like this:
methods: {
getPDF() {
this.viewer.loadDocument('https://pdftron.s3.amazonaws.com/downloads/pl/webviewer-demo.pdf')
}
}
In order to do this I thought I could create a viewer variable in my data variables and then save the pdftron viewer to it which is why I save the viewer to this.viewer. Unfortunately whenever I call the getPDF function, I get the following error: The viewer instance is not defined yet. I am not sure if this is the correct way to save a class instance in Vue.
The getPDF function gets called on a button like this:
<v-btn color="primary" #click="getPDF(url)" :disabled="!valid">Load PDF</v-btn>'
Update:
I updated my getPDF function to this:
getPDF() {
const viewerInstance = this.viewer.getInstance()
viewerInstance.loadDocument('https://pdftron.s3.amazonaws.com/downloads/pl/webviewer-demo.pdf')
}
but I still get same error The viewer instance is not defined yet and `Cannot read property loadDocument of undefined'

Somehow you are triggering the button click before WebViewer has loaded and initialized.
You cannot interact with WebViewer API (except the constructor), until you get the ready event.
https://www.pdftron.com/api/web/PDFTron.WebViewer.html#event:ready__anchor
See this page for an example.
https://www.pdftron.com/documentation/samples/js/viewing
I would recommend adding your button listeners in the ready event listener, so users can only use those buttons once the viewer is ready.

Related

Create svelte component on every click

I'm trying to create new instances of the same component in svelte whenever a button or other action happens on the page, without having to make a list and {each} over them.
I just want to do something like new Component(some context data) and forget.
Another concern is I don't want the component to disappear when the parent is removed.
Thank you
Just mount it straight on the document.body:
new Component({ target: document.body })
But be aware that if you do not $destroy it yourself, you will end up with a memory leak.
When I do something like this, e.g. for notifications, I dispatch an event from the component that tells the calling code when the component can safely be destroyed. Something like:
const notification = new Notification({ target: document.body, ... });
notification.$on('close', () => notification.$destroy());

Stencil: call public method from another component via the DOM

I have a Stencil sidebar component with a refresh() method. I've exposed it with #Method():
#Method() async refresh() { ... }
and I can call it from my app:
const sidebar = document.querySelector('my-sidebar');
await sidebar.refresh();
However, I also have a popup component which is generated ad-hoc by the app via a separate API, and I want to make a button in the popup trigger the sidebar's refresh(). I've set the method as a Prop on the popup:
#Prop() refresh: Function;
and I've set the Prop in the app code as a reference to the method:
popup.refresh = sidebar.refresh;
...but when I try to execute it I get an error:
Uncaught (in promise) TypeError: ref.$lazyInstance$[memberName] is not a function
How can I get the popup to see the sidebar's method?
If you look at the value of sidebar.refresh in your app when you're trying to assign that Prop, you'll see something like this:
value(...args) {
const ref = getHostRef(this);
return ref.$onInstancePromise$.then(() => ref.$lazyInstance$[memberName](...args));
}
Stencil components are lazy-loaded by generating proxies with references pointing to methods, so you're just passing a reference to a proxy. The easiest way to fix this is to make a function which explicitly calls the method, forcing its hydration. (If the source component is not already hydrated, you'll need to wait for that first by using its lifecycle methods.)
Here's what that would look like in your app:
popup.refresh = () => sidebar.refresh();
// or, passing any arguments:
popup.refresh = args => sidebar.refresh(args);
You can also do it in your popup component:
async popupRefresh() {
let sidebar = document.querySelector('my-sidebar');
await sidebar.refresh();
}
If calling the method from inside another component in this way, you may see the TypeScript error Property 'refresh' does not exist on type 'Element'.
To avoid this, import the sidebar component into your popup component:
import { mySidebar } from 'stencil';
Then, use it as the type:
let sidebar: mySidebar = document.querySelector('my-sidebar');

Use custom JavaScript code in a Vue.js app

I'm trying to insert JavaScript code in a Vue.js router app. I need to load data from the CMS the app is served from. In order to get the data from the CMS I have to use a JavaScript library from the CMS which is not made for Vue and is not exporting it's class/functions like modern JS. So I import the JS library from in the index.html by a script tag. This works as intended.
But now I have to use the class from this CMS JavaScript library.
Before writing this as a Vue-Router app I just have used Vue for templating purposes.
So I had some code packed in the window.onload event handler.
I have to create an instance for the CMS data access class.
But this leads to a build error (using vue-cli build). Since there
are no understandable error messages from the build process
I have to use trial and error. Even simple variable assignments like var a = 1 seem not to be allowed.
A console.log('something') works. But nothing else seemes to be allowed (except defining the onload-event handler)
I have added this code in a <script> Tag inside App.vue (which was created by vue-cli create)
window.onload = function() {
try {
// Instantiate class obj for CMS data access
cmsDataAccessObj = new CMSAccessData();
waitForPlayerData = true;
}
catch (e) {
console.error(e);
}
}
UPDATE
After testing the different solutions from the answers I got aware that using non-instance variables seems to cause the build errors.
This gives an error:
waitForPlayerData = true;
This works:
this.waitForPlayerData = true;
I wouldn't recommend using window.load to run your code. There are more native approaches to do this in Vue.js.
What you should do in the case you want to run it in the main component of the app before it's been loaded is to put the code inside the beforeCreate lifecycle hook of the main component.
...
beforeCreate () {
this.cmsDataLoader()
},
methods: {
cmsDataLoader () {
try {
// Instantiate class obj for CMS data access
cmsDataAccessObj = new CMSAccessData();
waitForPlayerData = true;
}
catch (e) {
console.error(e);
}
}
}
...
This will run the code everytime a component is created before the creation. You could also use the created lifecycle hook if you want to run it after the creation of the component.
Check the following link for more information about lifecycle hooks.
The best way to place JavaScript in Vue.js App is mounted function, it is called when the component is loaded:
export default {
name: "component_name",
mounted() {
let array = document.querySelectorAll('.list_item');
},
}
You don't need window.onload, you can just put whatever you want there. I'm not entirely certain when precisely in the lifecycle it renders and maybe someone can hop in and let us know but it for sure renders when page starts. (though it makes sense that it does before the lifecycle hooks even start and that it'll solve your needs)
Better & easier solution if you want to load it before Vue loads is to add it to the main.js file. You have full control there and you can load it before Vue initializes.
No need for window.onload there either, just put it before or import a JS file before you initialize Vue, because it's going to be initialized by order.

Pass data between components in a new tab

I have an object in one component and I want to pass it to another component. The problem is that this other component will open in a new tab.
I tried to use the data service strategy, but when the tab opens, the data service object comes undefined.
I thought about using the querys params and passing in the url. But the object is very complex
My data service:
#Injectable({providedIn: 'root'})
export class DataService {
private anime: Anime;
constructor() { }
setAnime(anime: Anime) {
this.anime = anime;
}
getAnime() {
return this.anime;
}
}
Setting object in data service:
goToDetailsByService(anime: Anime) {
this.dataService.setAnime(anime);
//this.router.navigateByUrl('/details');
window.open('/details');
}
Getting the anime object via service data:
ngOnInit(): void {
console.log(this.dataService.getAnime());
this.anime = this.dataService.getAnime()
}
When accessing the details component via navigate router works
I think there are two ways to do it. The first one is localStorage , the second one is PostMessage
localStorage
we can use localstorage because storage can be read across windows, and there is a storage event fire when you write something to storage.
Here is the code example.
// parent window
localStorage.setItem("EVENT.PUB", JSON.stringify(anime));
// child widnow
window.addEventListener('storage', function(event) {
console.log(event);
const anime = JSON.parse(event.newValue);
}, false);
postMessage
The window.postMessage() method safely enables communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it.
Here is the code example.
// parent window
const detailPage = window.open('/details');
detailPage.postMessage(anime, '*');
// important notice: anime should be object that can be serialize
// otherwise error will happen when execute this function.
// child window
window.addEventListener('message', (event) => {
// get out the message
console.log(event.data);
// and you can even send message back to parent window too.
event.source.postMessage('Got it!', event.origin);
}, false);
I think the easiest way to do it would be to use the browsers localStorage since that will keep the applicaton state between tabs. When you open a new tab the two web pages are seperate and the state doesn't carry over.
So using localStorage you can do..
SET
goToDetailsByService(anime: Anime) {
localStorage.setItem('anime', JSON.stringify(anime));
window.open('/details');
}
GET
ngOnInit(): void {
this.anime = JSON.parse(localStorage.getItem('anime'));
// here you can choose to keep it in the localStorage or remove it as shown below
localStorage.removeItem('anime');
}

Angular 7 call ngOnInit method or any other method of component that is contained by component

I have this situation, in which I have a component called user-table-list that is basically a table with entries that represent some users.
The component was designed so that there is another component, called table-list that describes a table and its behavior for several components (and user-table-list is one of them). The html for user-table-list is in fact just like this:
<app-table-list #table [tableConfiguration]="usersTableConfiguration"></app-table-list>
while the html for table-list has all the design for the table, describing columns, header, etc..., while the single component describes in .ts file the configuration for its personal table, meaning which data they load to fill the table, names for the columns, and so on.
In the header there's a button to add a new entry to the table, let it be of users or clients or anything else. The event and the database retrieval are handled inside table-list.component.ts, that performs a different onclick action according to which type of table the event came from. Something like that:
createNewButtonClicked(tableType: string) {
const dialogConfig = new MatDialogConfig();
dialogConfig.width = '60%';
dialogConfig.autoFocus = true;
switch (tableType) {
case 'clients': {
this.dialog.open(ClientDialogComponent, dialogConfig);
break;
}
case 'projects': {
this.dialog.open(ProjectDialogComponent, dialogConfig);
break;
}
case 'users':{
this.dialog.open(UserDialogComponent, dialogConfig);
break;
}
}
}
Inside the dialogs that are opened, a new client/project/user is added to the list. What I want is to refresh the list after closing the dialog, by calling a method of database retrieval. But this retrieval is called inside the single component, that is for example user-table-list. So I'm looking for a way to call the method of retrieval and refresh the list of user-table-list from the outside, without having to refresh the page from the browser. The problem is that I can't achieve that, not in the dialog component nor the table-list component. I've tried importing the component in the constructor and calling the method, both in dialog and table-list component, but it gives a warning for a circular reference and does nothing. Same if I create an external service to call the method, because there is always a circular reference.
I'm out of alternatives; how can this be done?
Unfortunately I can't alter the application design.
Assuming your are use a material dialog the dialogs give you an observable you can watch for to see when the dialog is closed. if you set it to a variable you can watch for when it is closed, for instance:
createNewButtonClicked(tableType: string) {
// your existing logic
// ...
const dialog = this.dialog.open(ClientDialogComponent, dialogConfig);
// example dialog .afterClosed
dialog.afterClosed().subscribe(closeResult => {
// update from database after dialog is closed
});
}
You could use an Output property to tell parent components that something happened inside of the child. Inside of the subscribe function (where I put a comment to update database) is where you would want to emit that the dialog was closed. Your code for list could look something like this:
#Output() dialogClosed = new EventEmitter();
createNewButtonClicked(tableType: string) {
const dialogConfig = new MatDialogConfig();
dialogConfig.width = '60%';
dialogConfig.autoFocus = true;
let dialog;
switch (tableType) {
case 'clients': {
dialog = this.dialog.open(ClientDialogComponent, dialogConfig);
break;
}
case 'projects': {
dialog = this.dialog.open(ProjectDialogComponent, dialogConfig);
break;
}
case 'users':{
dialog = this.dialog.open(UserDialogComponent, dialogConfig);
break;
}
}
// check to see if dialog was opened
if (!!dialog) {
dialog.afterClosed().subscribe(closeResult => {
// emit that dialog was closed
this.dialogClosed.next();
});
}
}
Then in your user-table component watch for the event to be emitted:
<app-table-list (dialogClosed)="onDialogClosed($event)"></app-table-list>
And finally your user tables ts. Note that I am not calling ngOnInit when the dialog is closed as the purprose of ngOnInit is to initialize. So extract the database logic inside of ngOnInit into another method and call this method to update data when the dialog is closed as well:
ngOnInit() {
this.getData();
}
onDialogClose() {
this.getData();
}
private getData() {
// make database calls here and update data from response
}
You should only use ngOnInit to initialize the view as you may be setting some defaults in here that you do not want to reset, so it is recommended to not call ngOnInit again later in the pages life cycle
You must issue an event from the submit button that is inside the Dialog. That is, capture the click event of the Dialog and from table-list notify the parent components that clicked.
Something like that:
table-list.component.ts
#Output() clickedOkDialog = new EventEmitter<boolean>();
onClickOkDialog() {
this.clickedOkDialog.emit(true);
}
Then you can capture the event in user-table-list
In your user-table-list.component.html
<app-table-list (clickedOkDialog)="onClickOkDialog($event)" #table [tableConfiguration]="usersTableConfiguration"></app-table-list>
In your user-table-list.component.ts
onClickOkDialog(event) {
// update from database after dialog is closed
}

Categories

Resources