Trying to use React.DOM to set body styles - javascript

How can I use React.DOM to change styles on HTML body?
I tried this code and it's not working:
var MyView = React.createClass({
render: function() {
return (
<div>
React.DOM.body.style.backgroundColor = "green";
Stuff goes here.
</div>
);
}
});
If you execute this from the browsers console it works (but I need it working in ReactJS code):
document.body.style.backgroundColor = "green";
Also see this question for similar but different solution:
Change page background color with each route using ReactJS and React Router?

Assuming your body tag isn't part of another React component, just change it as usual:
document.body.style.backgroundColor = "green";
//elsewhere..
return (
<div>
Stuff goes here.
</div>
);
It's recommended to put it at componentWillMount method, and cancel it at componentWillUnmount:
componentWillMount: function(){
document.body.style.backgroundColor = "green";
}
componentWillUnmount: function(){
document.body.style.backgroundColor = null;
}

With functional components and useEffect hook :
useEffect(() => {
document.body.classList.add('bg-black');
return () => {
document.body.classList.remove('bg-black');
};
});

A good solution to load multiple atributes from a js class to the document body can be:
componentWillMount: function(){
for(i in styles.body){
document.body.style[i] = styles.body[i];
}
},
componentWillUnmount: function(){
for(i in styles.body){
document.body.style[i] = null;
}
},
And after you write your body style as long as you want:
var styles = {
body: {
fontFamily: 'roboto',
fontSize: 13,
lineHeight: 1.4,
color: '#5e5e5e',
backgroundColor: '#edecec',
overflow: 'auto'
}
}

The best way to load or append extra classes is by adding the code in componentDidMount().
Tested with react and meteor :
componentDidMount() {
var orig = document.body.className;
console.log(orig); //Just in-case to check if your code is working or not
document.body.className = orig + (orig ? ' ' : '') + 'gray-bg'; //Here gray-by is the bg color which I have set
}
componentWillUnmount() {
document.body.className = orig ;
}

This is what I ended up using.
import { useEffect } from "react";
export function useBodyStyle(style: any){
useEffect(()=>{
for(var key in style){
window.document.body.style[key as any] = style[key];
}
return () => {
window.document.body.style[key as any] = '';
}
}, [style])
}

Even if you can set body styles from react with the provided answer, I prefer if a component is only responsible for setting its own style.
In my case there was an alternative solution. I needed to change the body backgroundColor. This could easily be achieved without changing the body style in a react component.
First I added this style to the index.html header.
<style>
html, body, #react-app {
margin: 0;
height: 100%;
}
</style>
Then, in my outermost component, I set the backgroundColor to the needed value and the height to 100%.

Related

How to make web component to redender specific elements upon property update

