contentful documentHtmlToString format links - javascript

I am using contentful for our CMS needs and using the contentful SDK for JavaScript.
I am trying to use their documentToHtmlString method to add some attributes to my anchor links.
I have tried doing it like this:
return documentToHtmlString(text, {
renderNode: {
[BLOCKS.PARAGRAPH]: (node, next) => {
let content: any[] = [];
node.content.forEach((item) => {
if (item.nodeType === 'hyperlink') {
let itemContent = item.content[0];
let value = itemContent['value'];
let uri = item.data.uri;
console.log(value, uri);
content.push(
`<p>${value}</p>`
);
} else {
content.push(`<p>${next([item])}</p>`);
}
});
console.log(content.join(''));
return content.join('');
},
},
});
But when I inspect the result, it doesn't have my data-category or data-action.
Is there a better way to add attributes to a link?
Their documentation shows this:
https://www.contentful.com/developers/docs/javascript/tutorials/rendering-contentful-rich-text-with-javascript/
but there is no mention of anchors :(
The plot thickens...
I figured maybe it doesn't like data attributes, so I added a few more:
content.push(
`<p><a class="test" href="${uri}" category="contact" data-category="contact" data-action="email" target="_blank">${value}</a></p>`
);
and what actually gets rendered is this:
<a class="test" href="mailto:bob#example.com" target="_blank">bob#example.com</a>
Notice how it's added class and target, but omitted category, data-category and data-action.....
Thanks to #stefan-judis for telling me about inline.
I have now updated my code to this:
[INLINES.HYPERLINK]: (node, next) => {
console.log(node);
let value = node.content[0]['value'];
let uri = node.data.uri;
return `<a class="test" href="${uri}" data="test" category="contact" data-category="contact" data-action="email" target="_blank">${value}</a>`;
},
and I removed the BLOCKS code, but unfortunately, I still get the same issue. It is not rendering all the attributes (only class and target). Infact, it renders exactly the same as above. It's like there is some internal formatting that removes attributes...

I created a quick example achieving your desired outcome.
client.getEntry("...").then(({ fields }) => {
const richText = fields.richText;
const renderOptions = {
renderNode: {
// this is the important part 👇
[INLINES.HYPERLINK]: (node, next) => {
console.log(node);
return `<a href="${
node.data.uri
}" style="background: red; color: white;" data-foo>${next(
node.content
)}</a>`;
},
[BLOCKS.EMBEDDED_ASSET]: (node, next) => {
// ...
}
}
};
document.getElementById("app").innerHTML = documentToHtmlString(
richText,
renderOptions
);
});
I'm not sure what going on in your environment, but as you see on CodeSandbox, the code renders data attributes and everything. :)

#Stefan was correct with his answer; it was Angular that was messing up my content.
I had to use angulars DomSanitizer to fix the problem like this:
public htmlToString(text: any): SafeHtml {
const renderOptions = {
renderNode: {
[INLINES.HYPERLINK]: (node, next) => {
return `<a href="${node.data.uri}" style="background: red; color: white;" data-foo>${next(node.content)}</a>`;
},
},
};
const content = documentToHtmlString(text, renderOptions);
return this.sanitizer.bypassSecurityTrustHtml(content);
}
That allowed me to add it to my html like this:
<div class="content" [innerHtml]="section.content"></div>
And it all worked as expected

Related

ReactJS - Print specific elements in the DOM

I am using ReactJS on an App and currently need to be able to print some elements from the page on user's request (click on a button).
I chose to use the CSS media-query type print (#media print) to be able to check if an element should be printed, based on a selector that could be from a class or attribute on an Element. The strategy would be to hide everything but those "printable" elements with a stylesheet looking like:
#media print {
*:not([data-print]) {
display: none;
}
}
However, for this to work I need to also add the chosen print selector (here the attribute data-print) on every parent element each printable element has.
To do that here's what I've tried so far:
export default function PrintButton() {
useEffect(() => {
const handleBeforePrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
let element = printableElement;
while (element.parentElement) {
element.setAttribute("data-print", "");
element = element.parentElement;
}
element.setAttribute("data-print", "");
}
});
};
const handleAfterPrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
let element = printableElement;
while (element.parentElement) {
element.removeAttribute("data-print");
element = element.parentElement;
}
element.removeAttribute("data-print");
}
});
};
window.addEventListener("beforeprint", handleBeforePrint);
window.addEventListener("afterprint", handleAfterPrint);
return () => {
window.removeEventListener("beforeprint", handleBeforePrint);
window.removeEventListener("afterprint", handleAfterPrint);
};
}, []);
return <button onClick={() => window.print()}>Print</button>;
}
With printNodeSelectors being a const Array of string selectors.
Unfortunately it seems that React ditch out all my dirty DOM modification right after I do them 😭
I'd like to find a way to achieve this without having to manually put everywhere in the app who should be printable, while working on a React App, would someone knows how to do that? 🙏🏼
Just CSS should be enough to hide all Elements which do not have the data-print attribute AND which do not have such Element in their descendants.
Use the :has CSS pseudo-class (in combination with :not one) to express that 2nd condition (selector on descendants):
#media print {
*:not([data-print]):not(:has([data-print])) {
display: none;
}
}
Caution: ancestors of Elements with data-print attribute would not match, hence their text nodes (not wrapped by a tag) would not be hidden when printing:
<div>
<span>should not print</span>
<span data-print>but this should</span>
Caution: text node without tag may be printed...
</div>
Demo: https://jsfiddle.net/6x34ad50/1/ (you can launch the print preview browser feature to see the effect, or rely on the coloring)
Similar but just coloring to directly see the effect:
*:not([data-print]):not(:has([data-print])) {
color: red;
}
<div>
<span>should not print (be colored in red)</span>
<span data-print>but this should</span>
Caution: text node without tag may be printed...
</div>
After some thoughts, tries and errors it appears that even though I managed to put the attribute selector on the parents I completely missed the children of the elements I wanted to print! (React wasn't at all removing the attributes from a mysterious render cycle in the end)
Here's a now functioning Component:
export default function PrintButton() {
useEffect(() => {
const handleBeforePrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
const elements: Element[] = [];
// we need to give all parents and children a data-print attribute for them to be displayed on print
const addParents = (element: Element) => {
if (element.parentElement) {
elements.push(element.parentElement);
addParents(element.parentElement);
}
};
addParents(printableElement);
const addChildrens = (element: Element) => {
elements.push(element);
Array.from(element.children).forEach(addChildrens);
};
addChildrens(printableElement);
elements.forEach((element) => element.setAttribute("data-print", ""));
}
});
};
const handleAfterPrint = () => {
document.querySelectorAll("[data-print]").forEach((element) => element.removeAttribute("data-print"));
};
window.addEventListener("beforeprint", handleBeforePrint);
window.addEventListener("afterprint", handleAfterPrint);
return () => {
window.removeEventListener("beforeprint", handleBeforePrint);
window.removeEventListener("afterprint", handleAfterPrint);
};
}, []);
return <button onClick={() => window.print()}>Print</button>;
}
I usually don't like messing with the DOM while using React but here it allows me to keep everything in the component without having to modify anything else around (though I'd agree that those printNodeSelectors need to be chosen from outside and aren't dynamic at the moment)

