Replace href link with a JS function - javascript

In my react electron app, that it is working with an API, I receive JSON values to display data into the components. So for example I have a Features component:
const Features = () => {
const { title } = useSelector(({ titles }) => titles);
let string = title.features;
// the string can contain some html tags. Example bellow:
// sting = 'This is a string containing a href to Google';
string = string.replace(/href="(.*?)"/g, function() {
return `onClick="${() => shell.openExternal('www.google.com')}"`;
});
return (
<>
<Heading>Features</Heading>
<Text content={parsedHTML} />
</>
);
};
What I want is to replace the href attribute with onClick and assign Electron's shell.openExternal() function.
The string.replace() callback function does that, but when I click on the <a> element, the app throws next error:
error: uncaughtException: Expected onClick listener to be a
function, instead got a value of string type.
UPDATE
Also tried this logic and the same error occurs:
global.openInBrowser = openInBrowser; // openInBrowser is basically a function that calls shell.openExternal(url)
const re = new RegExp('<a([^>]* )href="([^"]+)"', 'g');
string = string.replace(re, '<a$1href="#" onClick="openInBrowser(\'$2\')"');
Here's a link to Sandbox rep.
How do I do this correctly?

The onclick not being set on the React element is actually expected behavior.
Because there's an XSS security risk when evaling the onclick string. The recommended solution is to use the replace option in html-react-parser.
you can also use dangerouslySetInnerHTML which involves security risk.
Sandbox Demo
export default function App() {
let string =
'This is a string containing html link to Google';
const re = new RegExp('<a([^>]* )href="([^"]+)"', "g");
let replaced = string.replace(re, "<a onclick=\"alert('$2')\"");
return (
<div className="App">
<p dangerouslySetInnerHTML={{__html: replaced}}></p>
<p>{parse(string, {
replace: domNode => {
if (domNode.name === 'a') {
let href = domNode.attribs.href
domNode.attribs.onClick = () => { alert(href) }
delete domNode.attribs.href
}
}
})}</p>
</div>
);
}

Not sure on specifics of electron, but passing a (function(){functionName()})() would not work in html if there is no functionName variable available on window scope. Being there is a global environment in electron this might answer your question:
const Features = () => {
const { title } = useSelector(({ titles }) => titles);
let string = title.features;
// the string can contain some html tags. Example bellow:
// sting = 'This is a string containing a href to Google';
function runOpen(href){
shell.openExternal(href)
}
global.runOpen = runOpen;
string = string.replace(/href="(.*?)"/g, function() {
return `onClick="runOpen(${'www.google.com'})"`;
});
return (
<>
<Heading>Features</Heading>
<Text content={parsedHTML} />
</>
);
};
if it doesnt you can use something like onclick="console.log(this)" to find out what is the scope the onclick runs in and futher assign your runOpen variable there.

Related

Uncaught ReferenceError: variable is not defined javascript