class MyElement extends HTMLElement {
constructor() {
super();
// Props
this._color = this.getAttribute("color");
this._myArray = this.getAttribute("myArray");
// data
// Shadow DOM
this._shadowRoot = this.attachShadow({ mode: "open" });
this.render();
}
template() {
const template = document.createElement("template");
template.innerHTML = `
<style>
:host {
display: block;
}
span {color: ${this.color}}
</style>
<p>Notice the console displays three renders: the original, when color changes to blue after 2 secs, and when the array gets values</p>
<p>The color is: <span>${this.color}</span></p>
<p>The array is: ${this.myArray}</p>
`;
return template;
}
get color() {
return this._color;
}
set color(value) {
this._color = value;
this.render();
}
get myArray() {
return this._myArray;
}
set myArray(value) {
this._myArray = value;
this.render();
}
render() {
// Debug only
const props = Object.getOwnPropertyNames(this).map(prop => {
return this[prop]
})
console.log('Parent render; ', JSON.stringify(props));
// end debug
this._shadowRoot.innerHTML = '';
this._shadowRoot.appendChild(this.template().content.cloneNode(true));
}
}
window.customElements.define('my-element', MyElement);
<!DOCTYPE html>
<head>
<script type="module" src="./src/my-element.js" type="module"></script>
<!-- <script type="module" src="./src/child-element.js" type="module"></script> -->
</head>
<body>
<p><span>Outside component</span> </p>
<my-element color="green"></my-element>
<script>
setTimeout(() => {
document.querySelector('my-element').color = 'blue';
document.querySelector('my-element').myArray = [1, 2, 3];
}, 2000);
</script>
</body>
I have a native web component whose attributes and properties may change (using getters/setters). When they do, the whole component rerenders, including all children they may have.
I need to rerender only the elements in the template that are affected.
import {ChildElement} from './child-element.js';
class MyElement extends HTMLElement {
constructor() {
super();
// Props
this._color = this.getAttribute("color");
this._myArray = this.getAttribute("myArray");
// Shadow DOM
this._shadowRoot = this.attachShadow({ mode: "open" });
this.render();
}
template() {
const template = document.createElement("template");
template.innerHTML = `
<style>
span {color: ${this.color}}
</style>
<p>The color is: <span>${this.color}</span></p>
<p>The array is: ${this.myArray}</p>
<child-element></child-element>
`;
return template;
}
get color() {
return this._color;
}
set color(value) {
this._color = value;
this.render(); // It rerenders the whole component
}
get myArray() {
return this._myArray;
}
set myArray(value) {
this._myArray = value;
this.render();
}
render() {
this._shadowRoot.innerHTML = '';
this._shadowRoot.appendChild(this.template().content.cloneNode(true));
}
}
window.customElements.define('my-element', MyElement);
window.customElements.define('child-element', ChildElement);
Because each setter calls render(), the whole component, including children unaffected by the updated property, rerenders.
Yes, if you go native you have to program all reactivity yourself.
(but you are not loading any dependencies)
Not complex, Your code can be simplified;
and you probably want to introduce static get observedAttributes and the attributeChangedCallback to automatically listen for attribute changes
customElements.define('my-element', class extends HTMLElement {
constructor() {
super().attachShadow({ mode: "open" }).innerHTML = `
<style id="STYLE"></style>
<p>The color is: <span id="COLOR"/></p>
<p>The array is: <span id="ARRAY"/></p>`;
}
connectedCallback() {
// runs on the OPENING tag, attributes can be read
this.color = this.getAttribute("color");
this.myArray = this.getAttribute("myArray"); // a STRING!!
}
get color() {
return this._color;
}
set color(value) {
this.setDOM("COLOR" , this._color = value );
this.setDOM("STYLE" , `span { color: ${value} }`);
}
get myArray() {
return this._myArray;
}
set myArray(value) {
this.setDOM("ARRAY" , this._myArray = value );
}
setDOM(id,html){
this.shadowRoot.getElementById(id).innerHTML = html;
}
});
<my-element color="green" myArray="[1,2,3]"></my-element>
<my-element color="red" myArray="['foo','bar']"></my-element>
You are missing a certain level of abstraction. You are trying to emulate the Vue/React way of doing things where whenever the props change, the render() function is repeatedly called. But in these frameworks, render function doesn't do any DOM manipulation. It is simply working on Virtual DOM. Virtual DOM is simply a tree of JS object and thus very fast. And, that's the abstraction we are talking about.
In your case, it is best if you rely on some abstraction like LitElement or Stencil, etc. - any web component creation framework. They would take care of handling such surgical updates. It also takes care of scheduling scenarios where multiple properties are changed in a single event loop but render is still called exactly once.
Having said that, if you want to do this by yourself, one important rule: Never read or write to DOM from the web component constructor function. Access to DOM is after the connectedCallback lifecycle event.
Here is the sample that you can try and expand:
import { ChildElement } from './child-element.js';
class MyElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({ mode: "open" });
this.attached = false;
}
connectedCallback() {
// Check to ensure that initialization is done exactly once.
if (this.attached) {
return;
}
// Props
this._color = this.getAttribute("color");
this._myArray = this.getAttribute("myArray");
// Shadow DOM
this.render();
this.attached = true;
}
template() {
const template = document.createElement("template");
template.innerHTML = `
<style>
span {color: ${this.color}}
</style>
<p>The color is: <span id="span">${this.color}</span></p>
<p>The array is: ${this.myArray}</p>
<child-element></child-element>
`;
return template;
}
get color() {
return this._color;
}
set color(value) {
this._color = value;
this.shadowRoot.querySelector('#span').textContent = this._color;
this.shadowRoot.querySelector('style').textContent = `
span { color: ${this._color}; }
`;
}
get myArray() {
return this._myArray;
}
set myArray(value) {
this._myArray = value;
this.shadowRoot.querySelector('#span').innerHTML = this._myArray;
}
render() {
this._shadowRoot.innerHTML = '';
this._shadowRoot.appendChild(this.template().content.cloneNode(true));
}
}
window.customElements.define('my-element', MyElement);
window.customElements.define('child-element', ChildElement);
The above example is a classical vanilla JS with imperative way of updating the DOM. As said earlier to have reactive way of updating the DOM, you should build your own abstraction or rely on library provided abstraction.

