React JS - cloneElement example? - reactjs

When I change the inline style of a component depending on the state, I get this error.
Warning: `div` was passed a style object that has previously been mutated. Mutating `style` is deprecated.
In my render function, I'm calling this function before the return, to check a property.
this._isGameOver();
_isGameOver:
_isGameOver()
{
if (this.props.gameOver === false)
{
style.well.backgound = '#f5f5f5';
style.h1.color = '#32936F';
}
else
{
style.well.background = 'red';
style.h1.color = 'white';
}
}
So where and how do I use this clone? The documentation doesn't give any solid examples.
Sean

No need for cloneElement in your example, simply return a new style object instead of mutating the existing one.
_isGameOver() {
if (this.props.gameOver === false) {
return {
well: { background: '#f5f5f5' },
h1: { color: '#32936F' }
}
}
else return {
well: { background: 'red' },
h1: { color: 'white' }
}
}
Then you can merge the new styles with the old styles wherever you need using something like Object.assign
var newStyle = this._isGameOver()
return <h1 style={Object.assign({}, style.h1, newStyle.h1)} />

I'm guessing that you're defining that style object outside of your component (perhaps importing it?). So wherever you're going to modify that style you need to make a clone of it.
I always import as baseStyles (to indicate it shouldn't be mutated) and use lodash.cloneDeep() (because cloning deep objects is fiddly to do yourself).
So your function becomes
import cloneDeep from 'lodash/cloneDeep';
import baseStyle from '../blah/yourStyle.js';
_isGameOver()
{
const style = cloneDeep(baseStyle);
if (this.props.gameOver === false)
{
style.well.backgound = '#f5f5f5';
style.h1.color = '#32936F';
}
else
{
style.well.background = 'red';
style.h1.color = 'white';
}
}
You could also do something like:
const wellStyle = {
...style.well,
background: '$f5f5f5',
}
and then pass wellStyle to your component rather than style.well.
It might not suit your situation, but I only change styles inside the actual render method. It keeps everything in the one place (cloning your import in more than one place won't work).

Related

Best practice to create reusable and decoupled React components in 18n/l10n apps with custom formatting

I have the following React component which I would like to reuse in several applications:
import React from "react";
import { useFactory } from "react-js-utl/hooks";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
/**
* Shows current account balance with variation since previous balance.
*/
const Balance = compose(React.memo)(function Balance({
current,
previous,
label,
sinceLabel,
increaseColor = "light-green",
increaseIcon = "arrow-up",
decreaseColor = "red",
decreaseIcon = "arrow-down",
sameColor = "blue",
sameIcon = "equals",
formatNum = void 0,
formatPerc = void 0
} = {}) {
// Compute the change.
let change = ((current - previous) / previous) * 100;
// Implementation detail. Switch objects depeding on runtime value of change.
const outcomeFactory = useFactory(
() => [
// Increase:
[
change > 0, // Condition, if true, the object following it will be returned by `useFactory()`.
{
outcome: "increase",
color: increaseColor,
icon: increaseIcon
}
],
// Decrease:
[
change < 0,
{
outcome: "decrease",
color: decreaseColor,
icon: decreaseIcon
}
],
// Same (default, returned if the previous conditions evaluate to false):
{
outcome: "same",
color: sameColor,
icon: sameIcon
}
],
// deps array
[
change,
increaseColor,
increaseIcon,
decreaseColor,
decreaseIcon,
sameColor,
sameIcon
]
);
change = Math.abs(change);
return (
<div className="balance">
<div className="balance-label">{label}</div>
<div className="balance-current">
{formatNum ? formatNum(current) : current}
</div>
<span
className={`balance-outcome balance-outcome-${outcomeFactory.outcome} balance-outcome-${outcomeFactory.color}`}
>
{typeof outcomeFactory.icon === "string" ? (
// Defaults to FontAwesome's icon.
<FontAwesomeIcon icon={outcomeFactory.icon} />
) : (
// Custom component rendered by client code.
outcomeFactory.icon
)}
{outcomeFactory.outcome === "same"
? // Same (no change since previous):
""
: // Increase or decrease since previous:
// Edge case: previous balance was zero
Number(previous) === 0
? ""
: // Percentage increase/decrease:
formatPerc
? formatPerc(change)
: change}
</span>
<span className="balance-since-label">{sinceLabel}</span>
</div>
);
});
Balance.displayName = "Balance";
export default Balance;
Which I use in an app like this:
...
function myFormattingFunctionForNumbers(t, num) {
// I use accounting.js to format numbers:
return accounting.format(num, {
decimal: t("formatting:numbers.decimal_separator"), // For EN lang this will be "."
thousand: t("formatting:numbers.thousand_separator") // For EN this this will be ","
})
}
function myFormattingFunctionForPercentages(t, perc) {
return myFormattingFunctionForNumbers(t, perc) + "%";
}
...
// Inside app component:
// Using react-i18next:
const { t, i18n } = useTranslation();
// If the language changes, so does formatting:
formatNum = useCallback(current => myFormattingFunctionForNumbers(t, current), [t, i18n.language])
formatPerc = useCallback(changePerc => myFormattingFunctionForPercentages(t, changePerc), [t, i18n.language])
...
<Balance
current={11234.56} // These hardcoded values could be props, of course.
previous={9321.45}
label={t("Your account balance")} // i18n
sinceLabel={t("Since previous month")} // i18n
formatNum={formatNum} // Function to format current value.
formatPerc={formatPerc} // Function to format change/variation percentage.
/>
...
Which outputs something looking roughly like this:
Your account balance
11,234.56
↑ 20,52% Since previous month
Right now I am facing the following "issues":
The reusable component is pure (uses React.memo), so its formatting functions formatNum and formatPerc need to change when the app's language changes even if the other props like current and previous don't change because a different language potentially involves different formatting and therefore the component should rerender;
Because of point 1, the client is responsible of wiring all the formatting functions inside the consuming component using useCallback which creates lot of biolerplate...;
The two useCallbacks do not warn me that I need to pass i18n.language to the deps array, simply because the current language is not referenced directly by the formatting functions myFormattingFunctionForNumbers and myFormattingFunctionForPercentages, which only use the t function of i18next (which as far as I know does not change when the language changes);
Maybe there is a point 4 and even a point 5, of which I am not aware of for now.
What is the current best practice for reusable React components which support formatting and i18n/l10n?
Tips and tricks on how to organize formatting/i18n/l10n code and separate these concerns from reusable components will be appreciated.
Thank you for the attention.

elegant way to downgrade all angular components for angular js

My app is supposed to migrate from angularjs to angular.
I'm creating new angular components. Is there an elegant way to automatically import and downgrade component?
Current code:
import { ColorPickerComponent } from './angular-comp/color-picker/color-picker.component';
import {FileSelectComponent } from './angular-comp/file-select/file-select.component';
export default angular
.module('kn-components', myModuleNames)
.directive('colorPicker', downgradeComponent({component: ColorPickerComponent}))
.directive('fileSelect', downgradeComponent({component: FileSelectComponent}))
.name;
Each time I create a component I need to do it, it's quite verbose....
For my angularjs component, for example, I did the following:
const myModuleNames = [];
const loadModules = require.context(".", true, /\.module.js$/);
loadModules.keys().forEach(function (key) {
if(loadModules(key).default)
myModuleNames.push(loadModules(key).default);
});
then:
export default angular
.module('kn-components', myModuleNames)
and all my modules/components are imported
If the goal is to get rid of boilerplate code, you can get list of components to upgrade, get the selector name for each component and register corresponding directive
Get list of your components. This depends on your code structure. The simplest option is to just explicitly return components you need to downgrade. Or you can use require.context as you did in your example.
function getComponents() {
// depends on how your components are organized
// for example, iterate over require.context(".", true, /\.component.ts$/);
// or return fixed array
return [ColorPickerComponent, FileSelectComponent];
}
For each component get the selector name. If you don't use AOT compilation, you can get selector value from the #Component decorator. But if you do use it, that won't work and you can make a selector from a component name
function getComponentSelector(component) {
// if you don't need AOT
return toCamelCase(component.__annotations__[0].selector);
// if you do need it
let name: string = component.name;
const suffix = 'Component';
if (name.endsWith(suffix)) {
name = name.substr(0, name.length - suffix.length);
}
return uncapitalize(name);
}
function toCamelCase(selector: string) {
const splitted = selector.split('-');
for (let i = 1; i < splitted.length; i++) {
splitted[i] = capitalize(splitted[i]);
}
return splitted.join('');
}
function capitalize(name: string) {
return name.charAt(0).toUpperCase() + name.slice(1);
}
function uncapitalize(name: string) {
return name.charAt(0).toLowerCase() + name.slice(1);
}
Iterate over your components and register downgraded components
downgradeComponents(angular.module('kn-components'));
function downgradeComponents(module: ng.IModule) {
const components = getComponents();
for (const component of components) {
const selector = getComponentSelector(component);
module.directive(selector, downgradeComponent({ component }));
}
}

Es6 class with event handlers and issues with context/scope

I have a container with two panes. I am making one of them re-sizable and absolute, and the other adjusts in real time based on the size of the first.
Unfortunately I am having problems with the es6 "resize-controller" that I just wrote. Because I made the wrong decision to use arrow functions, I am unable to remove the event listeners. Other than that and a bug with my math, it is functioning as expected. However when I tried to fix my mistakes with several possible solutions, I either get no functionality or errors about context and functions not being functions.
Hoping someone here could take a look at my exact setup and show me how it should be. Replacing the Arrow functions with normal functions, with or without event params, does not work. Would I be better off ditching Es6 classes for this one? Perhaps I have unlocked a few layers of complexity.
class ResizeController {
constructor(){
this.chatContainer = document.getElementById("chatContainer");
this.messageWindowContainer = document.getElementById("messageWindowContainer");
this.messageWindowContentContainer = document.getElementById("messageWindowContentContainer");
this.messageWindowContent = document.getElementById("messageWindowContent");
this.startX = "";
this.startWidth = this.getMessageWindowWidth();
}
init(){
this.messageWindowContainer.addEventListener('mousedown', (e)=>{e.stopPropagation()});
this.messageWindowContentContainer.addEventListener('mousedown', (e)=>{this.onMouseDown(e)});
this.messageWindowContent.addEventListener('mousedown', (e)=>{e.stopPropagation()})
}
onMouseDown(e) {
this.onDown(e);
e.preventDefault();
}
onDown(e){
this.startX = e.clientX;
this.messageWindowContentContainer.addEventListener('mousemove', (e)=>{this.onMouseMove(e)});
this.messageWindowContentContainer.addEventListener('mouseup', (e)=>{this.onMouseUp(e)});
}
onMouseMove(e){
this.mouseMove(e);
e.preventDefault();
}
onMouseUp(e){
this.mouseUp(e);
e.preventDefault();
}
mouseMove(e) {
this.messageWindowContainer.setAttribute("style","width:"+( this.startWidth - e.clientX + this.startX)+"px");
}
mouseUp(e){
console.log("stopdrag")
this.messageWindowContentContainer.removeEventListener('mousemove', (e)=>{this.onMouseMove(e)});
this.messageWindowContentContainer.removeEventListener('mouseup', (e)=>{this.onMouseUp(e)});
}
getMessageWindowWidth(){
let chatContainerWidth = document.getElementById("chatContainer").offsetWidth;
let messageWindowContainerWidth = document.getElementById("messageWindowContainer").offsetWidth;
return (chatContainerWidth - messageWindowContainerWidth);
}
}
export default ResizeController
Answer found here: https://gist.github.com/Restuta/e400a555ba24daa396cc
I simply defined the following code once in my constructor and then used them as my event handlers.
this.bound_onMouseDown = this.onMouseDown.bind(this);
this.bound_onMouseMove = this.onMouseMove.bind(this);
this.bound_onMouseUp = this.onMouseUp.bind(this);
Declaring them outside the constructor is not a solution.
The value passed as second parameter to .addEventListener() and .removeEventListener() should be a function reference instead of an anonymous function. You can use .bind() to preserve this.
class ResizeController {
constructor() {
this.chatContainer = document.getElementById("chatContainer");
this.button = document.querySelector("button");
this.init();
}
init() {
// pass function reference
this._onMouseMove = this.onMouseMove.bind(this);
this.chatContainer.addEventListener("mousemove", this._onMouseMove);
this.button.onclick = () => this.removeListener();
}
onMouseMove(e) {
this.chatContainer.innerHTML = "moved at " + new Date();
}
removeListener() {
// pass function reference
this.chatContainer.removeEventListener("mousemove", this._onMouseMove);
this.chatContainer.textContent = "removed mousemove event listener";
}
}
onload = () => new ResizeController();
#chatContainer {
display: block;
position: relative;
width: 300px;
height: 200px;
border: 2px solid blue;
}
<div id="chatContainer"></div>
<button>remove mousemove event listener</button>

Custom attributes are removed when using custom blots

I created a custom blot for links that requires to be able to set rel and target manually. However when loading content that has those attributes, quill strips them. I'm not sure why.
I created a codepen to illustrate the issue.
This is my custom blot:
const Inline = Quill.import('blots/inline')
class CustomLink extends Inline {
static create(options) {
const node = super.create()
node.setAttribute('href', options.url)
if (options.target) { node.setAttribute('target', '_blank') }
if (options.follow === 'nofollow') { node.setAttribute('rel', 'nofollow') }
return node
}
static formats(node) {
return node.getAttribute('href')
}
}
CustomLink.blotName = 'custom_link'
CustomLink.tagName = 'A'
Quill.register({'formats/custom_link': CustomLink})
Do I have to tell Quill to allow certain atttributes?
Upon initialization from existing HTML, Quill will try to construct the data model from it, which is the symmetry between create(), value() for leaf blots, and formats() for inline blots. Given how create() is implemented, you would need formats() to be something like this:
static formats(node) {
let ret = {
url: node.getAttribute('href'),
};
if (node.getAttribute('target') == '_blank') {
ret.target = true;
}
if (node.getAttribute('rel') == 'nofollow') {
ret.follow = 'nofollow';
}
return ret;
}
Working fork with this change: https://codepen.io/quill/pen/xPxGgw
I would recommend overwriting the default link as well though instead of creating another one, unless there's some reason you need both types.

How to trigger animations in a React app based on the scroll position

Let's say I need to add an element to the navbar when the user have scrolled past the header of the site. How can I do something like this in React without using jQuery?
You can do some thing like this: (this function was copied from my own react-sticky-dynamic-header that I created before: https://github.com/thinhvo0108/react-sticky-dynamic-header )
componentDidMount() {
var h1 = parseInt(this.refs.header.offsetHeight);
window.addEventListener('scroll', this._calcScroll.bind(this, h1));
}
componentWillUnmount() {
window.removeEventListener('scroll', this._calcScroll)
}
_calcScroll(h1) {
var _window = window;
var heightDiff = parseInt(h1);
var scrollPos = _window.scrollY;
if (scrollPos > heightDiff) {
// here this means user has scrolled past your header,
// you may rerender by setting State or do whatever
this.setState({
//stateKey: stateValue,
});
} else {
// here the user has scrolled back to header's territory,
// it's optional here for you to remove the element on navbar as stated in the question or not
this.setState({
//stateKey: stateValue,
});
}
}
render() {
return (
<div ref="header">YOUR HEADER HERE</div>
);
}
For a smooth animation when your element added or removed from the navbar, you can just add this into the element's CSS style:
#your-element{
transition: opacity 0.3s ease-in;
}
You can try to install my library to see if it can extend your needs:
https://www.npmjs.com/package/react-sticky-dynamic-header
Feel free to post here some errors if any, thanks

Resources