How to extend styling of component without using wrapper element? - javascript

I'm working with styled-components in atomic system.
I have my <Atom title={title} onClick={onClick}/>
And I have molecule that extends that atom, by just adding some functionality to it without wrapping element:
const Molecule = ({}) => {
const [title, setTitle] = useState('Base title')
const onClick = () => {
// some actions
}
useEffect(()=>{
setTitle('New title')
},[conditions changed])
return(
<Atom title={title} onClick={onClick}/>
)
}
I have base styling in <Atom/> but I would like to add some more to it in <Molecule/>. Is it possible to do it without creating extra wrapper?

It is possible, but the question is if it's worthy the effort - the most anticipated way would be to do it as the documentation says - to wrap the styled component and extend the styles (but this is what you want to avoid). So either:
you could assign a className to Atom, so you can adjust/overwrite the styles with CSS
pass the extraStyles props to Atom and then pass to the styled component and just use inside after the previous, default styles to overwrite them
or either pass some extraStyles as CSSProperties object and just use them as inline styling.
https://codesandbox.io/s/withered-leftpad-znip6b?file=/src/App.js:64-545
/* styles.css */
.extraClass {
color: green;
}
const AtomStyled = styled.div`
font-size: 17px;
color: blue;
font-weight: 600;
`;
const Atom = ({ children, className, extraStyles }) => {
return (
<AtomStyled style={extraStyles} className={className}>
{children}
</AtomStyled>
);
};
const Molecule = () => {
return (
<Atom className={'extraClass'} extraStyles={{ fontSize: 27 }}>
Atom
</Atom>
);
};

Related

Material ui emotion/styled how to combine multiple snippets of css classes?

In the old way material ui (version 4 and earlier) styled components you could use className property to "Select" which styles are active, ie a component could be styled like:
const styles = (theme: ThemeTy) => ({
root: {
width: '100%',
},
rootLight: {
color: theme.palette.getContrastText(theme.palette.primary.main),
},
})
const useStyles = makeStyles(styles);
function MyComp() {
const classes = useLocalStyles();
return <Input
className=classNames(classes.root, highContrast ? classes.rootLight : undefined)}
value={100}
/>
}
However in the new api one would define the classes outside the component similar to styled-components does it:
const StyledInput = styled(Input)(({theme}) => `
width: '100%',
`);
function MyComp() {
return <StyledInput
value={100}
/>
}
However how would I add the conditional styling? Would I have to use the sx element? And thus use conditional styling everywhere?
You can pass any props to your StyledInput and then style the component based on them:
const StyledInput = styled(Input)(({ theme, showborders }) => ({
width: "100%",
border: showborders ? "3px solid red" : "none"
}));
...
const [showBorders, setShowBorders] = React.useState(false);
return (
<Box>
<StyledInput showBorders={showborders ? 1 : 0} value={100} />
</Box>
);
Demo

Having trouble with typeahead/predictive test functionality