Add CSS rules from textarea using v-html

I am building a WYSIWYG type application where a user can write CSS in a textarea and that CSS rule will be applied to the HTML on page i tried something like this in template
<textarea v-bind="css"></textarea>
<style v-html="css"></style>
VueCompilerError: Tags with side effect ( and ) are ignored in client component templates.
Old answer, below is better one
Add textarea with v-model:
<textarea v-model="css" />
You can create style tag in onMounted hook:
onMounted(() => {
const style = document.createElement("style");
style.type = "text/css";
updateCss(style, css.value);
document.getElementsByTagName("head")[0].appendChild(style);
el.value = style;
});
You must be able to access this element later, so assign style to
el.value.
Then add watch on input value:
watch(css, () => {
updateCss(el.value, css.value);
});
Where updateCss is a function:
const updateCss = (el, css) => {
if (el.styleSheet) {
el.styleSheet.cssText = css.value;
} else {
el.innerHTML = "";
el.appendChild(document.createTextNode(css));
}
};
Demo:
https://codesandbox.io/s/cocky-mestorf-uqz6f?file=/src/App.vue:246-463
Edit
I found much better solution:
<template>
<textarea v-model="css" />
<component :is="'style'" type="text/css" v-text="css"></component>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const css = ref("body { background-color: blue; }");
return { css };
},
};
</script>
Component doesn't throw the error about style tag:
<component :is="'style'">
Note that there is v-text instead v-html. V-html could be unsafe.
Demo:
https://codesandbox.io/s/festive-germain-q9tg3?file=/src/App.vue:122-281

Change direction with selecting Language in Vuejs

I have a Language select-option. If I choose Arabic then it will change the direction.
* {
direction: rtl!important;
}
While I am using this, then the whole direction changed to right to left. But how can I do that with Methods?
methods: {
languageSelection() {
if(this.lang == 'arb') {
document.getElementsByTagName("*")[0].style.direction = "rtl!important";
document.getElementsByTagName("html")[0].style.direction = "rtl!important";
document.getElementsByTagName("body")[0].style.direction = "rtl!important";
}
}
}
The above code is not working!
If I can add a CSS file then it will be better for me. For example:
languageSelection() {
if(this.lang == 'arb') {
// active a css file like: style-rtl.css
}
}
But how is this possible?
Ok. So when you use getElementsByTagName("*")[0] you will probably get a handle to a <html> element. So the same element you're accessing in the next line.
To change all elements direction you would have to iterate over the collection:
const elems = document.getElementsByTagName("*")
for (let elem of elems) {
elem.style.direction = 'rtl'
}
But this will still include <script>, <style>, <meta> tags, which is not the best solution.
My solution
I would create class in the css
html.is-rtl * {
direction: rtl;
}
then just toggle the class when you select the language which is read from right to left.
languageSelection() {
if (this.lang == 'arb') {
document.querySelector('html').classList.add('is-rtl')
}
}
Could create a stylesheet and append it to the head:
languageSelection() {
if(this.lang == 'arb') {
const style = document.createElement('style');
style.innerHTML = `*{direction:rtl!important}`;
document.head.appendChild(style);
}
}
You should manage it inside App.vue. add custom css class based on chosen language to #App element.
<template>
<div id="app" :class="{'dir-rtl': isRtl}">
...
</div>
</template>
<script>
export default {
computed: {
isRtl () {
return this.lang === 'arb'
}
}
}
</script>
<style lang="scss">
.dir-rtl {
direction: rtl !important;
}
</style>
best place to save and change lang is vuex store.

How can I conditionally show title tooltip if CSS ellipsis is active within a React component