Replace <a> with React <Link> in text

in my React app I am getting some HTML from another server (Wikipedia) and - in this text - want to replace all the links with react-router links.
What I have come up with is the following code:
// Parse HTML with JavaScript DOM Parser
let parser = new DOMParser();
let el = parser.parseFromString('<div>' + html + '</div>', 'text/html');
// Replace links with react-router links
el.querySelectorAll('a').forEach(a => {
let to = a.getAttribute('href');
let text = a.innerText;
let link = <Link to={to}>{text}</Link>;
a.replaceWith(link);
});
this.setState({
html: el.innerHTML
})
Then later in render() I then inserted it into the page using
<div dangerouslySetInnerHTML={{__html: this.state.html}} />
The problem is that React JSX is not a native JavaScript element thus not working with replaceWith. I also assume that such a Link object can not be stored as text and then later be restored using dangerouslySetInnerHTML.
So: what is the best way to do this method? I want to keep all the various elements around the link so I cannot simply loop through the links in render()
Umm, you really need to use Link
Option 1:
import { renderToString } from 'react-dom/server';
el.querySelectorAll('a').forEach(a => {
let to = a.getAttribute('href');
let text = a.innerText;
const link = renderToString(<Link to={to}>{text}</Link>);
a.replaceWith(link);
});
Option 2: Use html-react-parser
You can not render <Link> like this.
Instead you could try another way:
el.querySelectorAll('a').forEach((a) => {
a.addEventListener('click', (event) => {
event.preventDefault()
const href = a.getAttribute('href');
// use React Router to link manually to href
})
})
when click on <a> you routing manually.
See Programmatically navigate using react router
You can leverage functionality of html-react-parser
import parse, { domToReact } from 'html-react-parser';
import { Link } from 'react-router-dom';
function parseWithLinks(text) {
const options = {
replace: ({ name, attribs, children }) => {
if (name === 'a' && attribs.href) {
return <Link to={attribs.href}>{domToReact(children)}</Link>;
}
}
};
return parse(text, options);
}
And then use it like:
const textWithRouterLinks = parseWithLinks('Home page');