I'm using material-table and I decided to implement a "typeahead" kind of feature that Google has. Something like this:
To realize this, I wrapped the MTableEditField component with my own, which looks like this:
import React, { useState } from "react";
import { MTableEditField } from "material-table";
const CustomTableEditField = props => {
const [rawValue, setRawValue] = useState(props.value);
const [suggestedValue, setSuggestedValue] = useState("asdasda");
const handleOnChange = value => {
// ... logic to find best match and set state values
};
return (
<MTableEditField
inputProps={
suggestedValue
? {
style: {
backgroundColor: "#557D92",
color: "white",
padding: "offset"
}
}
: {}
}
{...props}
value={suggestedValue ?? rawValue}
onChange={handleOnChange}
/>
);
};
export default CustomTableEditField;
The problem is that when there is a value, it looks like:
I don't want it to change the whole background if there will be a match. I want it to keep the already-typed text, with suggested text appended to it.
I looked into gradients, which are treated as images that can be resized, but I wasn't able to get anything to render.
Is this possible to do at all, or am I just wasting my time?
Edit
Stackblitz
Not sure about the customization of MTableEditField, But you can try some thing like following by writing own component.
1) The main idea is split the words (raw and suggestion) and keep them separate span elements so that we can get full control of the styling.
2) Wrap the span elements in div and write the own onChange event handlers.
PS: this is sample code, Will need to fine tune the code.
Check out the working sample on stackblitz
import React, { useState } from "react";
const dict = ['apple', 'mango', 'berry'];
const CustomTableEditField = props => {
const [rawValue, setRawValue] = useState("");
const [suggestedValue, setSuggestedValue] = useState("");
const handleKeyPress = (event) => {
// console.log('key: ', event.key);
let new_raw;
if (event.key === "Backspace") {
new_raw = rawValue.slice(0, rawValue.length - 1);
} else {
new_raw = `${rawValue}${event.key}`;
}
setRawValue(new_raw);
const suggested = dict.find(word => word.startsWith(new_raw));
if (suggested && new_raw) {
setSuggestedValue(suggested.slice(new_raw.length));
} else {
setSuggestedValue("");
}
}
return (
<div
tabIndex="0"
onKeyDown={handleKeyPress}
style={{border: '1px solid green', height: '30px', color: 'black'}}>
<span>{rawValue}</span>
{suggestedValue && (
<span
style={{backgroundColor: "#557D92",
color: "white",
padding: "offset"}}> {suggestedValue} </span>
)}
</div>
);
};
export default CustomTableEditField;

How to get parent width/height in React using Hooks?