Problem:
I'm looking for a clean way to show a title tooltip on items that have a CSS ellipsis applied. (Within a React component)
What I've tried:
I setup a ref, but it doesn't exist until componentDidUpdate, so within componentDidUpdate I forceUpdate. (This needs some more rework to handle prop changes and such and I would probably use setState instead.) This kind of works but there are a lot of caveats that I feel are unacceptable.
setState/forceUpdate - Maybe this is a necessary evil
What if the browser size changes? Do I need to re-render with every resize? I suppose I'd need a debounce on that as well. Yuck.
Question:
Is there a more graceful way to accomplish this goal?
Semi-functional MCVE:
https://codepen.io/anon/pen/mjYzMM
class App extends React.Component {
render() {
return (
<div>
<Test message="Overflow Ellipsis" />
<Test message="Fits" />
</div>
);
}
}
class Test extends React.Component {
constructor(props) {
super(props);
this.element = React.createRef();
}
componentDidMount() {
this.forceUpdate();
}
doesTextFit = () => {
if (!this.element) return false;
if (!this.element.current) return false;
console.log(
"***",
"offsetWidth: ",
this.element.current.offsetWidth,
"scrollWidth:",
this.element.current.scrollWidth,
"doesTextFit?",
this.element.current.scrollWidth <= this.element.current.offsetWidth
);
return this.element.current.scrollWidth <= this.element.current.offsetWidth;
};
render() {
return (
<p
className="collapse"
ref={this.element}
title={this.doesTextFit() ? "it fits!" : "overflow"}
>
{this.props.message}
</p>
);
}
}
ReactDOM.render(<App />, document.getElementById("container"));
.collapse {
width:60px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="container"></div>
Since a lot of people are still viewing this question. I did finally figure out how to do it. I'll try to rewrite this into a working example at some point but here's the gist.
// Setup a ref
const labelRef = useRef(null);
// State for tracking if ellipsis is active
const [isEllipsisActive, setIsEllipsisActive] = useState(false);
// Setup a use effect
useEffect(() => {
if(labelRef?.current?.offsetWidth < labelRef?.current?.scrollWidth) {
setIsEllipsisActive(true);
}
}, [labelRef?.current, value, isLoading]); // I was also tracking if the data was loading
// Div you want to check if ellipsis is active
<div ref={labelRef}>{value}</div>
I use this framework agnostic snippet to this. Just include it on your page and see the magic happen ;)
(function() {
let lastMouseOverElement = null;
document.addEventListener("mouseover", function(event) {
let element = event.target;
if (element instanceof Element && element != lastMouseOverElement) {
lastMouseOverElement = element;
const style = window.getComputedStyle(element);
const whiteSpace = style.getPropertyValue("white-space");
const textOverflow = style.getPropertyValue("text-overflow");
if (whiteSpace == "nowrap" && textOverflow == "ellipsis" && element.offsetWidth < element.scrollWidth) {
element.setAttribute("title", element.textContent);
} else {
element.removeAttribute("title");
}
}
});
})();
From:
https://gist.github.com/JoackimPennerup/06592b655402d1d6181af32def40189d

Get div's offsetTop positions in React

