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;
}
Related
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)
I have some conditional renders of textarea elements in many places on a form that shows/hides these elements depending on what the user is doing. For example:
<li v-if="Form.Type === 1">
<textarea v-model="Form.Title" ref="RefTitle"></textarea>
</li>
There could be any number of textarea elements like above. What I need to do is resize these elements at certain points in the lifecycle (e.g. onMounted, onUpdated).
The function that gets triggered to do this is:
setup() {
...
const RefTitle = ref(); // This is the ref element in the template
function autosizeTextarea() {
RefTitle.value.style.height = "35px"; // Default if value is empty
RefTitle.value.style.height = `${RefTitle.value.scrollHeight}px`;
}
...
}
In the code above I am specifically targeting a known textarea by its ref value of RefTitle. I could test for its existence using an if(RefTitle.value) statement.
But how could I get all the textarea elements that may be rendered and then run autosizeTextarea on all of them?
I can get all the textarea elements like such:
setup() {
...
function autosizeTextarea() {
const AllTextareas = document.getElementsByTagName('TEXTAREA');
for (let i=0; i < AllTextareas.length; i++) {
// How can I set the style.height = `${RefTitle.value.scrollHeight}px`;
// in here for each element?
}
}
...
}
But how can style.height be set on all of them?
You could create your own custom component representing a textarea with the functionality in the component itself, so you don't have to get all textareas which are dynamically created.
It could look something like this:
<template>
<textarea :value="modelValue" #input="$emit('update:modelValue', $event.target.value)" ref="textarea" :style="styleObject"></textarea>
</template>
<script>
export default {
emits: {
'update:modelValue': null,
},
props: {
modelValue: {
type: String,
},
// Prop for dynamic styling
defaultHeight: {
type: Number,
required: false,
default: 35,
validator(val) {
// Custom Validator to ensure that there are no crazy values
return val > 10 && val < 100;
}
},
computed: {
styleObject() {
return {
height: this.$refs['textarea'].value.scrollHeight ? `${this.$refs['textarea'].value.scrollHeight}px` : `${this.defaultHeight}px`,
}
},
</script>
That way you can even use v-model on it.
<li v-if="Form.Type === 1">
<custom-textarea v-model="Form.Title" :defaultHeight="45"></textarea>
</li>
The Template I provided is just to show you how a custom component could look like. You might have to fit it into your logic depending on when you actually want things to change/trigger.
I have managed to do it like this:
const AllTextareas = ref(document.getElementsByTagName("TEXTAREA")); //returns an object not an array
for (const [key, value] of Object.entries(AllTextareas.value)) {
AllTextareas.value[key].style.height = AllTextareas.value[key].scrollHeight ? `${AllTextareas.value[key].scrollHeight}px` : "35px";
}
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
I have got a question regarding using a variable within the background-image of the style template. Is this possible at all?
I would like to achieve something like this:
<script>
export default {
name: 'setCss',
computed: {
cssVars() {
var ua = navigator.userAgent.toLowerCase();
var isAndroid = ua.indexOf("android") > -1; //&& ua.indexOf("mobile");
var path = '';
if(isAndroid) {
path = 'img/samsung/';
} else{
path = 'img/iphone/';
}
return {
'--bg-path': path,
}
}
}
};
</script>
and then with this CSS:
<style scoped>
div {
background-image: var(--bg-path) + '/1366x400#2x.jpg';
background-size: 1366px 400px;
}
#media not all,
not all,
only screen and (max-width: 480px) {
div {
background-image: var(--bg-path) + '/480x288.jpg';
background-size: 480px 288px;
}
}
</style>
This will not work but I am wondering if there is a way to do this?
For this to work, you will simply need to pass the computed property cssVars to the component as dynamic style attribute like:
<button :style="cssVars">My button</button>
Here is a working demo:
(here I am passing a hardcoded value for bgColor, but you can assume that being passed as a prop to a component also)
new Vue({
el: "#myApp",
data: {
bgColor: "green"
},
computed: {
cssVars() {
return {
'--bg-color': this.bgColor,
}
}
}
})
button {
background-color: var(--bg-color);
padding:10px 20px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="myApp">
<button :style="cssVars">My button</button>
</div>
You will need to use class/style binding.
In your case, binding style would look something like the following. First, you would need to bind a style with the computed background image (the rest of the styles don't need to be bound, they can remain in your style tag).
<template>
<div :style="{backgroundImage: computedBgImage}"></div>
</template>
...
computed:{
computedBgImage() {
let path;
if (isAndroid) {
path = 'img/samsung/';
} else{
path = 'img/iphone/';
}
return path + '/480x288.jpg'
}
}
...
If you need to use the same image in multiple places, consider defining a class for each image, and binding the class, instead of the style, as explained in the above link.
I would like to add and remove 'over' from my class on an element created using a lit-html template triggered by 'dragEnter' and 'dragLeave':
#app {
background-color: #72a2bc;
border: 8px dashed transparent;
transition: background-color 0.2s, border-color 0.2s;
}
#app.over {
background-color: #a2cee0;
border-color: #72a2bc;
}
const filesTemplate = () =>
html`
<button id="app"
#dragover=${??}
#dragleave=${??}
>
Click Me
</button>
`;
In my old system I called these methods in a separate module via an event emitter, but I am hoping I can make it all defined in the template using lit-html.
dragEnter(e) {
this.view.element.className += ' over';
}
dragLeave(e) {
this.view.element.className = element.className.replace(' over', '');
}
It depends what your custom element looks like. With your template you could just put #dragover=${this.dragEnter}. However, if you want this to apply to your entire custom element and not just the button you can do something like this:
connectedCallback() {
super.connectedCallback();
this.addEventListener('dragover', this.dragEnter);
}
If you do not have custom element and just use lit-html by itself you have to put your event handlers dragEnter(e)and dragLeave(e) into the template like so: #dragover=${this.dragEnter}
You need to add the class with classList.add in dragEnter and remove it in dragLeave. In the future you maybe can use classMap directive in lit-html, however there is nothing wrong with just using classList. I would stick with just using classList. In a very distant future css might also have a selector for it: Is there a CSS ":drop-hover" pseudo-class?
I think that, in order to solve the problem in a "lit-html style", the solution has to be something like this:
import { html, render} from 'lit-html';
import { classMap } from 'lit-html/directives/class-map.js';
const myBtnClasses = {
over: false
};
function dragEnter(e) {
myBtnClasses.over = true;
renderFiles();
}
function dragLeave(e) {
myBtnClasses.over = false;
renderFiles();
}
const filesTemplate = (classes) =>
html`
<button id="app" class="${classMap(myBtnClasses)}"
#dragover=${dragEnter} #dragleave=${dragLeave}
>
Click Me
</button>
`;
function renderFiles() {
render(filesTemplate(myBtnClasses), YOUR_CONTAINER);
}
When using lit-html you have to express your UI as a function of your "state" and "rerender" each time your state changes, so the best solution in this little example is to consider your classes as part of your state.
Anyway better than
this.view.element.className += ' over';
is
this.view.element.classList.add('over');
And instead
this.view.element.className = element.className.replace(' over', '');
use
this.view.element.classList.remove('over');
This is better because of allowing to avoid many bugs like adding the same class many times.
I do not know lit-html but try
let sayHello = (name, myClass) => html`<h1 class="${myClass}">Hello ${name}</h1>`;
https://lit-html.polymer-project.org/