I'm creating a component and I need to get it's parent <div> width and height. I'm using Hooks, so all my components are functions. I've read some examples using classes, but this won't apply to my component.
So I have this component:
export default function PlantationMap(props) {
<div className="stage-canvas">
<Stage
width={window.innerWidth * 0.5}
height={window.innerHeight * 0.5}
onWheel={handleWheel}
scaleX={stage.stageScale}
scaleY={stage.stageScale}
x={stage.stageX}
y={stage.stageY}
draggable
/ >
</div>
}
How could I get the <div> height and width to use in <Stage width={} height={} />?
Thank you very much in advance
Edit: I tried using the useRef() hook, like this:
const div = useRef();
return (
<div ref={div}>
...
</div>
)
But I can't access the div.current object
I think useCallback is what you want to use so you can get the width and height when it changes.
const [height, setHeight] = useState(null);
const [width, setWidth] = useState(null);
const div = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
setWidth(node.getBoundingClientRect().width);
}
}, []);
return (
<div ref={div}>
...
</div>
)
Declare a reference using useRef hook and then read current.offsetHeight and current.offsetWidth properties.
Here is the code:
import React, { useEffect, useRef } from 'react';
const PlantationMap = (props) => {
const stageCanvasRef = useRef(null);
// useEffect will run on stageCanvasRef value assignment
useEffect( () => {
// The 'current' property contains info of the reference:
// align, title, ... , width, height, etc.
if(stageCanvasRef.current){
let height = stageCanvasRef.current.offsetHeight;
let width = stageCanvasRef.current.offsetWidth;
}
}, [stageCanvasRef]);
return(
<div className = "stage-canvas" ref = {stageCanvasRef}>
<Stage
width={window.innerWidth * 0.5}
height={window.innerHeight * 0.5}
onWheel={handleWheel}
scaleX={stage.stageScale}
scaleY={stage.stageScale}
x={stage.stageX}
y={stage.stageY}
draggable
/ >
</div>);
}
export default PlantationMap;
You can make use of the built-in ResizeObserver:
export default function PlantationMap(props) {
const [width, setWidth] = useState(100);
const [height, setHeight] = useState(100);
useEffect(() => {
const resizeObserver = new ResizeObserver((event) => {
// Depending on the layout, you may need to swap inlineSize with blockSize
// https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/contentBoxSize
setWidth(event[0].contentBoxSize[0].inlineSize);
setHeight(event[0].contentBoxSize[0].blockSize);
});
resizeObserver.observe(document.getElementById("div1"));
});
return (
<div id="div1" className="stage-canvas">
<Stage
width={width * 0.5}
height={height * 0.5}
onWheel={handleWheel}
scaleX={stage.stageScale}
scaleY={stage.stageScale}
x={stage.stageX}
y={stage.stageY}
draggable
/ >
</div>
);
}
I think ResizeObserver is the way to go as mentioned in the answer from Dan.
I just wouldn't use the document.getElementById. Either use useMeasure from react-use or create everything on your own.
There are two scenarios:
Component contains the container that you'd like to observe
Component is a child component and doesn't have the container reference
To 1 - Reference directly accessible
In this case, you can create the reference with useRef in the component and use it at resizeObserver.observe(demoRef.current).
import "./styles.css";
import React, { useEffect, useRef, useState } from "react";
const DisplaySize = ({ width, height }) => (
<div className="centered">
<h1>
{width.toFixed(0)}x{height.toFixed(0)}
</h1>
</div>
);
const Demo = () => {
const [width, setWidth] = useState(100);
const [height, setHeight] = useState(100);
const demoRef = useRef();
useEffect(() => {
const resizeObserver = new ResizeObserver((event) => {
// Depending on the layout, you may need to swap inlineSize with blockSize
// https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/contentBoxSize
setWidth(event[0].contentBoxSize[0].inlineSize);
setHeight(event[0].contentBoxSize[0].blockSize);
});
if (demoRef) {
resizeObserver.observe(demoRef.current);
}
}, [demoRef]);
return (
<div ref={demoRef} className="App">
<DisplaySize width={width} height={height} />
</div>
);
}; //);
export default function App() {
return <Demo />;
}
To 2 - Reference of container not directly accessible:
This case is probably happening more often and requires slightly more code.
You need to pass the reference from the parent to the child component with React.forwardRef.
Demo code can be found below or in the following Codesandbox
Some words to the code:
In the parent component you create a reference with const containerRef = useRef() and use it at the main container with <div ref={containerRef}/>. Under the hood it will do something like ref => containerRef.current=ref
Next, pass the reference to the Demo component.
Why not use React.createRef?
That would work too but it would recreate the reference on every render of your App. Please have a look here for an explanation of the difference between useRef and createRef.
In short, use useRef with functional components and use createRef with class-based components.
const {useEffect, useRef, useState} = React;
const DisplaySize = ({ width, height }) => (
<div className="centered">
<h1>
{width.toFixed(0)}x{height.toFixed(0)}
</h1>
</div>
);
const Demo = React.forwardRef((props, ref) => {
const [width, setWidth] = useState(100);
const [height, setHeight] = useState(100);
useEffect(() => {
const resizeObserver = new ResizeObserver((event) => {
// Depending on the layout, you may need to swap inlineSize with blockSize
// https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/contentBoxSize
setWidth(event[0].contentBoxSize[0].inlineSize);
setHeight(event[0].contentBoxSize[0].blockSize);
});
if (ref && ref.current) {
resizeObserver.observe(ref.current);
}
}, [ref]);
return <DisplaySize width={width} height={height} />;
});
function App() {
const containerRef = useRef();
return (
<div ref={containerRef} className="App">
<Demo ref={containerRef} />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
/* apply a natural box layout model to all elements, but allowing components to change */
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
html,
body {
margin: 0;
padding: 0;
}
.App {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
font-family: sans-serif;
text-align: center;
border: 4px solid red;
}
.centered {
display: flex; /* establish flex container */
flex-direction: column; /* make main axis vertical */
justify-content: center; /* center items vertically, in this case */
align-items: center; /* center items horizontally, in this case */
height: 100%;
}
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
Library React-use
There are also some useful hooks in React-use that could help here.
useWindowSize and useSize look pretty similar but after looking at the source code the first one relies on the window.onresize event and requires less code to implement.
useSize will add an iframe below the current component (z-index: -1) to track the size with resize event and requires more code. It also adds a little debounce with setTimeout.
So use useWindowSize if you just need the width/height to do some calculations on the first render and useSize if you'd like to show that the size changed.
useWindowSize
If you just need to get the window size useWindowSize is the way to go.
They're doing it with onresize event with document.addEventlistener('resize', resizeHandler) and checking innerWidth / innerHeight
Codesandbox Demo
useMeasure
To track an element size, useMeasure can be used. It is using ResizeObserver under the hood, so it's like the code above where ref is the reference you'd like to track:
The first element returned by useMeasure is the setRef method.
So you can do the following in your component:
const [setRef, { width, height }] = useMeasure();
useEffect(() => {
setRef(ref.current)
}, [])
Please have a look at the following Codesandbox.
useSize
If you want to track the size of a component useSize could be used as mentioned in the docs.
Codesandbox Demo useSize
to my knowledge if it is concerned with style can only be registered by:
<Stage style={{width:window.innerWidth * 0.5,height:width:window.innerWidth * 0.5}} />

Can I select all react components of a type, without assigning a class to each one?

I have a Playground here: https://codesandbox.io/s/736v9vjzw1
const Something = ({ classes, children, variant }) => {
return (
<div className={classes.someThing}>
<p> I'm some thing </p>
<SomeOtherThing />
<SomeOtherThing> I have some children </SomeOtherThing>
<SomeOtherThing> I have some children </SomeOtherThing>
<SomeOtherThing> I have some children </SomeOtherThing>
</div>
);
};
const styles = {
someThing: {
color: "green",
border: "solid 2px black",
margin: 30,
"& $someOtherThing": {
backgroundColor: "pink" // Doesn't work
},
"& p": {
fontWeight: "bold" //This works but is too broad
}
}
};
I have a situation here, where I want to style all the SomeOtherThings inside my SomeThing.
I can use & p selector to select the p element - but I don't like this. It would style any random ps I have around - and I don't want to have to look inside the component definition to find what it's top level element is.
How can I do this? Something like & SomeOtherElement.
The real world application of this, is that in some places I want have SomeOtherElement be displayed block and other places inline-block.
I would extend the SomeOtherThing component to accept a className and add it to the div if present. This will also work on a production setup, where the class names is minified to e.g. .t-0-root.
Here is a forked playground: https://codesandbox.io/s/zlzx277zzm which shows how to use it.
const SomeOtherThing = ({ classes, children, className }) => {
return (
<p className={`${classes.someOtherThing} ${className && className}`}>
I'm another thing {children}
</p>
);
};
I would most likely use the package classnames to conditionally render the class name instead of string interpolation.
const Something = ({ classes, children, variant }) => {
return (
<div className={classes.someThing}>
<p> I'm some thing </p>
<SomeOtherThing />
<SomeOtherThing className={classes.someOtherThing}>
I have some children
</SomeOtherThing>
<SomeOtherThing className={classes.someOtherThing}>
I have some children
</SomeOtherThing>
<SomeOtherThing className={classes.someOtherThing}>
I have some children
</SomeOtherThing>
</div>
);
};
const styles = {
someThing: {
color: "green",
border: "solid 2px black",
margin: 30
},
someOtherThing: {
backgroundColor: "pink"
}
};
It's so simple, in your someThing CSS codes select the p elements with class someOtherThing class name and use not() CSS operation for p in top level, see following code:
const styles = {
someThing: {
color: "green",
border: "solid 2px black",
margin: 30,
"& [class*='SomeOtherThing']": {
backgroundColor: "pink" //
},
"& :not([class*='SomeOtherThing'])": {
fontWeight: "bold" // Just for top level p
}
}
};
and
const SomeOtherThing = ({ classes, children }) => {
return (
<p className={classes.root}> I'm another thing {children} </p>
);
};
CodeSandBox
The way that this works, is that by giving SomeOtherThing any jss class, it's going to render the dom element as something like:
<p class="SomeOtherThing-root-0-1-2"> I'm another thing I have some children </p>
which the [class*='SomeOtherThing'] attribute selector will match on.
You should note that this selector will apply to any deeper nested SomeOtherThings as well.
One problem with the "cascading" aspect of CSS is that it sort of breaks React's component model. But in React you can always create a wrapper or higher-order component that returns another with some predefined props, kind of like a factory function:
const Something = props => {
return (
<div className={props.className}>
// ...
</div>
)
}
const SomethingInline = props => {
return <Something className='display-inline' {...props} />
}
const SomethingBlock = props => {
return <Something className='display-block' {...props} />
}
const App = () => {
return (
<div>
<SomethingInline />
<SomethingBlock />
<SomethingBlock> I have children </SomethingBlock>
</div>
)
}
Instead of using the & selector to define your style, create a class that only applies to these specific versions of your component. In this way, the global scope of CSS is avoided and you can create these sort of declarative components that describe their own style, but don't require you to explicitly pass a class name.

Style parent component on input focus of child component in styled-components

I've seen some burdensome solutions to this problem, using refs or event handlers in React. I'm wondering if there's a solution at all in styled-components.
The code below is clearly incorrect. I'm trying to figure out the easiest way to style my parent component, when my input child component has focus. Is this possible using styled-components?
What's the best way to go about this, specifically with styled-components in mind, even if it means relying on one of the React methods mentioned above?
const Parent = () => (
<ParentDiv>
<Input/>
</ParentDiv>
);
const ParentDiv = styled.div`
background: '#FFFFFF';
${Input}:focus & {
background: '#444444';
}
`;
const Input = styled.input`
color: #2760BC;
&:focus{
color: '#000000';
}
`;
Check out :focus-within! I think it's exactly what you're looking for.
https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-within
// update 2022:
You can use :focus-within (thanks for figuring out #hoodsy)
div:focus-within {
background: #444;
}
<div>
<input type="text" />
</div>
// original answer (with IE and Edge support):
Sadly there is no way of selecting the parent, based only on the child's state with pure CSS/styled-components. Although it is a working draft in CSS4, currently no browsers support it. More about this here.
I usually add an onFocus and onBlur attribute to the input field which then triggers a state change. In your case you have a stateless component. Therefore you could use innerRef to add or remove a class from the parent. But I guess you have found this solution already. Nevertheless I'll post it as well:
const styled = styled.default;
const changeParent = ( hasFocus, ref ) => {
// IE11 does not support second argument of toggle, so...
const method = hasFocus ? 'add' : 'remove';
ref.parentElement.classList[ method ]( 'has-focus' );
}
const Parent = () => {
let inputRef = null;
return (
<ParentDiv>
<Input
innerRef={ dom => inputRef = dom }
onFocus={ () => changeParent( true, inputRef ) }
onBlur={ () => changeParent( false, inputRef ) }
/>
</ParentDiv>
);
};
const ParentDiv = styled.div`
background: #fff;
&.has-focus {
background: #444;
}
`;
const Input = styled.input`
color: #2760BC;
&:focus{
color: #000;
}
`;
ReactDOM.render( <Parent />, document.getElementById( 'app' ) );
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://unpkg.com/styled-components/dist/styled-components.min.js"></script>
<div id="app"></div>
A more rigid method is to convert Parent to a class, so you'd be able to use states:
const styled = styled.default;
class Parent extends React.Component {
state = {
hasFocus: false,
}
setFocus = ( hasFocus ) => {
this.setState( { hasFocus } );
}
render() {
return (
<ParentDiv hasFocus={ this.state.hasFocus }>
<Input
onFocus={ () => this.setFocus( true ) }
onBlur={ () => this.setFocus( false ) }
/>
</ParentDiv>
);
}
};
const ParentDiv = styled.div`
background: ${ props => props.hasFocus ? '#444' : '#fff' };
`;
const Input = styled.input`
color: #2760BC;
&:focus{
color: #000;
}
`;
ReactDOM.render( <Parent />, document.getElementById( 'app' ) );
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://unpkg.com/styled-components/dist/styled-components.min.js"></script>
<div id="app"></div>
This way you are in better control of your component and can decide more easily to pass the focus/blur state to other elements.
#hoodsy thanks,it worked like a charm I used like below on the parent div to change color of a label when an input focused .
&:focus-within label{
color: blue;
}

Categories