I want to create a DOM element from an HTML string. Inside that string, I have a button that should call a global function called downloadFile with predefined arguments.
My first try:
<button onclick="downloadFile(${message_result})">Download</button>
But this failed to work.
Whole code looks like this
function downloadFile(result){
console.log(result)
}
(() => {
const msg = {user: 'User', message: 'Message', result: {}} // any
var markup = `
<h4>${msg.user}</h4>
<p>${msg.message}</p>
<button onclick="downloadFile(message_result)">Download</button>
`
document.body.innerHTML = markup
})()
the error:
Uncaught ReferenceError: message_result is not defined
at HTMLButtonElement.onclick
any suggestions how i can pass a variable into my function
Let's first solve your problem then talk about a better approach.
If we assume msg.result is a string: You need to wrap it with quote marks.
<button onclick="downloadFile('${message_result}')">Download</button>`;
But if your msg.result is not a simple string or you want to take the right path to the solution, then we need to move next approach.
// This is your downloadFile function
const downloadFile = (data) => {
console.log(data)
}
// This function creates download buttons
const createDownloadButton = (response) => {
const markup = `
<h4>${response.user}</h4>
<p>${response.message}</p>
<button>Download</button>
`
const container = document.createElement("div")
container.innerHTML = markup;
// Here, we assign "click" functionality dynamically even before we inject the element into DOM
container.getElementsByTagName("button")[0].addEventListener('click', () => {
downloadFile(response.result)
})
document.body.append(container)
}
// Fetch your response from an external service
const msg = {
user: 'Username',
message: 'Message',
result: {
file: {
link: 'https://stackoverflow.com'
}
}
}
// Call function to create download button
createDownloadButton(msg)
If message_result is a variable, like msg.message, then you need to reference it similarly.
var markup=`
<h4>${msg.user}</h4>
<p>${msg.message}</p>
<button onclick="downloadFile('${message_result}')">Download</button>`;

How to use getElementsByClassName on dynamic html

What I'm trying to do is to get HTML tag by className on dynamic HTML that I fetched,
but it returns undefined. It works if I try to getElementByClassName("main-page") because that class isn't dynamic
const HomePage = () => {
const [pageData, setPageData] = useContext(PageContext)
useEffect(() => {
const allImages = document.getElementsByClassName("wp-block-column")
console.log([...allImages])
}, [])
//render fronpage
const renderMainPage = () => {
//map the data and check for the site url (www.siteurl.com = front page)
if (pageData) {
return pageData.map(page => {
if (window.location.origin + "/" === page.link) {
return <section dangerouslySetInnerHTML={{ __html: page.content.rendered }}></section>
}
})
}
}
return (
<h1 className="main-page"">{renderMainPage()}</h1>
)
}
export default HomePage
From what I see the problem in pageData in context, probably your default value to context's pageData is false and renderMainPage() is not going further then first "if" statement
But its not preferred to use document selectors, use refs in React instead.
Also from naming I see that you are trying to get images and not dom nodes, but with this logic you are going to get dom nodes. I'm sure there is better way/flow to access images you need than looking for them in nodes.
const elementsRef = useRef(data.map(() => createRef()));
you can you dynamic create ref to access or create random iniNumber Array then assing to you elements when select them
You can use this function that useLayoutEffect, you can modify your code like this,
useLayoutEffect(()=> {
document.getElementsByClassName("yourclassname")
})

Responding to User Input lit-html

I am using lit-html and I can't seem to get the renderer to update, or any function to run on user input. I would like to check an input field on typing. I can't find any examples online showing responding to user input.
Here is the full code.
my_input.ts
import {html, directive, Part} from 'lit-html';
const stateMap = new WeakMap();
export const stateManager = directive(() => (part: Part) => {
let mystate: IInputData = stateMap.get(part);
if(mystate === undefined)
{
mystate = {
auto_id: Math.floor(Math.random() * 100000 + 1),
helper_eval: "",
input_message: "",
tab_index: 0,
input_value: "",
span_message: "",
}
stateMap.set(part, mystate);
}
else {
console.log("Hey");
part.setValue(mystate);
part.commit();
}
});
export interface IInputData {
auto_id: number,
helper_eval: string,
input_message: string,
tab_index: number,
input_value: string,
span_message: string
}
export let my_input = () => {
let d: IInputData = stateManager() as unknown as IInputData;
return html`
<section>
<label for="${d.auto_id}"
title="${d.helper_eval}">
${d.input_message} &#x24D8
<span class="float_left">
${d.span_message}
</span>
</label>
<input id="${d.auto_id}"
tabindex="${d.tab_index}" value="${d.input_value}">
</input>
<section>
`;
}
app.ts
const myTemplate = () => html`
<div>
${renderCounter(0)}
</div>`;
const renderCounter = directive((initialValue) => (part: any) => {
if(part.value === undefined)
{
part.setValue(initialValue);
}
else { part.setValue(part.value +1)}
}
);
export let app = (data: TemplateResult[]) => html`
${data}
`;
render(app([my_input(), myTemplate]), document.body);
I have included the render counting example, and don't see the render counter increase while typing in the input field. I am also not sure how I can add hooks to respond to the IInputData that change in that template. My best guess is to add some sort of callback function to the stateManager directive.
If you are looking for a basic usage of lit-html, please read on. If you are trying to figure out how to implement a custom directive, then I hope someone else can answer it for you. Also, I don't know Typescript so my answer below is in a plain JavaScript.
The idea behind lit-html is for you to call render() function every time you want to update the HTML. It does not self-update its output.
So, to use it with an input element, you need to implement a callback function to update template's dependent data-structure and to re-render. The callback must be attached to your template with #input/#change/#click etc.
Here is a simple example (loosely based on yours):
// create a data object containing the template variables
const data = {
span_message: "input text copy appears here"
}
// callback function for the input event
const inputCallback = (evt) => {
// update the template data
d.span_message = evt.target.value;
// re-render the template
render(myTemplate(data), document.body);
};
// function to generate a template
// d - data object, which controls the template
const myTemplate = (d) => html`
<input
#input=inputCallback
</input>
<span>
${d.span_message}
</span>`;
// initial rendering of the template
render(myTemplate(data), document.body);
Hope this helps

How can I make some text from a json string bold?

I want to concatenate two strings in react such that the first displays bold and the second does not. I have the string I want bolded in a JSON file and I have the string I want to concatenate coming from backend API call. Here's my setup:
This is in a JSON file:
{ stuff: {
stuffIWantBolded: "bold text"
}
}
And my frontend looks like this:
render() {
// props for this component in which I'm rendering SomeComponent (see below)
const { data } = this.props
const { theStringFromBackend } = data
// a method to get the string that is working for me
const stuff = this.getTheStringFromTheJSON();
const textIWantToDisplay = `${stuff.stuffIWantBolded} ${theStringFromBackend}`;
return (
<SomeComponent someProp={textIWantToDisplay} />
);
};
That concatenates the two strings successfully. I've tried using .bold() at the end of stuff.stuffIWantBolded, but that apparently doesn't work, because then the string renders as <b>bold text</b> the string from backend (assuming that's what the string from backend says), with the HTML tags written out explicitly instead of rendering actual bold text. Is there something I'm missing? I don't think one can just make the string bold in the JSON...perhaps I need to use a regex? Any help would be greatly appreciated.
How about this:
return (
<>
<span style={{fontWeight: "bold"}}>{stuff.stuffIWantBolded}</span>
<span>{theStringFromBackend}</span>
</>
);
The <> and </> effectively allow you to return multiple items from one render function.
I would do it by using composition (I prefer the strong tag to b to make text bold):
render() {
// props for this component in which I'm rendering SomeComponent (see below)
const { data } = this.props
const { theStringFromBackend } = data
// a method to get the string that is working for me
const stuff = this.getTheStringFromTheJSON();
return (
<SomeComponent>
<p><strong>{stuff.stuffIWantBolded}</strong> {theStringFromBackend}</p>
</SomeComponent>
);
};
And in SomeComponent you will find the nested html inside props.children
for more info check here: https://reactjs.org/docs/composition-vs-inheritance.html
It turns out you CAN pass in a prop within a self-closing tag component the way I want to. Not sure if it's conventionally sound/overly bloated but it looks like this:
render() {
// props for this component in which I'm rendering SomeComponent (see below)
const { data } = this.props
const { theStringFromBackend } = data
// a method to get the string that is working for me
const stuff = this.getTheStringFromTheJSON();
const theBoldedText = `${stuff.stuffIWantBolded}`;
return (
<SomeComponent someProp={<span><b>{theBoldedText}</b> <span>{theStringFromBackend}</span></span>} />
);
};

Replace img tags with Gallery in React

There is problem with render gallery component:
I get string with html from server
let serverResponse = `
<h3>Some title</h3>
<p>Some text</p>
<p>
<img src="">
<img src="">
<img src="">
<br>
</p>
...
`
Now I render this response with dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: serverResponse }} />
But when I got 2 or more repeating <img> tags I want to replace them with component.
How can I do that? I tried to do it with Regex and replace them with <Gallery/> but it doesn't work. I think that I need split string in array of tags and then replace images with <Gallery/> component.
I tried do it with renderToString
...
getGallery = images => {
// Loop throw images and get sources
let sources = [];
if (images) {
images.map(img => {
let separatedImages = img.match(/<img (.*?)>/g);
separatedImages.map(item => sources.push(...item.match(/(https?:\/\/.*\.(?:png|jpg))/)));
});
}
if (sources.length) {
return <Gallery items={sources}>
}
return <div/>
};
...
<div dangerouslySetInnerHTML={{__html: serverResponse.replace(/(<img (.*?)>){2,}/g,
renderToString(this.getGallery(serverResponse.match(/(<img (.*?)>){2,}/g))))}}/>}
And this doesn't work, because I get just html without logic :(
Fist of all, dangerouslySetInnerHTML is not the way to go, you can't insert gallery into in it and have it processed by React. What you need to do is multistep procedure.
1. Parse HTML into document. In this stage you will convert string to valid DOM document. This is very easy to do with DOMParser:
function getDOM (html) {
const parser = new DOMParser()
const doc = parser.parseFromString(`<div class="container">${html}</div>`, 'text/html')
return doc.querySelector('.container')
}
I make this helper function to return container with your HTML nodes. It will be need in the next step.
2. Transform DOM document into React JSX tree. Now that you have DOM tree it's very easy to convert it to JSX by creating individual React elements out of corresponding DOM nodes. This function needs to be recursive to process all levels of the DOM tree. Something like this will do:
function getJSX(root) {
return [...root.children].map(element => {
const children = element.children.length ? getJSX(element) : element.textContent
const props = [...element.attributes].reduce((prev, curr) => ({
...prev,
[curr.name]: curr.value
}), {})
return React.createElement(element.tagName, props, children)
})
}
This is enough to create JSX out of DOM. It could be used like this:
const JSX = getJSX(getDOM(htmlString))
3. Inject Gallery. Now you can improve JSX creation to inject Gallery into created JSX if element contains more then 1 image tag. I would pass inject function into getJSX as the second parameter. The only difference from above version would be is how children is calculated in gallery case:
if (element.querySelector('img + img') && injectGallery) {
const imageSources = [...element.querySelectorAll('img')].map(img => img.src)
children = injectGallery(imageSources)
} else {
children = element.children.length ? getJSX(element) : element.textContent
}
4. Create Gallery component. Now it's time to create Gallery component itself. This component will look like this:
import React from 'react'
import { func, string } from 'prop-types'
function getDOM (html) {
const parser = new DOMParser()
const doc = parser.parseFromString(`<div class="container">${html}</div>`, 'text/html')
return doc.querySelector('.container')
}
function getJSX(root, injectGallery) {
return [...root.children].map(element => {
let children
if (element.querySelector('img + img') && injectGallery) {
const imageSources = [...element.querySelectorAll('img')].map(img => img.src)
children = injectGallery(imageSources)
} else {
children = element.children.length ? getJSX(element) : element.textContent
}
const props = [...element.attributes].reduce((prev, curr) => ({
...prev,
[curr.name]: curr.value
}), {})
return React.createElement(element.tagName, props, children)
})
}
const HTMLContent = ({ content, injectGallery }) => getJSX(getDOM(content), injectGallery)
HTMLContent.propTypes = {
content: string.isRequired,
injectGallery: func.isRequired,
}
export default HTMLContent
5. Use it! Here is how you would use all together:
<HTMLContent
content={serverResponse}
injectGallery={(images) => (
<Gallery images={images} />
)}
/>
Here is the demo of this code above.
Demo: https://codesandbox.io/s/2w436j98n
TLDR: You can use React HTML Parser or similar libraries.
While it looks very alike, JSX get parsed into a bunch of React.createElement so interpolate React component into HTML string will not work. renderToString won't do too because it is used to server-side render React page and will not work in your case.
To replace HTML tags with React component, you need a parser to parse the HTMl string to nodes, map the nodes to React elements and render them. Lucky for you, there are some libraries out there that do just that, like React HTML Parser for example.

Categories

Resources