JavaScript recursive array to select html elements

I'm currently trying to make a recursive function that takes html elements as an array so I can take html elements like the querySelector function
The reason i'm doing this is because I can't use getElementsByTagName() or querySelector()
Here is my code:
function flatten(items)
{
const flat = [];
items.forEach(item => {
if (Array.isArray(item)) {
flat.push(...flatten(item));
}
else {
flat.push(item);
}
});
return flat;
}
var button = flatten(footer).flatten(div);
count = 0;
button.onclick = function() {
count += 1;
button.innerHTML = count;
};
I get the following error: ReferenceError: footer is not defined
Thanks
Here is my HTML code:
<div class="wrapper">
<div class="container">
<footer>
<div>
</div>
</footer>
</div>
</div>
Edit:
footer is defined in my HTML, I want to select footer in my function
Also, I can't add class or id to my html, I can't edit it
If, for the sake of practice (or a lost bet), you'd want to write your own querySelectorAll, you could write a recursive function that walks the DOM tree... The only thing you rely on is an entrance to the DOM: window.document.
Note that this will never be able to compete with the performance of your browser's default query implementations. We're just doing it to show we can.
Step 1: recursively walking the document (depth-first)
const walk = (el) => {
console.log(el.nodeName);
Array.from(el.children).forEach(walk);
};
walk(document);
<div class="wrapper">
<div class="container">
<footer>
<div>
</div>
</footer>
</div>
</div>
As you can see, this function loops over each element in the document and its children.
Step 2: Adding the filter logic
If you want it to actually find and return elements, you'll have to pass some sort of filtering logic. querySelectorAll works with string inputs, which you could try to recreate... Since we're redoing this for fun, our select will work with functions of HTMLElement -> bool.
const selectIn = (pred, el, result = []) => {
if (pred(el)) result.push(el);
Array.from(el.children)
.filter(e => e)
.map(el2 => selectIn(pred, el2, result));
return result;
}
// EXAMPLE APP
// Define some selectors
const withClass = className => el =>
el && el.classList && el.classList.contains(className);
const withTag = tagName => el =>
el && el.nodeName === tagName.toUpperCase();
// Select some elements
const footer = selectIn(withTag("footer"), document)[0];
const container = selectIn(withClass("container"), document)[0];
const divsInFooter = selectIn(withTag("div"), footer);
// Log the results
console.log(`
footer:
${footer.outerHTML}
container:
${container.outerHTML}
divsInFooter:
${divsInFooter.map(d => d.outerHTML)}
`);
<div class="wrapper"><div class="container"><footer><div></div></footer></div></div>

