create "TouchableOpacity" for ReactJS without using react-native-web - javascript

I'd like to create a wrapper component that will behave like react- natives <TouchableOpacity/> without using react-native-web.
I'm thinking something that manages state and using inline styles that transitions from .5 to 1 for opacity with a setInterval or should it be a css transition.
Is there anything already out there?
What's the best approach for this?

You can achieve this affect with css transitions and using state to track when the user is clicking the button:
class App extends React.Component {
state = {
touched: false
}
toggleTouched = () => {
this.setState( prevState => ({
touched: !prevState.touched
}));
}
handleMouseUp = () => {
// Handle smooth animation when clicking without holding
setTimeout( () => {
this.setState({ touched: false });
}, 150);
}
render () {
const { touched } = this.state;
const className = touched ? 'btn touched' : 'btn';
return (
<button
className={className}
onMouseDown={this.toggleTouched}
onMouseUp={this.handleMouseUp}
>
Touch Here
</button>
)
}
}
ReactDOM.render(<App/>, document.getElementById('root'));
.btn {
background: #ededed;
padding: 10px 15px;
border: none;
outline: none;
cursor: pointer;
font-size: 15px;
opacity: 1;
transition: opacity 300ms ease;
}
.touched {
opacity: 0.5;
}
<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>
<div id="root"></div>

