Assign styled-component CSS property based on props passed to component - javascript

I have two components TextField and Label.
The TextField is passing the prop req to the Label. I want to modify the styled-component based on the req prop being passed in. Here is my current code that is not working.
No errors are being reported to the console.
TextField.js
import React, {Component} from 'react';
import styled from 'styled-components';
import Label from '../Label/Label';
const Wrapper = styled.div`
display: flex;
flex-direction: column;
margin: 16px 8px 8px 8px;
`;
const Input = styled.input`
border-bottom: 1px solid rgba(0, 0, 0, .23);
&:focus {
border-bottom: 1px solid #2196f3;
}
`;
class TextField extends Component {
render() {
const {
label,
req = true,
} = this.props;
return (
<Wrapper>
<Label req={req} text={label}/>
<Input type={'textfield'}/>
</Wrapper>
);
}
}
export default TextField;
Label.js
import React, {Component} from 'react';
import styled from 'styled-components';
const LabelBase = styled.label`
color: rgba(0, 0, 0, .54);
font-size: 1rem;
line-height: 1;
&:after {
content: ${props => props.req ? '*' : ''};
}
`;
class Label extends Component {
render() {
const {
req,
text,
} = this.props;
return (
<LabelBase req={req}>{text}</LabelBase>
);
}
}
export default Label;

You say you want to style the component based on the ref prop, but it seems that you're using that prop as a boolean to add text, not styles so I just went with a simplified solution for that since psuedo-selectors like :after aren't supported in React's JS styles. There are other ways around that if need be, but I think you can just do the following. However, I've included a way to pass styles to the child component as well for your reference:
class Label extends React.Component {
render() {
const {
req,
text,
moreStyles
} = this.props;
const styles = {
"color": "rgba(0, 0, 0, .54)",
"fontSize": "1rem",
"lineHeight": 1
}
return (
<div style={{...styles, ...moreStyles}}>{text + (req ? '*' : '')}</div>
);
}
}
ReactDOM.render(<Label text="test" req="Yes" moreStyles={{"backgroundColor": "blue", "border": "1px solid black"}}/>, document.getElementById("root"));
<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>

Related

How to style Next.js active link with Styled Components

I'm trying to add a style to the active link with styled components.
I have a navbar and depending on which link is currently active, the respective link will have a style added to it
Thanks
import React from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { bool } from 'prop-types';
import StyledSidebar from '../../styles/header/styled.sidebar'
import StyledLink from '../../styles/header/styled.links'
export default function Sidebar({ open }) {
const router = useRouter()
return (
<StyledSidebar open={open}>
<div>
<ul>
<StyledLink><Link href="/" className={router.pathname == "/" ? 'active' : ''}>HOME</Link></StyledLink>
<StyledLink><Link href="/destination" className={router.pathname == "/destination" ? 'active' : ''}>DESTINATION</Link></StyledLink>
<StyledLink><Link href="/crew" className={router.pathname == "/crew" ? 'active' : ''}>CREW</Link></StyledLink>
<StyledLink><Link href="/technology" className={router.pathname == "/" ? 'active' : ''}>TECHNOLOGY</Link></StyledLink>
</ul>
</div>
</StyledSidebar>
)
}
Sidebar.propTypes = {
open: bool.isRequired,
};
Check this https://codesandbox.io/s/exciting-kilby-eb3pj?file=/src/App.js:0-584 link where you see this logic live
Pass the props to styled-components as normal props restructure and use it for conditionally render the styles
import styled, { css } from "styled-components";
// props which are received are destructured
const Button = styled.button(
({ someprops }) => css`
${console.log(someprops)}
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border-radius: 3px;
// Conditionally render the styles inside style
color: ${someprops === "root" ? "red" : "green"};
border: 2px solid ${someprops === "root" ? "red" : "green"};
`
);
export default function App() {
return (
<div>
<Button someprops="root">Themed</Button>
</div>
);
}

React Localhost unresponsive from one component

