Display custom third party html element inside a React component - javascript

I'm trying to show a custom HTML modal that is returned from a third-party library inside a functional React component.
This is the function that should load the modal, which is being called inside useEffect:
const modalRef = useRef()
const loadScript = async () => {
// custom script loader
const loader = new ScriptLoader({
src: 'checkout.reepay.com/checkout.js',
global: 'Reepay',
})
await loader.load()
// flag for displaying the div
setLoaded(true)
// Reepay.ModalCheckout(sessionId) should open a modal
modalRef.current.innerHTML = new window.Reepay.ModalCheckout(sessionId)
}
In my return:
return loaded ? <div ref={modalRef}></div> : <></>
When I try to display the component I just get a [Object object] inside the div. I don't know if using refs is the way to go, I'm a beginner and I didn't really understand how to integrate third-party code inside React.

From their documentation (https://docs.reepay.com/reference#overlay-checkout), it seems that instance of Reepay.ModalCheckout is an object, not HTML:
// Step 1: Initialize
var rp = new Reepay.ModalCheckout(); // No session id given
// ... Backend to backend call ...
// Step 2: Load the modal
rp.show(' YOUR SESSION ID HERE '); // Call the .show function with the session id
You do not have to inject it inside of a modal, since it comes with their own modal.
If you really want to inject it into a container, you should be using:
var rp = new Reepay.EmbeddedCheckout(' YOUR SESSION ID HERE ', { html_element: 'rp_container' } );

Related

A React, Vue, or other code depends on an object, not yet available. The best ways to wait for that variable, before rendering component content?

When an HTML document defined a variable that is not available until a later time during the page load.
Issue: A React, Vue, or other block of code depends on an object that has not yet been declared and outside the direct scope of the component, like window['varname']. What are the proper way(s) to wait for that variable to be defined before rendering a component's real content.
My Attempt:
import React from 'react'
import ReactDOM from 'react-dom/client'
import AppWrapper from "./components/AppWrapper";
const App = () => {
let intervalId
intervalId = setInterval(() => {
console.log('Waking up... checking if window.app is defined')
if (window['app'] !== undefined) {
console.log('yes')
clearInterval(intervalId)
} else {
console.log('no')
}
}, 1000)
if(app.ins.length === 0) {
return 'Loading...'
}
return (
<AppWrapper app={window['app']}></AppWrapper>
)
}
export default App
What other ways could you, should you, do it?
I will make it clearer for you :) I will describe exactly my problem: So I am writing a custom Joomla Component for Joomla. The Joomla Component is written in PHP and uses the Joomla Framework. Inside this component, I have written a Reactjs component. The way that you inject JavaScript into Joomla is via Joomla Methods. These methods either load the JS on the head of the document or in the body. Now, my Reactjs component is loaded during this process. This is fine and it works as long as I do not need to rely on outside variables.
I am using Joomla to store data that is need by the Reactjs component. The way that Joomla makes data available to JS is by a Joomla library that will inject the JS object into a script tag. This is also okay. The issue is that when the head tag loads the Reactjs component before the injected JS object, needed by the Reactjs component, is available. In my example above I store the global JS object into the window object as window.app = Some Object. Because the window.app object is not available at the time the Reactjs component has been loaded, I add a setInterval and check every 500 ms.
Then the setInterval wakes up and checks to see if the window["app"] is available yet. It keeps doing that until it is available. Once it is, it quits the interval and loads the Reactjs component container, passing in the required object.
Now, two things here:
I have no way of synchronizing this process in Joomla. Joomla is stubborn like that.
This is my attempted to only load the Reactjs container component once the data is available.
Question: Knowing the situation, what are the best strategies to accomplish this, apart from my current strategy?
Thanks :)
I believe, one of the approaches could be any kind of callback or subscription.
For example, you can define a function, which changes a state in state-container like redux.
(Pseudocode)
async function loadAppData(store) {
const data = await fetch('/some-data');
const json = await data.json();
store.dispatch('data-loaded', json)
}
And in component
function App() {
const appData = useSelector(store => store.appData);
if (!appData) {
return 'Loading...'
}
return <Markup />
}
Other option can be subscription. Again you can add some function which emits some event:
async function loadAppData(store) {
const data = await fetch('/some-data');
const json = await data.json();
eventBus.emit('data-loaded', json)
// or
window.appData = json
}
In react you can
function App() {
const [appData, setAppData] = useState();
useEffect(() => {
setAppData(window.appData)
}, [window.appData])
if (!appData) {
return 'Loading...'
}
return <Markup />
}
Or in Vue.js you could
data() {
return {
appData: ''
}
}
mounted() {
this.$on('data-loaded', this.onDataLoaded)
}
methods: {
onDataLoaded($event) {
this.appData = $event;
}
}

React Telemetry - Create span without passing parent span / Create nested span from different files

I am currently trying to implement custom tracing using telemetry. I am trying to trace the load time of the page load and until a certain network request is finished. What I want to know is how can i create a span from my jsx file and it will have some child span on the saga file.
Let's say in my index.jsx I have this
useEffect(() => {
const tracer = trace.getTracer('foo')
const parentSpan = tracer.startSpan('parentSpan')
context.with(trace.setSpan(context.active(), parentSpan), () => {
// dont know what to do here
parentSpan.end()
});
and on my saga file
const tracer = trace.getTracer('foo');
const childSpan = tracer.startSpan('childSpan', undefined, context.active());
context.with(trace.setSpan(context.active(), childSpan), () => {
// do network request here
childSpan.end();
});
However this only creates two separate spans.
My references can be found below
https://github.com/open-telemetry/opentelemetry-js/issues/1963
https://opentelemetry.io/docs/instrumentation/js/instrumentation/#using-sdk-trace-base-and-manually-propagating-span-context

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

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.

Bind the vue to root element after plugin finishes ajax call

I am binding my application root element #app to vue and before that I am loading my custom plugin with Vue.use(myplugin). My plugin makes an ajax call, load the data and set it into Vue.$permission property.. so in short I want to load my user permission before mounting the app. but while my ajax call is fetching permission data, app is mounted and my page is getting rendered, which need the permission object.
is there a way I can bind the app root element to vue after my plugin finishes.. or any other alternate solution?
Yeah, that's quite simple actually:
const Vue = require('vue');
const vueInstance = new Vue({
// don't specify the 'el' prop there
});
doingAnAjaxCall() // assuming it returns a promise
.then(() => {
vueInstance.$mount('#root'); // only now the vue instance is mounted
});
If a Vue instance didn’t receive the el option at instantiation, it will be in “unmounted” state, without an associated DOM element. vm.$mount() can be used to manually start the mounting of an unmounted Vue instance.
See: https://v2.vuejs.org/v2/api/#vm-mount
So for your case you may use any asynchronous mechanism to detect the end of the ajax call. Maybe the simplest solution is to pass a callback function to your plugin object, and mount your vue instance inside.
/* Define plugin */
MyPlugin = {};
MyPlugin.install = function(Vue, options) {
doingAnAjaxCall()
.then(data => {
// do something with data
options.callback();
});
};
const Vue = require('vue');
/* Create the vue instance */
const vueInstance = new Vue({
// don't specify the 'el' prop there
});
/* Install the plugin */
Vue.use(MyPlugin, { // This object will be passed as options to the plugin install()
callback: () => {
vueInstance.$mount('#root'); // only now the vue instance is mounted
}
});

Categories

Resources