Extract all URL links from CSS file

I have a asset file with CSS and that contains properties as:
background-image: url(my-url.jpg)
Im trying to extract all images from the url(). How can i achieve that in JS?
If the style sheet is loaded to the page, you can get the StyleSheetList, and pick your style sheet using document.styleSheets. After selecting the style sheet, iterate it's rules with Array#reduce, extract the backgroundImage url using a regular expression, and if the result is not null (we have a url), push it into the urls array:
You can can get relevant s
const result = [...document.styleSheets[0].rules]
.reduce((urls, { style }) => {
var url = style.backgroundImage.match(/url\(\"(.+)\"\)/);
url && urls.push(url[1]);
return urls;
}, []);
console.log(result);
.bg {
width: 100px;
height: 100px;
}
.a {
background: url(http://lorempixel.com/200/200);
}
.b {
background: url(http://lorempixel.com/100/100);
}
<div class="bg a"></div>
<br />
<div class="bg b"></div>
I've extended #guest271314's answer with jQuery ajax. You may use it.
$(document).ready(function() {
$.get("main.css", function(cssContent){
let res = cssContent.match(/url\(.+(?=\))/g).map(url => url.replace(/url\(/, ""));
alert( res );
});
});
You can use .match(), .map() and .replace()
let res = CSSText.match(/url\(.+(?=\))/g).map(url => url.replace(/url\(/, ""));
I just wrote an updated version of this that iterates of all stylesheets on the page:
let css_urls = [...document.styleSheets]
.filter(sheet => {
try { return sheet.cssRules }
catch {}
})
.flatMap(sheet => Array.from(sheet.cssRules))
.filter(rule => rule.style)
.filter(rule => rule.style.backgroundImage !== '')
.filter(rule => rule.style.backgroundImage !== 'initial')
.filter(rule => rule.style.backgroundImage.includes("url"))
.reduce((urls, {style}) => {
urls.push(style.backgroundImage);
return urls;
}, []);
console.log(css_urls);

Apply style "cursor: pointer" to all React components with onClick function

I would like to apply the style cursor:pointer to all React elements that have an onClick function. I know I could do this to every element:
<a onClick={handleClick} style={{cursor:'pointer'}}>Click me</a>
or this:
<a onClick={handleClick} className="someClassWithCursorPointer">Click me</a>
But I'd much rather be able to just do something like this to apply the style to all elements:
<style>
[onclick] {
cursor: pointer;
}
</style>
But that won't work because there's no actual onclick attribute in the rendered HTML of an element when using React's onClick attribute.
Fiddle: https://jsfiddle.net/roj4p1gt/
I am not certain there's a good way to do this automatically without using some sort of mechanism that intercepts the creation of React elements and modifies them (or perhaps source level transforms). For example, using Babel, you could use babel-plugin-react-transform and add a className to all elements with an onClick prop using something along these lines (warning: pseudocode):
export default function addClassNamesToOnClickElements() {
return function wrap(ReactClass) {
const originalRender = ReactClass.prototype.render;
ReactClass.prototype.render = function render() {
var element = originalRender.apply(this, arguments);
return addClickClassNamesToApplicableElements(element);
}
return ReactClass;
}
}
function addClassNamesToApplicableElements(elem) {
if (!elem || typeof elem === "string") return elem;
const children = elem.children;
const modifiedChildren = elem.props.children.map(addClassNamesToApplicableElements);
if (elem.props.onClick) {
const className = elem.props.className || "";
return {
...elem,
props: {
...elem.props,
className: (className + " hasOnClick").trim(),
children: modifiedChildren
}
};
} else {
return {
...elem,
props: {
...elem.props,
children: modifiedChildren
}
}
};
}
Here's a quick example of the second part working: https://bit.ly/1kSFcsg
You can do that easily with css:
a:hover {
cursor:pointer;
}

Categories