ReactJS - render external DOM element with events - javascript

I use ReactJS 16.13.1 and now I want to render an external DOM element with its events.
So let's assume there is a
<button type="button" id="testBtnSiri" onclick="alert('Functionality exists');">Testbutton Siri</button>
Which has been generated by a library (with some other events as well).
Now I want:
To copy it (with events) & render this element in my ReactJS render function.
What is the most appropriate way to do this ?
I am not looking for ReactDOM.createPortal().
I want to show an external DOM element with its events in my React Component.

In order to use HTML within your React code you need to make use of dangerouslySetInnelHTML prop on an element.
function libraryReturnedHTML() {
return '<button type="button" id="testBtnSiri" onclick="alert('Functionality exists');">Testbutton Siri</button>'
}
function MyComponent() {
return <div dangerouslySetInnerHTML={__html: libraryReturnedHTML} />;
}
However you must know that this makes your code susceptible to XSS attacks. In order to prevent such scenarios its a good idea to first sanitize your HTML so that you have only the desired scripts and link tags. In order to sanitizehtml you can simply use normal Javascript functions and document.createTreeWalker
You can also make use of an existing library to sanitize your HTML. For example sanitize-html
However the sanitization is just an added precaution.

Create a wrapper over Node.addEventListener() which will catch all the event listeners added to the button when it's rendered by other library. It could look like this(this code have to be executed before the external library creates the button):
let ael = Node.prototype.addEventListener
let eventListeners = []
Node.prototype.addEventListener = function(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void {
if((this as HTMLElement).id === "buttonID")
eventListeners.push({type, listener, options})
this.addEventListener = ael
this.addEventListener(type, listener, options)
}
Create react component with your button - you can create a new one, or inject using dangerouslySetInnerHtml. Pass eventListeners array as a prop. On component mount and unmount, add/remove the event listeners to your button.
function ClonedButton({eventListeners}: any){
useEffect(()=>{
eventListeners.forEach((el) => document.getElementById("my-migrated-new-button")!.addEventListener(el.type, el.listener, el.options));
return () => {
eventListeners.forEach((el) => document.getElementById("my-migrated-new-button")!.removeEventListener(el.type, el.listener, el.options));
}
}, [])
return <button id="my-migrated-new-button"></button>
}

Related

Vue3 web component set attribute

I created stand alone web component, with Vite and Vue3, to replace old jQuery libraries.
When rendered in html, it looks something like:
<custom-datepicker id="deliveryTime">
#shadow-root
<div>...actual component...</div>
</custom-datepicker>
When user select date it would be perfect if I can set attribute value to this element, like:
<custom-datepicker value="selected value HERE" ...
so the form can then use this value from the element by it's id.
Problem is that the only way I manage to achieve this is by emitting event (selected) from my web component and listening to that event, so the form can use value, like:
const datepicker = document.querySelector('#deliveryTime');
const dateSelected = (e) => {
datepicker.value = e.detail.val;
}
window.addEventListener('selected', dateSelected);
If I set value attribute within web component, rest of the app (Apache Velocity template) can't access it because it is in the shadow-root:
<custom-datepicker id="deliveryTime">
#shadow-root
<div value="selected value is sadly HERE">...actual component...</div>
</custom-datepicker>
Is it even possible to achieve this from inside the web component (so I can omit listener)?
I found solution, maybe not the cleanest one but it works :)
In my web-component, in mounted hook component is selected:
const dp = ref(null)
onMounted(() => {
dp.value = document.querySelector("#deliveryTime");
})
After date is selected, instead of emitting just set attribute:
dp.value.setAttribute('value', value)
What if you access the HTMLElement linked to your shadowhost and then use the "setAttribute" function on it ? Assuming you have access to the shadowRoot inside your component using 'this', I think something like that will do the trick (https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/host) :
const shadowHost = this.shadowRoot.host; // The HTMLElement linked to <custom-datepicker />
shadowHost.setAttribute("value", someValueOfYourChoice);