When I saw the issue the first thing I did is find the culprit which ended up being the component holding a simple image. When I remove it the project works fine, however, I need it for my project and I can't see what went wrong for it to crash the localhost and it's becoming extremely frustrating. If I can get help and see what I'm not seeing it would be extremely appreciated.
Container (parent)
import React from 'react';
import { CardItem } from './card-item';
import {PlayArrow,Add,ThumbUpAltOutlined,ThumbDownAltOutlined} from '#material-ui/icons';
export function CardItemContainer() {
return(
<CardItem>
{/*Cause of crash */}
CardItem.CardPicture src="/images/misc/hxh.jpg" alt="Feature Picture" />
{/*Cause of crash */}
<CardItem.Info>
<CardItem.Icons>
<PlayArrow/>
<Add/>
<ThumbUpAltOutlined/>
<ThumbDownAltOutlined/>
</CardItem.Icons>
<CardItem.ItemInfoTop/>
<CardItem.Description>
To fulfill his dreams of becoming a legendary Hunter like his dad,
a young boy must pass a rigorous examination and find his missing father.
</CardItem.Description>
<CardItem.Genre>Action</CardItem.Genre>
</CardItem.Info>
</CardItem>
)
}
Components (child)
import React from 'react';
import {Container,CardPicture,Info,Icons,Description,ItemInfoTop,Genre} from './styles/card-item';
export default function CardItem({ children, ...restProps }) {
return <Container {...restProps}>{children}</Container>
}
----------
CardItem.CardPicture = function CardItemCardPicture({ ...restProps }) {
return (
<CardPicture {...restProps} />
)
}
----------
CardItem.Info= function CardItemInfo({ children, ...restProps }) {
return <Info {...restProps}>{children}</Info>;
}
CardItem.Icons= function CardItemIcons({ children, ...restProps }) {
return <Icons {...restProps}>{children}</Icons>;
}
CardItem.ItemInfoTop= function CardItemInfoTop({ children, ...restProps }) {
return <ItemInfoTop {...restProps}>
<span>22 minutes</span>
<span id="limit">13+</span>
<span>2001</span>
{children}</ItemInfoTop>;
}
CardItem.Description= function CardItemDescription({ children, ...restProps }) {
return <Description {...restProps}>{children}</Description>;
}
CardItem.Genre= function CardItemGenre({ children, ...restProps }) {
return <Genre {...restProps}>{children}</Genre>;
}
styled component (CSS)
import styled from 'styled-components';
export const Container=styled.div`
width:225px;
height:120px;
background-color:#000000;
margin-right:5px;
overflow:hidden;
cursor:pointer;
color:white;
&:hover
{
width:325px;
height:300px;
-webkit-box-shadow: 0px 2px 13px 5px rgba(255,255,255,0.22);
box-shadow: 0px 2px 13px 5px rgba(255,255,255,0.22);
}
`
export const CardPicture=styled.img`
width:100%;
height:100%
object-fit:cover;
/*&:hover
{
height:140px;
}
`
export const Info=styled.div`
`
export const Icons=styled.div`
color:white;
`
export const Description=styled.div`
`
export const ItemInfoTop=styled.div`
`
export const Genre=styled.div`
`

How to extend component style in styled-components

I have a basic ElipseDetail component that looks like the image below
<ElipseDetail text="1.07" />
When I am using the component everything works as expected.
But now I want to reuse the component in another place but add an extension to the Text component style
How can I achieve that with styled-components and reuse the component but change the Text which is a child?
import React, { ReactElement } from "react";
import styled from "styled-components";
interface Props {
text: string;
children?: React.ReactNode;
}
export const Container = styled.div`
padding: 4px 12px;
border-radius: 30px;
background-color: #eaeef2;
display: inline-block;
`;
export const Text = styled.p`
font-size: 12.5px;
font-weight: normal;
font-stretch: normal;
font-style: normal;
letter-spacing: 0.63px;
color: #687c97;
`;
export default function ElipseDetail({ text, children }: Props): ReactElement {
return (
<Container>
<Text>{text}</Text>
{children}
</Container>
);
}
Since ElipseDetails is not a styled component, but calls to a styled one, you can do something like:
function ElipseDetail({ text, children }: Props): ReactElement {
return (
<Container>
<Text>{text}</Text>
{children}
</Container>
);
}
ElipseDetail.Styled = Container;
export default ElipseDetail
And then, in a different component, you can change it like so:
const StyledElipseDetail = styled(ElipseDetail.Styled)`
${Text} {
//
}
`;
...
return <StyledElipseDetailed>...</StyledElipseDetail>
PS - I have taken this approach from an older question of mine which I found quite useful.
I would suggest to create a container and if Text is in that container then add different properties. & here means this class name so it evaluates to Text so when text would be in ElipseContainer then it will behave in different way depending on your use case. Here if it's wrapped with ElipseContainer then colour of Text is green and it's red otherwise.
Here is sandbox link :
sandobx
const ElipseContainer = styled.div``;
const Text = styled.p`
color: red;
${ElipseContainer} & {
color: green;
}
`;
const App =() =>
<ElipseContainer>
<ElipseDetail text="word2" />
</ElipseContainer>

Home component only loads on refresh (not on initial load), race condition?