I am trying to implement a List view in React.
What I am trying to achieve is that to store the list headers informations and register the components and register the scroll event.
every time when user scroll the window, I'd like to take out the stored div and re-calculate the offsetTop data.
The problem now is that, I found the console just print out the initial value (the value is fixed and never changed) offsetTop data never change in onscroll function.
Anyone suggest how to get latest offsetTop from the _instances object?
import React, { Component } from 'react';
import ListHeader from './lib/ListHeader';
import ListItems from './lib/ListItems';
const styles = {
'height': '400px',
'overflowY': 'auto',
'outline': '1px dashed red',
'width': '40%'
};
class HeaderPosInfo {
constructor(headerObj, originalPosition, originalHeight) {
this.headerObj = headerObj;
this.originalPosition = originalPosition;
this.originalHeight = originalHeight;
}
}
export default class ReactListView extends Component {
static defaultProps = {
events: ['scroll', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll', 'resize', 'touchmove', 'touchend'],
_instances:[],
_positionMap: new Set(),
_topPos:'',
_topWrapper:''
}
static propTypes = {
data: React.PropTypes.array.isRequired,
headerAttName: React.PropTypes.string.isRequired,
itemsAttName: React.PropTypes.string.isRequired,
events: React.PropTypes.array,
_instances: React.PropTypes.array,
_positionMap: React.PropTypes.object,
_topPos: React.PropTypes.string,
_topWrapper: React.PropTypes.object
};
state = {
events: this.props.events,
_instances: this.props._instances,
_positionMap: this.props._positionMap,
_topPos: this.props._topPos
}
componentDidMount() {
this.initStickyHeaders();
}
componentWillUnmount() {
}
componentDidUpdate() {
}
refsToArray(ctx, prefix){
let results = [];
for (let i=0;;i++){
let ref = ctx.refs[prefix + '-' + String(i)];
if (ref) results.push(ref);
else return results;
}
}
initHeaderPositions() {
// Retrieve all instance of headers and store position info
this.props._instances.forEach((k)=>{
this.props._positionMap.add(new HeaderPosInfo(
k,
k.refs.header.getDOMNode().offsetTop,
k.refs.header.getDOMNode().offsetHeight
));
});
let it = this.props._positionMap.values();
let first = it.next();
this.props._topPos = first.value.originalPosition;
this.props._topWrapper = first.value.headerObj;
}
initStickyHeaders () {
this.props._instances = this.refsToArray(this, 'ListHeader');
this.initHeaderPositions();
// Register events listeners with the listview div
this.props.events.forEach(type => {
if (window.addEventListener) {
React.findDOMNode(this.refs.listview).addEventListener(type, this.onScroll.bind(this), false);
} else {
React.findDOMNode(this.refs.listview).attachEvent('on' + type, this.onScroll.bind(this), false);
}
});
}
onScroll() {
// update current header positions and apply fixed positions to the top one
console.log(1);
let offsetTop = React.findDOMNode(this.props._instances[0].refs.header).offsetTop;
}
render() {
const { data, headerAttName, itemsAttName } = this.props;
let _refi = 0;
let makeRef = () => {
return 'ListHeader-' + (_refi++);
};
return (
<div ref="listview" style={styles}>
{
Object.keys(data).map(k => {
const header = data[k][headerAttName];
const items = data[k][itemsAttName];
return (
<ul key={k}>
<ListHeader ref={makeRef()} header={header} />
<ListItems items={items} />
</ul>
);
})
}
</div>
);
}
}
The whole source code is on Github, you can clone and compile it from here:
Github
You may be encouraged to use the Element.getBoundingClientRect() method to get the top offset of your element. This method provides the full offset values (left, top, right, bottom, width, height) of your element in the viewport.
Check the John Resig's post describing how helpful this method is.
I do realize that the author asks question in relation to a class-based component, however I think it's worth mentioning that as of React 16.8.0 (February 6, 2019) you can take advantage of hooks in function-based components.
Example code:
import { useRef } from 'react'
function Component() {
const inputRef = useRef()
return (
<input ref={inputRef} />
<div
onScroll={() => {
const { offsetTop } = inputRef.current
...
}}
>
)
}
Eugene's answer uses the correct function to get the data, but for posterity I'd like to spell out exactly how to use it in React v0.14+ (according to this answer):
import ReactDOM from 'react-dom';
//...
componentDidMount() {
var rect = ReactDOM.findDOMNode(this)
.getBoundingClientRect()
}
Is working for me perfectly, and I'm using the data to scroll to the top of the new component that just mounted.
A quicker way if you are using React 16.3 and above is by creating a ref in the constructor, then attaching it to the component you wish to use with as shown below.
...
constructor(props){
...
//create a ref
this.someRefName = React.createRef();
}
onScroll(){
let offsetTop = this.someRefName.current.offsetTop;
}
render(){
...
<Component ref={this.someRefName} />
}
import ReactDOM from 'react-dom';
//...
componentDidMount() {
var n = ReactDOM.findDOMNode(this);
console.log(n.offsetTop);
}
You can just grab the offsetTop from the Node.
A better solution with ref to avoid findDOMNode that is discouraged.
...
onScroll() {
let offsetTop = this.instance.getBoundingClientRect().top;
}
...
render() {
...
<Component ref={(el) => this.instance = el } />
...
onScroll has a events whict contains all the native and child elements inside this div so u can use it like this shown below and get the targetted element offsetTop.
const getoffSet = e => {
console.log(e, e.natiiveEvent.target.childNodes[0].offsetTop)
}
return (
<div onScroll={(e) => getoffSet(e)} ref={listview} style={styles}>
</div>
)
Checking if height Property Is Not Set on Parent:
If the parent element has no height set then the sticky element won't
have any area to stick to when scrolling. This happens because the
sticky element is meant to stick/scroll within the height of a
container.
Checking if a Parent Element Is a Flexbox
If sticky element's parent is a flexbox, there are two scenarios to
check for:
The sticky element has align-self: auto set (which is the default);
The sticky element has align-self: stretch set. If the Sticky Element
Has align-self: auto Set: In this case the value of align-self would
compute to the parent's align-items value. So,
if the parent has align-items: normal (which is the default) or
align-items: stretch set, then it means the height of the sticky element would stretch to fill the entire available space. This would leave no room for the sticky element to scroll within the parent.
If the Sticky Element Has align-self: stretch Set:
In this case, the sticky element would stretch to the height of the parent, and would not have any area to scroll within.
How to Make Sticky Element Scrollable Within a Flexbox:
You could simply set the value of the align-self property to align-self: flex-start. This would put the sticky element at the start and won't stretch it.enter link description here

Categories

Resources