React hooks onclick event with multiple params without unnecessary rerender?

I am using react hooks and functional components and was wondering how I can add multiple params to an react onClick event.
I know there are different options to achieve this. In the past I used this style below (from https://reactjs.org/docs/handling-events.html):
class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
// This syntax ensures `this` is bound within handleClick
return (
<button onClick={() => this.handleClick()}>
Click me
</button>
);
}
}
But now I am facing this exact described problem from the official react docs. I am getting to many rerenders because of these arrow functions in the onClick attribute:
The problem with this syntax is that a different callback is created
each time the LoggingButton renders. In most cases, this is fine.
However, if this callback is passed as a prop to lower components,
those components might do an extra re-rendering. We generally
recommend binding in the constructor or using the class fields syntax,
to avoid this sort of performance problem.
I have put my function already in a useCallback hook. But if I use this function in a onClick event with an arrow function it will trigger rerenders again.
If I change it to the function reference only it is not triggering rerenders.
So far this is fine.
But: How do I add multiple parameters to this functionreference when using react hooks and functional components.
Will I get by default always the e (event parameter?) as first parameter?
Can somebody explain to me when and how I am getting the react event parameter and when I will not receive this event?
How can I add multiple params beside the event parameter in my onClick attribute?
For example:
What if I have this function and want to use it in the react onClick attribute, prevent unnecessary rerender and add multiple different parameter in the function call
const myClickFunction = (e, value1, value2, value3) => {
// ...
}
// this would trigger rerenders because of the arrow function how do I prevent this?
<button onClick={(e) => myClickFunction(e, "input1", "input2", "input3")}>
Click me
</button>
One trick I like to use in this case is to "bind" the parameters to rendered element using data attributes
const myClickFunction = (e) => {
const value1 = e.currentTarget.getAttribute('data-value1')
const value2 = e.currentTarget.getAttribute('data-value2')
const value2 = e.currentTarget.getAttribute('data-value2')
}
// this would trigger rerenders because of the arrow function how do I prevent this?
<button onClick={myClickFunction} data-value1="a" data-value2="b" data-value3="c">
Click me
</button>
This way you can memoise your function using useCallback safely and you can reuse the same function if you want to pass it to array of children for example. This is not ideal, you couple parents and children and you can only use data which is serializeable to string (basically only primitives).
Better solution would be to store your values somewhere out of component tree so you can access them without closures (for example in redux-thunk you don't need to pass a lot of stuff around, you can just get data you need from store directly by calling getState)

Need a callback when it is OK to access the DOM

I have an Angular component that fetches markdown, converts it to HTML, then inserts it into the DOM. After insertion it does some more DOM manipulation. So the template is this:
<div id="fixroot" [innerHTML]="content"></div>
and the component is like this:
content: string;
...
async getPage(url: string) {
try {
this.content = this.mdToHtml(await this.http.get(url, { responseType: 'text'}).toPromise();
setTimeout(() => this.fixupDOM(), 400);
....
fixupDom() {
const el = document.getElementById('fixroot');
// do stuff to the DOM element that are children of el
The use of setTimeout here is really awful, but without it the fixupDom method doesn't work
because the DOM is not ready when the Promise concludes. But it has to go.
What I need is a callback (probably from the HTMLElement
el) that calls my routine as soon as the DOM structure is ready. I haven't been able
to make anything work. Anyone know the right function?
You can try and use the ngAfterViewInit hook that fires after your content has been loaded.
If you use a template reference for your div then you can access it after the ngAfterViewInit fires.
This is the angular way of accessing the DOM which makes it framework agnostic in case you want to run in a web worker etc...
Since you are inserting HTML you made also need to pipe that HTML through a Sanitizer because angular has a sanitizing context due to XSS.
ngAfterContentInit lifecycle hook is called earlier during creation and rendering of component and directive.
You can implement it in your directive and access content to be reflected in the directive's view. For example,
ngAfterContentInit() {
const el = this.content.nativeElement;
// mutate el as desired
}

Access DOM when using hyper.Component

When using HyperHTMLElement it's possible to access the contents of the component by simply using this.children or this.querySelector(), since it's an element.
But how would I achieve similar behavior when using hyper.Component?
The hypothetical example I have in mind is from React docs: https://facebook.github.io/react/docs/refs-and-the-dom.html - I'd like to focus a specific node inside my DOM.
I have a codepen sandbox where I'm trying to solve this: https://codepen.io/asapach/pen/oGvdBd?editors=0010
The idea is that render() returns the same Node every time, so I could save it before returning and access it later as this.node:
render() {
this.node = this.html`
<div>
<input type="text" />
<input type="button" value="Focus the text input" onclick=${this} />
</div>
`;
return this.node;
}
But that doesn't look clean to me. Is there a better way to do this?
The handleEvent pattern is there to help you. The idea behind that pattern is that you never need to retain DOM references when the behavior is event-driven, 'cause you can always retrieve nodes via event.currentTarget, always pointing at the element that had the listener attached, or event.target, suitable for clicks happened in other places too within a generic click handler attached to the wrap element, in your demo case the div one.
If you'd like to use these information, you can enrich your components using an attribute to recognize them, like a data-is="custom-text-input" on the root element could be, and reach it to do any other thing you need.
onclick(e) {
var node = e.target.closest('[data-is=custom-text-input]');
node.querySelector('[type=text]').focus();
}
You can see a working example in a fork of your code pen:
https://codepen.io/WebReflection/pen/RLNyjy?editors=0010
As alternative, you could render your component and address its content once as shown in this other fork:
https://codepen.io/WebReflection/pen/LzEmgO?editors=0010
constructor() {
super().node = this.render();
}
at the end of the day, if you are not using custom elements but just basic, good'ol DOM nodes, you can initialize / render them whenever you want, you don't need to wait for any upgrade mechanism.
What is both nice and hopefully secure here, is that there's no way, unless you explicitly expose it, to address/change/mutate the instance related to the DOM element.
I hope these possibilities answered your question.
This is something I've worked on in the past via https://github.com/joshgillies/hypercomponent
The implementation is actually quite trivial.
class ElementalComponent extends hyper.Component {
constructor () {
super()
const _html = super.html
this.html = (...args) => {
this.node = _html.apply(this, args)
return this.node
}
}
}
class HelloWorld extends ElementalComponent {
render () {
return this.html`<div>Hello World!</div>`
}
}
This works really well and is inline with your question. However, it's worth noting hyperHTML can render not only a single node but also multiple nodes. As an example:
hyper`<div>Hello World!</div>` // returns a single DOM Node
hyper`<div>Hello</div> <div>World!</div>` // returns multiple DOM Nodes as an Array.
So this.node in the above ElementalComponent can be either a DOM Node, or Array based on what the renderer is doing.

Render HTML string w/JSX expression in React dangerouslySetInnerHTML()

I have successfully used React's dangerouslySetInnerHTML to render HTML string responses I'm getting from an API, but now I need to add a click handler to parts of certain responses that are set by dangerouslySetInnerHTML. Is there a way to add handlers to dangerouslySetInnerHTML conent? Example:
var api_response = '<a onClick={this.props.methodName} href="www.google.com">example</a>';
return <div dangerouslySetInnerHTML={_markupFromString(api_response)}></div>
function _markupFromString (msg) {
return { '__html': msg };
}
This isn't going to work. dangerouslySetInnerHTML() doesn't work with JSX. A better strategy would be to have the AJAX call simply return the required strings and (link and title) and supply a component with these values as props. The component is simply the link in this situation.
Or you can add your SVG in the file public/index.html then create In your component a function like this
doSomething() {
console.log('ajajaj');
}
componentDidMount() {
window.NameVarible = this.doSomething;
}
And add your event onClick="NameVariable()" in the SVG

Categories

Resources