I am having a situation where, when loading the page for the first time, the images that should be loaded inside Home.js aren't loaded, until refresh is pushed on the browser (Chrome). This is happening in development server and live production server, and sometimes one must hit refresh multiple times before images show up.
Please note that I am new to all web development as well as React as most of my experience is in Android development.
I'm thinking it's something to do with the way I load the images onto the Home component. With logging I found that the images were loaded before the page was loaded (used componentDidMount()), so I'm looking for any solutions available.
Thanks and here's the code...
Main.js:
import React from 'react';
import { withRouter, Route, NavLink, HashRouter } from 'react-router-dom';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faInstagram, faGithub, faFacebook } from '#fortawesome/fontawesome-free-brands';
import 'bootstrap/dist/css/bootstrap.css';
import Favicon from 'react-favicon';
import Home from './Home';
import Contact from './Contact';
import socialMediaLinks from './utilities/Utils';
class Main extends React.Component {
constructor() {
super();
this.state = {
screenWidth: null,
isMobile: false,
};
this.handleResize = this.handleResize.bind(this);
}
componentDidMount() {
window.addEventListener('resize', this.handleResize.bind(this));
this.handleResize();
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
handleResize() {
this.setState({ screenWidth: window.innerWidth });
console.log(`width is ${this.state.screenWidth}`);
}
render() {
return (
<HashRouter>
<div>
<Favicon url='./favicon.ico' />
{/* Navigation */}
<nav className='navbar fixed-top bg-dark flex-md-nowrap'>
{/* Social Media */}
<a className='social-media-link' href={socialMediaLinks['instagram']}><FontAwesomeIcon icon={faInstagram} size='lg' /></a>
<a className='social-media-link' href={socialMediaLinks['github']}><FontAwesomeIcon icon={faGithub} size='lg' /></a>
<a className='social-media-link' href={socialMediaLinks['facebook']}><FontAwesomeIcon icon={faFacebook} size='lg' /></a>
<ul className="header">
<li className='nav-option'><NavLink exact to='/'>Home</NavLink></li>
<li className='nav-option'><NavLink to='/contact'>About/Contact</NavLink></li>
</ul>
</nav>
{/* Main Page */}
<div className='content container-fluid' align='center'>
<div className='row'>
<Route exact path='/' component={withRouter(Home)} />
<Route path='/contact' component={withRouter(Contact)} />
</div>
<footer>Created by me :)</footer>
</div>
</div>
</HashRouter>
);
}
}
export default Main;
Home.js:
import React from 'react';
import 'bootstrap/dist/css/bootstrap.css';
import './index.css';
var imageList = [];
function importAll(r) {
const keys = r.keys();
let images = {};
for (var k in keys) {
images[keys[k].replace('./', '')] = r(keys[k]);
}
return images;
}
const images = importAll(require.context('./images/resized/', false, /\.(png|jpe?g|svg)$/));
for (var image in images) {
var newImage = new Image(images[image], images[image], null);
newImage.name = images[image];
newImage.src = images[image];
imageList.push(newImage);
}
class Home extends React.Component {
render() {
let images = imageList.map(image => {
if (image.naturalHeight > image.naturalWidth) { // portrait
return <img className='portrait-img' src={image.src} alt=''/>
} else if (image.naturalHeight < image.naturalWidth) { // landscape
return <img className='landscape-img' src={image.src} alt=''/>
}
});
return (
<div>
{images}
</div>
);
}
}
export default Home;
index.css:
body {
background-color: #FFF;
padding: 20px;
margin: 0;
font-family: Helvetica;
}
h1 {
color: #111;
font-weight: bold;
padding-top: 2px;
}
ul.header li {
display: inline;
list-style-type: none;
margin: 0;
}
ul.header {
background-color: #111;
padding: 0;
}
ul.header li a {
color: #FFF;
font-weight: bold;
text-decoration: none;
padding: 20px;
display: inline-block;
}
.content {
background-color: #FFF;
padding: 20px;
}
.content h2 {
padding: 0;
margin: 0;
}
.content li {
margin-bottom: 10px;
}
.active {
background-color: #0099FF;
}
.portrait-img {
max-width: 55%;
height: auto;
padding: 5px;
}
.landscape-img {
max-width: 75%;
height: auto;
padding: 5px;
}
.navbar {
max-height: 110px;
background-color: #FFCC00 !important;
}
.content {
padding-top: 80px;
}
footer {
background-color: #FFF;
font-size: 7pt;
position: relative;
margin: auto;
float: left;
}
.social-media-link {
color: #111;
}
::-webkit-scrollbar {
display: none;
}
#media only screen and (max-width: 500px) {
h1 {
font-size: 18pt;
}
.navbar {
max-height: 120px;
}
.content {
padding-top: 104px;
}
.header {
font-size: 10pt;
}
.portrait-img {
max-width: 100%;
height: auto;
padding: 2px;
}
.landscape-img {
max-width: 100%;
height: auto;
padding: 2px;
}
footer {
font-size: 5pt;
}
.social-media-link {
padding: 5px;
}
}
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.1/css/font-awesome.min.css">
<link rel='icon' href='../src/favicon.ico' type='image/x-icon' />
</head>
<body>
<div id="root"></div>
</body>
</html>
index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import Main from './Main';
import './index.css';
ReactDOM.render(
<Main />,
document.getElementById('root'),
);
Edit:
revised Home.js below to use a Promise. The idea here is, a Promise to render the images will alter this.state.images and that value will be used in the HTML in the return of the Component's render(). This does not fix the problem, but, it does result in a list of imgs logged instead of a list of undefined as before.
import React from 'react';
import 'bootstrap/dist/css/bootstrap.css';
import './index.css';
import { resolve } from 'q';
var imageList = [];
function importAll(r) {
const keys = r.keys();
let images = {};
for (var k in keys) {
images[keys[k].replace('./', '')] = r(keys[k]);
}
return images;
}
const images = importAll(require.context('./images/resized/', false, /\.(png|jpe?g|svg)$/));
for (var image in images) {
var newImage = new Image(images[image], images[image], null);
newImage.name = images[image];
newImage.src = images[image];
imageList.push(newImage);
}
class Home extends React.Component {
constructor(props) {
super();
this.state = {
images: null,
};
}
componentDidMount() {
let promise = this.getImages();
promise.then(result => {
let images = result.map(image => {
if (image.naturalHeight > image.naturalWidth) { // portrait
return <img className='portrait-img' src={image.src} alt='' />
} else if (image.naturalHeight < image.naturalWidth) { // landscape
return <img className='landscape-img' src={image.src} alt='' />
}
});
this.setState({ images: images });
}, function(error) {
this.setState({ images: error });
});
}
getImages() {
let promise = new Promise((resolve, reject) => {
let imageList = [];
const images = importAll(require.context('./images/resized/', false, /\.(png|jpe?g|svg)$/));
for (var image in images) {
var newImage = new Image(images[image], images[image], null);
newImage.name = images[image];
newImage.src = images[image];
imageList.push(newImage);
}
if (imageList.length > 0) {
console.log(imageList);
resolve(imageList);
} else {
console.log('shit');
reject(Error('oh shit'));
}
});
return promise;
}
render() {
return (
<div>
{this.state.images}
</div>
);
}
}
export default Home;
This results in:
after hard reload (ctrl+F5):
images = [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]
after refresh (ctrl+R):
images = [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}] -- all with typeof: Symbol(react.element), type: "img".
console.log is not guaranteed to be synchronous, so you can't depend on it for order of execution. Some engines are, some aren't.
I wouldn't rely on require.context either, particularly if you're using create-react-app. See: https://github.com/facebook/create-react-app/issues/517
In general, if your project is already set up for ES modules and you find yourself needing require, that's a bit of a code smell IMO. The whole point of ES modules is to make scopes static, not dynamic, so that the compiler can infer what to throw away when minifying. The directory structure doesn't actually exist anymore at runtime; it's all been minified into one big file, either on the fly by the webpack dev server, or as part of the build for production.
Are the files too numerous to import directly into a module? You could have a separate module for the imageList that handles the loading and exports it, then just import and use directly in your component(s):
// imageList.js
import foo from './images/foo.jpg'
import bar from './images/bar.jpg'
import baz from './images/baz.jpg'
import blah from './images/blah.jpg'
export default {
foo,
bar,
baz,
blah,
}
You can then have a test to make sure none get missed in the future (NOTE, tests run in node directly with the file structure, not the webpack-ed final procuct):
// __tests__/imagesList.test.js
import imageList from '../imageList'
import fs from 'fs'
import path from 'path'
const dir = path.join(__dirname, '..', 'images')
it('has all the images', () => {
const imageFiles = fs.readdirSync(dir).filter((file) => {
return path.extname(file).match(/\.(png|jpe?g|svg)$/)
})
expect(Object.keys(imageList)).toEqual(imageFiles)
})
If there really are too many, or they will change often, I generally like to automate the code creation on the command line. Below is quick n dirty, but should be close to what you need, depending on paths:
#!/usr/bin/env node
const { readdirSync, writeFileSync } = require('fs')
const { extname, join } = require('path')
const relPath = (...paths) => join(__dirname, ...paths)
const dir = relPath('src', 'images')
const printLoaderFile = () => {
const imageFiles = readdirSync(dir).filter((file) => {
return extname(file).match(/\.(png|jpe?g|svg)$/)
})
const output = []
imageFiles.forEach((file, index) => {
output.push(`import image${index} from '${dir}/${file}'`)
})
output.push('\nexport default {')
imageFiles.forEach((file, index) => {
output.push(` '${file}': image${index},`)
})
output.push('}\n')
writeFileSync(relPath('src', 'imagesList.js'), output.join('\n'))
}
printLoaderFile()
Revising Home.js in the following way resulted in the images loading fast and without refreshing:
import React from 'react';
import 'bootstrap/dist/css/bootstrap.css';
import './index.css';
var imageList = [];
function importAll(r) {
const keys = r.keys();
let images = {};
for (var k in keys) {
images[keys[k].replace('./', '')] = r(keys[k]);
}
return images;
}
const images = importAll(require.context('./images/resized/', false, /\.(png|jpe?g|svg)$/));
for (var image in images) {
var newImage = new Image(images[image], images[image], null);
newImage.name = images[image];
newImage.src = images[image];
imageList.push(newImage);
}
class Home extends React.Component {
constructor() {
super();
this.state = {
images: [],
}
}
componentDidMount() {
this.setState({ images: imageList });
}
render() {
let imagesFinal = [];
for (var image in this.state.images) {
console.log(this.state.images[image].src);
if (this.state.images[image].naturalHeight > this.state.images[image].naturalWidth) {}
imagesFinal.push(<img src={this.state.images[image].src} className='landscape-img' alt=''></img>);
}
return <div>{imagesFinal}</div>;
}
}
export default Home;

