Pass data between components in a new tab - javascript

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');
}

Related

Service Worker determine number of clients

We've got a SaaS app that's PWA capable and we're using Workbox for the grunt work. Up to now we've been following the tried and trusted recipe of displaying an update is available banner to the users, prompting them to update their web app.
Viewing usage data (via Sentry.io) we've noticed that most users simply seem to ignore the update banner and continue with the version they're on.
So we're looking at trying something different. This is to perform the update automatically when they change the route in the web app (when we know there's an update available).
Testing this shows that it does work. However there's a side-effect, and that is if they've got web app open in multiple tabs, then all the tabs get updated. This could be problematic for users' if they've got an un-saved form open in one of the tabs in the background - they'll potentially loose their work.
This happens during this piece of code:
// app shell page, created lifecycle hook
document.addEventListener('swUpdated', this.SetPwaRegistration, { once: true })
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (this.refreshing) {
return
}
this.refreshing = true
window.location.reload()
})
// app shell page, method in methods collection
SetPwaRegistration (event) {
// call mutation to pass the registration object to Vuex
this.PWA_REGISTRATION_SET({ pwaRegistration: event.detail })
}
// main.js
router.afterEach((to, from) => {
// retrieve the registration object from Vuex
const pwaRegistration = app.$store.getters.pwaRegistration
if (pwaRegistration) {
pwaRegistration.waiting.postMessage('skipWaiting')
}
})
the above code is from our Vue.js app code-base, the this.refreshing is set to false by default in the data property collection.
What I'd like to know if whether it is possible to determine if the Service Worker has only one client under it's control (i.e. the web app is only open in 1 browser tab), and if this is the case, the auto-update can happen without potential issues. If there's more than one client, then we'll display the update banner as usual.
As a brief update to this, I've come across code examples similar to this:
self.clients.matchAll().then(clients => {
const clientCount = clients.length
// store count of client in Vuex
})
Which looks like an option (i.e. count how many clients there are, store this in Vuex store), I'm just not sure where it should be used.
If you want to orchestrate the reload from entirely within the service worker, you can effectively do that by using the WindowClient interface to programmatically navigate to whatever the current URL is, which is roughly equivalent to a reload.
The two things to keep in in mind that navigate() will only work on WindowClients (not Workers) and that you can only call it if the service worker controls the WindowClient.
Putting that together, here's something to try:
// This can be run, e.g., in a `message` handler in your SW:
self.clients.matchAll({
// These options are actually the defaults, but just
// to be explicit:
includeUncontrolled: false,
type: 'window',
}).then((clients) => {
if (clients.length === 1) {
clients[0].navigate(clients[0].url);
}
})

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
}

Save Class Instance to Vue Data

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.

Passing event down to a component that is not a parent or child of the other in Angular 2 app

I am trying to figure out the simplest way to pass an event from one Angular 2 component to another when they do NOT share a parent-child relationship.
Specifically I have a component that is used to open a dialog box (when clicked a new component opens). Once the user has clicked on a button on that new dialog component, I want to send notification of that event back to the original component (where the button was clicked to open the dialog box/component to begin with) so that I can fire a function there.
Here's the code that is used in the originating component to trigger the opening of the dialog:
public onResponseSelected(option, selectedService)
{
const primaryEmail = this.getPrimaryEmail();
let currentService = this.selectedService;
let activeList = this.getActiveList();
if (option && option.toString() === 'Follow-up Required')
{
// Create dialog
let dialogRef: MdDialogRef<ResponseProcessComponent>
= this.mdDialog.open(ResponseProcessComponent, {
disableClose: true,
data: {
option: option,
currentService: currentService,
primaryEmail: primaryEmail,
activeList: activeList
}
});
dialogRef = null;
}
}
I am "packaging" up some data to send to the dialog component as part of "data" here. That's all working as expected.
Now I simply need a way to send an event trigger from dialog component (responseProcessComponent) back down to the originating component. As a reminder, there is no shared view here, so I can't pass down via Input() as if these were components with a parent-child relationship.
I just need a way to trigger an event in the dialog box component and have that sent back down to the other component. What's the simplest way to do this?
You could use a shared service and send a notification from any component injecting the service.
Consider the following :
dialog.service.ts
import {Injectable} from '#angular/core';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
#Injectable()
export class DialogService {
public dialog = new BehaviorSubject<string>('Not Clicked');
dialogObservable = this.dialog.asObservable();
changeDialogState (value: string): void {
this.dialog.next(value);
}
}
And then in your component:
reciver.component.ts
To get the dialog state you do:
constructor(private ds: DialogService) {
this.ds.dialogObservable
.subscribe((dialogState) => {
//add your logic here!! for now I'm just gonna console log the sate of the dialog
console.log(dialogState);
});
}
To set a new state for the dialog you do:
changeDialogState(){
this.ds.changeDialogState('Cliked');
}
and don't forget to add the service in the provides arrays and the component in the declarations array of the same module so you don't get any errors.

Methods no longer exist when sending to/from ipcRenderer

I have the following in my render process:
class Component {
findClosestComponent(c) { /* Do stuff */ }
}
class Item extends Component {
download() {
this.myProperty = 123;
ipcRenderer.send('download-game', this);
ipcRenderer.on('download-complete', (evt, dl) => {
console.log(dl.myProperty);
dl.findClosestComponent(GameCard);
});
}
}
I then have this in my main process:
ipcMain.on('download-game', (evt, dl) => {
/* Download the file the send back the object */
evt.sender.send('download-complete', dl);
});
When I execute the download() method, it sends the current object to the main process, and then the main process sends it back. When I try to access the method findClosestComponent() it is no longer there. However, when I try to access the property myProperty it does exist and prints out.
How can I send the object to the main process then get it back and access the methods?
Electron's ipcMain and ipcRenderer modules serialize the message to JSON before sending it, so if you want to get a better idea of what actually gets sent between processes put the message through JSON.stringify(). Functions/methods and prototype chains won't make it across the process boundry, if you want that to work you'll have to implement your own serialization/deserialization scheme.

Categories

Resources