The answer from #ChaseDeAnda is really great. I have made my typescript component version of it
TouchableOpacity.tsx
import React from 'react'
import classNames from 'classnames'
import styles from './index.module.scss'
export default function TouchableOpacity({ className, children, ...rest }: TTouchableOpacityProps) {
const [touched, touchedSet] = React.useState(false)
return (
<button
className={classNames(styles.container, className)}
style={{ opacity: touched ? 0.5 : 1, transition: 'opacity 300ms ease' }}
onMouseDown={() => touchedSet(true)}
onMouseUp={() => touchedSet(false)}
{...rest}>
{children}
</button>
)
}
types.d.ts
type TTouchableOpacityProps = React.ButtonHTMLAttributes<HTMLButtonElement>
(you don't have to use the 'classnames` package - string interpolation is just fine)

Related

React styled-components - How to render styles based on prop condition or pseudo class?

I am trying to conditionally render a hover state / view within styled-components, leveraging the props coming in from react...
Currently, my code looks something like this:
${isHovered}, &:hover {
background: red;
}
Unfortunately, this does no work to be able to have a true/false as I am missing something I presume to be able to do the either / or pseudo..
I want to be able to explicitly show the hover state while retaining the default pseudo hover. How can I achieve this?
The way you have your selectors right now is invalid:
Problem
${isHovered}, &:hover {
background: red;
}
As of now, this will always translate to:
undefined, &:hover {
background: red;
}
For simplicity, in order to interpolate properly, it must be a function accepting props that returns a string:
color: ${props => props.isHovered && "pink" };
On a related note, even if you wrapped your styled component in a higher order component where isHovered is defined as an argument, it unfortunately still won't work in production for styled-components v5 -- this worked in v4, but v5.x doesn't handle css interpolations properly within a styled component when compiled for production (see issue tracker here).
Solutions
A better and recommended approach would be to interpolate within a CSS property:
background: ${({ isHovered }) => isHovered && "red"};
:hover {
background: red;
}
Alternatively, if you have multiple CSS rules, then you can interpolate outside of a CSS property:
${({ isHovered }) => isHovered && "background: red;color: white;"};
:hover {
background: red;
}
Now you just would pass your component an isHovered prop.
<StyledComponent isHovered={isHovered} />
Although, technically you can do this (notice that falsey values equate to true, which may be a bug or an unhandled edge case) ...
${({ isHovered }) => !isHovered && ".hovered"}, &:hover {
background: red;
}
...it is NOT recommended because of how it's being interpreted:
".hovered", &:hover {
background: red;
}
Arguably, this isn't what you'd want because .hovered isn't being used at all within the DOM, which may be confusing to other developers. Instead, it reuses the compiled hashed class name to add the CSS property within another rule (focus on the Styles tab to see the difference):
screenshot
While the recommended approach sets the CSS property to the hashed class within the same rule block:
screenshot
Working demo (this demo includes both example codes above, where Title uses the recommended approach and SubTitle doesn't):
import styled, { css } from 'styled-components';
const getHoverStyle = (props) => {
if(props?.isHovered) {
return (
css`
&:hover {
background: red;
}
`
)
}
}
export const SomeName = styled.div`
${getHoverStyle}
`;
You can use class selector to apply style and in your styled component you can use attr call to set className attribute based on property value like:
const MyComp = styled.div.attrs((props) => ({
className: props.isHovered ? "hovered" : ""
}))`
color: ${(props) => props.color};
&.hovered,
&:hover {
color: blue;
}
`;
Take a look at this react sandbox.
I have created an interactive playground. You can visit the below link. It might help you.
https://codesandbox.io/s/focused-sky-vp8d9?file=/src/App.js
Below is the code.
import React, { useState } from "react";
import "./styles.css";
import styled, { css } from "styled-components";
const HoverExample = styled.div`
background: pink;
width: 100%;
min-height: 80px;
${(props) =>
props.isHovered &&
css`
&:hover {
background: green;
}
`}
`;
export default function App() {
const [isHovered, setisHovered] = useState(false);
return (
<div className="App">
<button onClick={() => setisHovered(!isHovered)}>
{!isHovered ? "Enable" : "disabled"} Hover
</button>
<p>Hover is {isHovered ? "enabled" : "disabled"}</p>
<HoverExample isHovered={isHovered}></HoverExample>
</div>
);
}
Thanks.

React does not recognize the `backgroundColor` prop on a DOM element

I've been searching about this error a while but couldn't find a solution...
I'm using styled components and ant.design.
Button Component
import React from 'react'
import {Btn} from './style';
const ComponentButton = (props) =>{
const {title, backgroundColor,color, hoverColor, handleClick,shape} = props
return(
<Btn
shape={shape || "round"}
onClick={handleClick}
backgroundColor={backgroundColor}
color={color}
hoverColor={hoverColor}
>
{title}
</Btn>
)
}
export default ComponentButton;
styled-Component
import styled, {css} from 'styled-components';
import {primaryColor, white} from '../../../../config';
import { Button } from 'antd';
export const Btn = styled(Button)`
${(props, {color, backgroundColor, hoverColor, width} = props) =>
css`
color: ${color ? color : white};
background-color: ${backgroundColor ? backgroundColor : primaryColor} !important;
width: ${`${width}px` ? `${width}px` : '-webkit-fill-available'};
border: none !important;
&:hover, &:focus{
color: ${hoverColor ? hoverColor : white};
border: none !important;
}
&:after{
box-shadow: none !important;
}
`}
`
I don't know why I still getting this error:
React does not recognize the backgroundColor prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase backgroundcolor instead.
styled-components will automatically add all props into DOM element by default, such as:
<button backgroundColor="" color="" hoverColor="" ... />
and react will check the props of the DOM element are legal.
also, this line ${(props, {color, backgroundColor, hoverColor, width} = props) looks a little weird, this should only have one parameter.
you can try this:
// avoid pass all props into button element
export const Btn = styled(({color, backgroundColor, hoverColor, width, ...props}) => <Button {...props} />)`
${(p = props) =>
css`
color: ${p.color ? p.color : white};
background-color: ${p.backgroundColor ? p.backgroundColor : primaryColor} !important;
width: ${`${p.width}px` ? `${p.width}px` : '-webkit-fill-available'};
border: none !important;
&:hover, &:focus{
color: ${p.hoverColor ? p.hoverColor : white};
border: none !important;
}
&:after{
box-shadow: none !important;
}
`}
`
In case you arrive here because of the error message in the title: Here's a solution for the problem in the world of react and javascript:
/**
* This layer of *styled* will make the error go away
**/
const StylableButton = styled.button({}, (props) => ({
...props,
// Just in case you'd use typescript:
//...(props as any),
}));
/**
* Your custom button component
**/
const MyButton= (props) => {
return <StylableButton {...props} />;
};
Using it like this:
const MyStyledButton = styled(MyButton)({
backgroundColor: 'red',
})
Code for your case

How to use styled-components in react component

Total newbie on using styled-components. I'm wondering what's the usage of it? How should I implement component life cycle methods after styling it? For simplicity sake I've removed all the other style.
import styled from 'styled-components';
const Button = styled.button`
background-color: 'green'
`
export default Button;
I'm wondering how do I further working on this Button component?
Traditionally we can declare a class-based component and implement some lifecycle methods, but now with this styled-components, I'm not really sure how to combine them together as they are really the single Button Component?
UPDATES:
Full sourcecode for Button.js. By having the below code, all styles will be gone and I can't understand the problem
import React from 'react';
import styled from 'styled-components';
// import Button from 'react-bootstrap/Button';
import color from '../config/color';
const Button = ({ children, onPress }) => (
<button type="button" onPress={onPress}>{children}</button>
);
const StyledButton = styled(Button)`
width: 12rem;
height: 54px;
font-size: 1rem;
background-color: ${(props) => {
if (props.inverted) return 'white';
if (props.disabled) return color.disabled;
return (props.color || color.primary);
}};
color: ${(props) => {
if (props.disabled) return color.disabledText;
if (props.inverted) return (props.color || color.primary);
return 'white';
}};
border:${(props) => (props.inverted ? `2px solid ${props.color || color.primary}` : 'none')};
border-radius: 60px;
&:hover {
filter: ${(props) => (props.inverted || props.disabled ? 'none' : 'brightness(95%)')}
}
`;
export default StyledButton;
In order to style a custom react component you can pass on the custom component name as argument to styled. According to the doc:
The styled method works perfectly on all of your own or any
third-party component, as long as they attach the passed className
prop to a DOM element.
import React from 'react';
import styled from 'styled-components';
// import Button from 'react-bootstrap/Button';
import color from '../config/color';
const Button = ({ children, className onPress }) => (
<button type="button" className={className} onPress={onPress}>{children}</button>
);
const StyledButton = styled(Button)`
width: 12rem;
height: 54px;
font-size: 1rem;
background-color: ${(props) => {
if (props.inverted) return 'white';
if (props.disabled) return color.disabled;
return (props.color || color.primary);
}};
color: ${(props) => {
if (props.disabled) return color.disabledText;
if (props.inverted) return (props.color || color.primary);
return 'white';
}};
border:${(props) => (props.inverted ? `2px solid ${props.color || color.primary}` : 'none')};
border-radius: 60px;
&:hover {
filter: ${(props) => (props.inverted || props.disabled ? 'none' : 'brightness(95%)')}
}
`;
export default StyledButton;
Read the styled-component documentation for more details on styling any component
Let's rename the styled button component to reduce confusion between the 2 similarly named components.
styled-button.tsx:
import styled from 'styled-components';
const StyledButton = styled.button`
background-color: 'green'
`
export default StyledButton;
When you import the styled button component into your Button component, you can actually use make use of it the way you usually do when you are working with traditional HTML <button> elements, as its props are exposed and available on the styled component as well.
button.tsx:
import StyledButton from './StyledButton'
class Button extends React.Component {
componentDidMount() {
const { someProps, otherProps } = this.props;
// some lifecycle logic
}
handleClick() {
// do the rest
}
render() {
return <StyledButton onClick={() = this.handleClick()} />;
}
}
If you want, you can even pass in props from the parent Button component, to the child StyledButton component. This will allow you to customise it.
render() {
const { color } = this.props;
return <StyledButton background={color} onClick={() = this.handleClick()} />;
}
And on your StyledButton component, you just need to make the following changes:
const StyledButton = styled.button`
background-color: ${({ color }) => color || 'green'}
`
What other answers lack is for styling custom components like Button you have to pass a className prop thought it.
The styling is injected through className property.
const ButtonDefaultStyle = styled.button`
width: 5rem;
`;
const Button = ({ className, children, onPress }) => (
<ButtonDefaultStyle className={className} type="button" onPress={onPress}>
{children}
</ButtonDefaultStyle>
);
export default Button;
Then the styles can be applied:
import Button from './Button.js'
// Will override width: 5rem;
const StyledButton = styled(Button)`
width: 12rem;
`;

How can I force React to re-render all components on a page that come from array.map?

I have a parent component (Game) that renders children (Card) from an array. I also have a Menu component that does a callback to Game to change its state. When I change levels (button click from Menu), I want all current cards to fade out, then fade in with the new ones, so they mimic the fade-in CSS applied to newly rendered Cards. I've tried forceUpdate in a couple places, and passing dummy props to check for a change. Basicly, I want all cards currently on the page to re-render while the page renders new ones when needed.
Here is a snippet from Game:
{this._gameArray.map( (item, i) => {
let dummyProp = Math.random();
return <Card key={i}
...
dummyProp={dummyProp}/>
})}
And a snippet from Card:
componentWillReceiveProps(nextProps) {
...
if (nextProps.dummyProp !== this.props.dummyProps) {
this.forceUpdate();
}
}
I'm using componentWillReceiveProps for another purpose, and decided to try it for testing the dummyProp change. The new Cards fade in, but the ones before do not do anything.
React has some algorithms to define which components should be re-rendered, in your case React won't re-render the component because you use indexes as keys and indexes are the same. You need to update key between re-renderings.
As the simplest solution you can use the index + some trail in a key, something like this:
<Card key={index + '-' + Date.now()} card={card}/>;
In this case, the <Card/> component will be re-rendered every time when the state of the <Game/> component has changed.
class Card extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<div class="card">Card {this.props.card.name}</div>
)
}
}
class Game extends React.Component {
constructor(props) {
super(props)
this.state = {
counter: 0,
cards: [
{ name: 'Card1' },
]
}
}
increaseCounter = () => {
this.setState({ counter: this.state.counter + 1 })
}
render() {
return (
<div>
<h2 onClick={this.increaseCounter}>Click me: {this.state.counter}</h2>
<h3>Will be re-rendered</h3>
{this.state.cards.map((card, index) => {
return <Card key={index + '-' + Date.now()} card={card} />;
})}
<h3>Won't be re-rendered</h3>
{this.state.cards.map((card, index) => {
return <Card key={index} card={card} />;
})}
</div>
)
}
}
ReactDOM.render(<Game />, document.querySelector("#app"))
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
.card{
animation: fade-in-out 3000ms ease-in-out forwards;
}
#keyframes fade-in-out {
0% {
background-color: transparent;
}
50% {
background-color: red;
}
100%{
background-color: transparent;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
BUT, for real-life application, I would recommend you to consider the using of some React animation library like react-transition-group, because if you animate cards only with CSS, there is can be some flashing if re-rendering will happen faster than animation timeout.
You can simply try a different trick.
If you want all the components to re-render each time a level changes, you can use a boolean, set it to false when you start the change, and set it to true when you finish the change / after some time.
Then add a condition to the rendering of the elements.
{shouldRender && this._gameArray.map( (item, i) => {
let dummyProp = Math.random();
return <Card key={i}
...
dummyProp={dummyProp}/>
})}
when shouldRender is true the cards will be rendered, and when it's false they won't. That way each time it will be true they will all re-rendered and you'll have you effect.

React stop/start fade-out on mouseEnter and mouseLeave without Jquery

I am trying to show an error message as a toast(React Component) that will fade-out after certain seconds. However When the user hover-in the mouse on the toast while fading, the fade-out should stop and the toast should be restored to its initial state and when he hovers-out the mouse on the toast, the fade-out should start again. It can be achieved by using JQuery like this -
//function to start the fade-out after time - t sec
static fadeOutToast(id, t) {
let toast = document.getElementById(id);
if (toast) {
setTimeout(() => {
FadeAndRemove('#' + id);
}, t * 1000);
}
}
/**
* t1 - time for fadeout animation
*/
static FadeAndRemove(id,t1) {
jQuery(id).fadeOut(t1 * 1000, function () {
jQuery(this).hide();
});
handleReAppear(id);
}
static handleReAppear(id) {
jQuery(id).on("mouseover", function (e) {
jQuery(this).stop(true).fadeIn(0);
});
jQuery(id).on("mouseleave", function (e) {
FadeAndRemove(this);
});
}
Its working perfectly fine. However due to projects constraints I am not supposed to mixup Jquery and react.
I tried to achieve it by manipulating the CSS opacity on mouseEnter and mouseLeave events. The problem I face is the toast never goes away from the page using opacity. Is there any way in which we can detect when the opacity of the toast becomes 0 so that I can remove it from the page just when the opacity becomes 0 ?
Can someone help me in achieving the same without using Jquery ?
For the fading animation I would use React-Spring. With a Spring you can delay the start animation so it will fade-out after the delay.
Then you can add onMouseEnter and onMouseLeave event handler to detect the hovering of the toastr.
With this mouse detection you can toggle the to value of the Spring to opacity 1. That way it won't fade-out if the mouse is over the toast.
For the removal of the toastr you can use onRest of Spring and check if opacity is zero. onRest will be called as soon as the animation will end.
The state management is done inside Toastrcomponent which will render all displayed toasts. This component will also handle the removal of the toast with no opacity.
For click event addToast I'm using a higher order component withToastr so I can add the prop of to the containing component.
For event handling I'm using Eventemitter3. If you're using Redux you could also use it to trigger the toasts.
In the next sections I'll give some details to every component that I've created in the following Codesandbox. (Note: The snippets here are not running - for testing the code please have a look at the sandbox)
ToastrItem component
Responsible for rendering a toast and for the animation.
import React, { PureComponent } from "react";
import { Spring } from "react-spring";
import styled from "styled-components";
import PropTypes from "prop-types";
class ToastrItem extends PureComponent {
static propTypes = {
id: PropTypes.string,
timeout: PropTypes.number,
destroy: PropTypes.func
};
static defaultProps = {
timeout: 5000
};
state = {
hovered: false
};
handleRest = ({ opacity }) => {
if (opacity === 0) {
this.props.destroy(this.props.id);
}
};
handleMouseEnter = () => {
this.setState({
hovered: true
});
};
handleMouseLeave = () => {
this.setState({
hovered: false
});
};
render() {
const { message, index, timeout } = this.props;
const { hovered } = this.state;
return (
<Spring
config={{ duration: 600, delay: timeout }}
from={{ opacity: 1.0 }}
to={{ opacity: hovered ? 1.0 : 0 }}
onRest={this.handleRest}
>
{interpolated => (
<Wrapper>
<ToastBox
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
pos={index}
opacity={interpolated.opacity}
>
{message}
{/*- debug info: {JSON.stringify(interpolated)}*/}
</ToastBox>
</Wrapper>
)}
</Spring>
);
}
}
const Wrapper = styled.div`
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
z-index: 100;
`;
const ToastBox = styled.div.attrs(props => ({
style: {
transform: `translateY(${props.pos * 80}px)`,
opacity: props.opacity
}
}))`
width: 60%;
height: 50px;
line-height: 50px;
margin: 0 auto;
color: white;
padding: 10px;
background: rgba(0, 0, 0, 0.8);
text-align: center;
box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.5);
border-radius: 10px;
pointer-events: auto;
`;
export default ToastrItem;
The Spring is doing the animation as mentioned before. The mouse events enter/leave are setting local state hovered so we can change the animation end opacity - this will avoid the animation.
I've also tried reset prop from React-Spring but that wasn't working as expected.
Toastr component
This component is managing the active toasts. Nothing special here. It's rendering the toasts array that are added with addToast.
addToast is creating a relatively unique key with timestamp and array index. It's needed so React is getting a key prop on the component. We could also use a uuid library here but I think the timestamp-id is OK.
destroy will be called if opacity is 0 then it's filter by key and update the state. The map is just there so we're updating the positions of the toasts.
class Toastr extends PureComponent {
state = {
toasts: []
};
addToast = (message, config) => {
const index = this.state.toasts.length;
const id = `toastr-${Date.now()}-${index}`;
const ToastComponent = (
<ToastrItem
key={id}
id={id}
index={index}
message={message}
timeout={config.timeout || 3000}
destroy={this.destroy}
/>
);
this.setState(state => ({
toasts: [...state.toasts, ToastComponent]
}));
};
destroy = id => {
this.setState(state => ({
toasts: [
...state.toasts
.filter(toast => toast.key !== id)
.map((toast, index) => ({
// map for updating index
...toast,
props: {
...toast.props,
index: index
}
}))
]
}));
};
componentDidMount() {
emitter.on("add/toastr", this.addToast);
}
render() {
const { toasts } = this.state;
return toasts;
}
}
export const withToastr = WrappedComponent => {
return class extends PureComponent {
render() {
return <WrappedComponent addToast={actions.add} />;
}
};
};
Usage in the app
We're adding addToast by using withToastr(App). This will add the prop addToastr to the App component.
Then we're rendering the Toastr component that will manage & render our toasts.
Finally we add a button so we can trigger the toasts.
class App extends Component {
toastr;
render() {
const { addToast } = this.props;
return (
<div className="App">
<Toastr />
<button onClick={() => addToast("Hello", { timeout: 4000 })}>
Show toast
</button>
</div>
);
}
}
const rootElement = document.getElementById("root");
const AppWithToasts = withToastr(App);
ReactDOM.render(<AppWithToasts />, rootElement);
Conclusion
The code is working but I would add native prop to the Spring and I would also check if a transition would be a better fit for the use-case. See the example from MessageHub example from React-spring docs. Should be also possible to prevent the fade-out but I haven't checked.
You might want to think about using the Animatable library. It uses a declarative syntax that's quite easy to incorporate.
import * from 'react-native-animatable';
return(
<Animatable.View animation="fadeOut" duration={2000} delay={1000}>
<View>
{/* YOUR CONTENT */}
</View>
</Animatable.View>
);
https://github.com/oblador/react-native-animatable

Categories

Resources