Testing DOM in Enzyme

Let's say I have a tiny component like this:
Button.js
import React from 'react';
import './Button.css';
export default class Button extends React.Component {
render() {
return (
<a href={ this.props.url } className={`button button-${ this.props.type }`}>
{ this.props.content }
</a>
);
}
}
And there's some super basic styling like this:
Button.css
.button {
color: white;
padding: 1em;
border-radius: 5px;
text-decoration: none;
}
.button-primary {
background-color: red;
}
.button-primary:hover {
background-color: darkred
}
.button-secondary {
background-color: aqua;
color: black;
}
.button-secondary:hover {
background-color: darkcyan;
color: white;
}
And let's say I want to write some tests for this:
Button.test.js
import React from 'react';
import Enzyme, {shallow, mount} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({adapter: new Adapter()});
import Button from './Button';
import './Button.css';
// Render buttons
const primaryButton = mount(
<Button
content="Primary button"
url="http://www.amazon.co.uk"
type="primary"
/>
);
const secondaryButton = mount(
<Button
content="Secondary button"
url="http://www.ebay.co.uk"
type="secondary"
/>
);
it('should exist', () => {
expect(primaryButton).toBeDefined();
expect(secondaryButton).toBeDefined();
});
it('should display text in the button', () => {
expect(primaryButton.text()).toEqual('Primary button');
});
it('should have the correct CSS classes', () => {
expect(primaryButton.find('.button').hasClass('button-primary')).toEqual(true);
expect(secondaryButton.find('.button').hasClass('button-secondary')).toEqual(true);
});
I've set this up using react-create-app and all the above works perfectly.
My question is: how do I test that what is getting rendered looks correct? For example, in this case I would want to make sure that the buttons have the correct background colours defined in the CSS file and that they have the correct border radius. This will prevent other developers accidentally overriding critical styling for example.
I was under the impression that Enzyme did this out of the box, but I cannot understand how to interrogate the virtual DOM which I assume is happening in the background? I thought that JSDOM was automatically running and I'm executing this from the CLI which is a Node environment.
I've tried this so far:
it('should have the correct background colours', () => {
const domNode = primaryButton.find('.button').at(0).getDOMNode();
const background = getComputedStyle(domNode).getPropertyValue('background');
expect(background).toBe('red');
});
But background is returned blank, in fact if I do console.log(getComputedStyle(domNode)) I get this returned which seems to be missing the styles:
console.log src/modules/Button/Button.test.js:42
CSSStyleDeclaration {
_values: {},
_importants: {},
_length: 0,
_onChange: [Function] }
The getDOMNode of an enzyme wrapper gets you the corresponding DOM node.
You can then use getComputedStyle to get the style of that DOM:
const renderedComponent = mount(<MyComponent /);
const domNode = renderedComponent.find('div').at(0).getDOMNode();
const background = getComputedStyle(domNode).getPropertyValue('background');
expect(background).toBe('red');

Categories

Resources