I've created a React NavBar following a tutorial, when I click the burger menu, the nav expands and collapses as expected, but when I click a link on the nav menu, it goes to the page but the nav bar doesn't collapse. I've checked a few questions/guides but they all link to Bootstrap and this code doesn't use Bootstrap, I'd rather not change the NavBar to Bootstrap if it can be avoided! Any help would be appreciated.
import React, { Component } from "react";
import logo from "../images/logo.svg";
import { FaAlignRight } from "react-icons/fa";
import { Link } from "react-router-dom";
export default class Navbar extends Component {
state = {
isOpen: false
};
handleToggle = () => {
this.setState({ isOpen: !this.state.isOpen });
};
componentDidMount() {
window.addEventListener("scroll", this.resizeHeaderOnScroll);
window.addEventListener("scroll", this.navTransparent);
window.addEventListener("scroll", this.navShadow);
};
resizeHeaderOnScroll() {
const distanceY = window.pageYOffset || document.documentElement.scrollTop,
shrinkOn = 100,
headerEl = document.getElementById("logo");
if (distanceY > shrinkOn) {
headerEl.classList.add("logoShrink");
} else {
headerEl.classList.remove("logoShrink");
}
}
navTransparent() {
const distanceY = window.pageYOffset || document.documentElement.scrollTop,
shrinkOn = 100,
headerEl = document.getElementById("navbar");
if (distanceY > shrinkOn) {
headerEl.classList.add("navbarBg");
} else {
headerEl.classList.remove("navbarBg");
}
}
navShadow() {
const distanceY = window.pageYOffset || document.documentElement.scrollTop,
shrinkOn = 100,
headerEl = document.getElementById("navbar");
if (distanceY > shrinkOn) {
headerEl.classList.add("navShadow");
} else {
headerEl.classList.remove("navShadow");
}
}
render() {
return <nav id="navbar">
<div className="nav-center">
<div className="nav-header">
<Link to="/">
<img id="logo" src={logo} alt="" />
</Link>
<button type="button" className="nav-btn" onClick={this.handleToggle}>
<FaAlignRight className="nav-icon" />
</button>
</div>
<ul className={this.state.isOpen ? "nav-links show-nav" : "nav-links"}>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/nigelservices">Services</Link>
</li>
<li>
<Link to="/contact">Contact</Link>
</li>
</ul>
</div>
</nav>;
}
}
To answer the question here you can always do this because the Link component accepts the onClick prop:
export default class Navbar extends Component {
// Rest of your code
handleLinkClick = () => {
this.setState({ isOpen: false });
};
render() {
return (
// Your JSX
<Link to="/" onClick={handleLinkClick}>Home</Link>
)
}
}
Remember to add this in every link component.
As a side note you can also use the NavLink component in react router to handle the styling when the route is the current one. https://reactrouter.com/web/api/NavLink
Related
I am trying to implement a custom dropdown menu in react, but I am facing some weird issues.
I can't understand why the items in the Services menu don't show up onMouseEnter.
With debugger, the function onMouseEnter gets called, but not able to see list of items
Thanks in advance.
import Dropdown from '../Dropdown'
import React, { useState } from "react";
import { Link } from "react-router-dom";
const Navbar = () => {
const [click, setClick] = useState(false)
const [dropdown, setDropdown] = useState(false)
const handleClick = () => setClick(!click)
const closeMobileMenu = () => setClick(false)
const onMouseEnter = () => {
if (window.innerWidth < 960) {
setDropdown(false)
} else {
setDropdown(true)
}
}
const onMouseLeave = () => {
if (window.innerWidth < 960) {
setDropdown(false)
} else {
setDropdown(false)
}
}
return (
<>
<nav className="nb">
LOGO
<div className='menu-icon' onClick={handleClick}>
<i className={click ? 'fas fa-times' : 'fas fa-bars'}/>
</div>
<ul>
<li
className='nav-item'
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<Link
to='/services'
className='nav-links'
onClick={closeMobileMenu}
>
Services <span className='fas fa-caret-down'></span>
</Link>
{dropdown && <Dropdown />}//here is the problem. I can't see items onMouseEnter
</li>
</ul>
</nav>
</>
)
}
export default Navbar
Here is the Dropdown.js class
import "./Dropdown.css";
import React, { useState } from "react";
import { Link } from "react-router-dom";
const MenuItems = [
{
title: 'Marketing',
path: '/marketing',
cName: 'dropdown-links'
},
]
const Dropdown = () => {
const [click, setClick] = useState(false);
const handleClick = () => setClick(!click);
return (
<>
<ul
onClick={handleClick}
className={click ? "dropdown-menu clicked" : "dropdown-menu"}
>
{MenuItems && MenuItems.map((item, index) => {
return (
<li key={index}>
<Link
className={item.cName}
to={item.path}
onClick={() => setClick(false)}
>
{item.title}
</Link>
</li>
);
})}
</ul>
</>
);
};
export default Dropdown;
you can use these links in index.html so that we have the same icon
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.15.3/css/all.css"
integrity="sha384-SZXxX4whJ79/gErwcOYf+zWLeJdY/qpuqC4cAa9rOGUstPomtqpuNWT9wdPEn2fk"
crossorigin="anonymous"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=PT+Sans:wght#700&display=swap"
rel="stylesheet"
/>
In the following code
import { useEffect } from "react";
import Link from "next/link";
import styles from "./header.module.css";
import { useRouter } from "next/router";
export default function Header() {
const router = useRouter();
const handleClick = (path) => {
if (path === "/mvg") {
console.log("I clicked on the mvg Page");
}
};
useEffect(() => {
const header = document.querySelector("header");
const nav = document.querySelector("nav");
if (router.pathname === "/") {
header.classList.add("headerTest");
nav.classList.add("menuTest");
}
}, []);
useEffect(() => {
const header = document.querySelector("header");
const nav = document.querySelector("nav");
function checkScroll() {
if (window.innerWidth <= 768) {
header.classList.remove("headerTest");
nav.classList.remove("menuTest");
}
if (window.scrollY === 0) {
header.classList.add("headerTest");
nav.classList.add("menuTest");
}
}
window.addEventListener("scroll", checkScroll);
// Remove event listener on cleanup
return () => window.removeEventListener("resize", checkScroll);
}, []); // Empty array ensures that effect is only run on mount
return (
<header className={styles.header}>
<h1>
<Link href="/">
<a>
<span className="offscreen">Home</span>
</a>
</Link>
</h1>
<nav id="menu" className={styles.menu}>
<ul>
<li>
<Link href="/mvg">
<a onClick={() => handleClick("/mvg")}>MVG</a>
</Link>
</li>
</ul>
</nav>
</header>
);
}
I'd like my header background to be blue and its menu item text be white when the user arrives on the home page by adding classes in my pages/globals.css, which I achieve with
if (router.pathname === "/") {
header.classList.add("headerTest");
nav.classList.add("menuTest");
}
Then when the user scrolls down on the home page, I want the header to be white and the menu item text to return to its default. But when the user scrolls back to the top, I want the header to be blue and its menu item text be white again, which I achieve with my checkScroll function.
My problem is that when the user clicks on the mvg page link, I want the header to be white and its menu item text be its default color on the mvg page arrival, when the mvg page is scrolled down, and when the mvg page is scrolled to the top.
The current code fails the last condition so that whenever the mvg page is scrolled to the top, the mvg page header is blue and its menu item text is white, from the same checkScroll function.
How do I revise my code so that the mvg page header is blue and its menu item text be the default color when the user scrolls to the top while still having my home page header be white and its menu item text be white when scrolled to the top?
My codesandbox
The following solves the issue
import { useState, useEffect } from "react";
import Link from "next/link";
import styles from "./header.module.css";
import { useRouter } from "next/router";
export default function Header() {
const [test, setTest] = useState(false); // Defaults to `false`
const router = useRouter();
useEffect(() => {
const header = document.querySelector("header");
const nav = document.querySelector("nav");
if (router.pathname === "/") {
setTest(true);
header.classList.add("headerTest");
nav.classList.add("menuTest");
}
}, []);
useEffect(() => {
const header = document.querySelector("header");
const nav = document.querySelector("nav");
function checkScroll() {
if (window.innerWidth <= 768) {
header.classList.remove("headerTest");
nav.classList.remove("menuTest");
}
if (window.scrollY === 0) {
header.classList.add("headerTest");
nav.classList.add("menuTest");
}
}
if (test) {
window.addEventListener("scroll", checkScroll);
// Remove event listener on cleanup
return () => window.removeEventListener("resize", checkScroll);
}
}, [test]);
return (
<header className={styles.header}>
<h1>
<Link href="/">
<a>
<span className="offscreen">Home</span>
</a>
</Link>
</h1>
<nav id="menu" className={styles.menu}>
<ul>
<li>
<Link href="/mvg">
<a>MVG</a>
</Link>
</li>
</ul>
</nav>
</header>
);
}
I know this should be stupid simple. All of the examples I can find on here are stupid simple. But for some reason I can't get a className to change conditionally in my component.
I'm trying to animate the site logo when the user scrolls down. fullLogo is changing as expected according to the logs. But the class never changes. The only thing I can think is that once the render happens that's it, and it needs to be declared as a class in order to re-render like that. Is that the issue?
const headerHeight = 89
let fullLogo = true
window.addEventListener('scroll', (e) => {
if (window.pageYOffset > headerHeight) {
console.log('scrolled beyond header height')
fullLogo = false;
} else {
fullLogo = true;
}
console.log('fullLogo', fullLogo)
})
<header className={(!!fullLogo ? 'full-logo' : '')}>
Here's the full component if that helps:
import React, { useState } from "react"
import PropTypes from "prop-types"
import {
Collapse,
Navbar,
NavbarToggler,
NavbarBrand,
Nav,
NavItem,
NavLink
} from 'reactstrap'
import "./header.scss"
const Header = ({ siteTitle, menu, location }) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
const headerHeight = 89
let fullLogo = true
window.addEventListener('scroll', (e) => {
if (window.pageYOffset > headerHeight) {
console.log('scrolled beyond header height')
fullLogo = false;
} else {
fullLogo = true;
}
console.log('fullLogo', fullLogo)
})
menu.forEach((item, index) => {
if (item.path === location.pathname) {
item.active = true
} else {
item.active = false
}
})
return (
<header className={(!!fullLogo ? 'full-logo' : '')}>
<Navbar color="light" light expand="md" fixed="top">
<NavbarBrand href="/">
<span className="initial">L</span>
<span className="rest">OGO</span>
<span className="splitter"></span>
<span className="initial">N</span>
<span className="rest">AME</span>
</NavbarBrand>
<NavbarToggler onClick={toggle} />
<Collapse isOpen={isOpen} navbar>
<Nav className="mr-auto" navbar>
{menu.map((item, index) => (
<NavItem key={`nav_link_${index}`}>
<NavLink href={item.path} className={item.active ? `active` : null}>{item.title}</NavLink>
</NavItem>
))}
</Nav>
</Collapse>
</Navbar>
</header>
)
}
Header.propTypes = {
siteTitle: PropTypes.string,
menu: PropTypes.array
}
Header.defaultProps = {
siteTitle: ``,
menu: {
title: `Home`,
path: `/`
}
}
export default Header
The code by using hooks be like
import React, { useState } from "react"
import PropTypes from "prop-types"
import {
Collapse,
Navbar,
NavbarToggler,
NavbarBrand,
Nav,
NavItem,
NavLink
} from 'reactstrap'
import "./header.scss"
const Header = ({ siteTitle, menu, location }) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
const headerHeight = 89
const [fullLogo,setFullLogo] = useState(true)
window.addEventListener('scroll', (e) => {
if (window.pageYOffset > headerHeight) {
console.log('scrolled beyond header height')
setFullLogo(false);
} else {
setFullLogo(true);
}
console.log('fullLogo', fullLogo)
})
menu.forEach((item, index) => {
if (item.path === location.pathname) {
item.active = true
} else {
item.active = false
}
})
return (
<header className={(!fullLogo ? 'full-logo' : '')}> //If You Want to toggle current state
<Navbar color="light" light expand="md" fixed="top">
<NavbarBrand href="/">
<span className="initial">L</span>
<span className="rest">OGO</span>
<span className="splitter"></span>
<span className="initial">N</span>
<span className="rest">AME</span>
</NavbarBrand>
<NavbarToggler onClick={toggle} />
<Collapse isOpen={isOpen} navbar>
<Nav className="mr-auto" navbar>
{menu.map((item, index) => (
<NavItem key={`nav_link_${index}`}>
<NavLink href={item.path} className={item.active ? `active` : null}>{item.title}</NavLink>
</NavItem>
))}
</Nav>
</Collapse>
</Navbar>
</header>
)
}
Header.propTypes = {
siteTitle: PropTypes.string,
menu: PropTypes.array
}
Header.defaultProps = {
siteTitle: ``,
menu: {
title: `Home`,
path: `/`
}
}
export default Header
Your assumption is correct. There's no lifecycle event that's triggering a re-render. Functional components only update if you change props or use hooks. Here you could use a hook to setDisplayFullLogo.
I'm having problem with sliding menu in just one item.
When I click on the config button every item shows menu. I tried to figure out something by passing props {mail.id} but I'm afraid I don't understand this.
I would like to have sliding menu just in one item -- the clicked one.
This is ConfigButton
import React, { Component } from "react";
import './Menu.css';
class ConfigButton extends Component {
render() {
return (
<button className="configButton"
onClick={this.props.onClick}
>
<i className="configButtonIcon fas fa-cog"></i>
</button>
);
}
}
export default ConfigButton;
And this is the Component which renders:
import React, { Component } from 'react';
import { NavLink, HashRouter } from 'react-router-dom';
import axios from 'axios';
import Menu from './Menu';
import ConfigButton from './ConfigButton';
const API = myAPI;
const navLinkStyle = {
textDecoration: 'none',
color: '#123e57'
};
class Emails extends Component {
constructor(props) {
super(props);
this.state = {
visible: false,
mails: []
};
this.handleMouseDown = this.handleMouseDown.bind(this);
this.toggleMenu = this.toggleMenu.bind(this);
}
handleMouseDown(e) {
this.toggleMenu();
e.stopPropagation();
}
toggleMenu() {
this.setState({
visible: !this.state.visible
});
}
componentDidMount() {
axios.get(API)
.then(response => {
const mails = response.data;
this.setState({ mails });
})
}
truncate = (text, chars = 140) =>
text.length < chars ? text : (text.slice(0, chars) + '...')
render() {
let mails = this.state.mails;
console.log(mails);
mails = mails.map(mail => {
return (
<div key={mail.id}>
<div className='mail'>
{
!mails.displayed
? <i className="notDisplayed fas fa-circle"></i>
: <i className="displayed far fa-circle"></i>
}
<HashRouter>
<NavLink
to={`/openemail/${mail.id}`}
style={navLinkStyle}
>
<ul className='ulMailWrap'>
<div className='mailHeader'>
<li>{mail.sender}</li>
<li>{mail.created}</li>
</div>
<li>{mail.subject}</li>
<li>{this.truncate(mail.message)}</li>
</ul>
</NavLink>
</HashRouter>
<ConfigButton onClick={this.handleMouseDown} />
<Menu handleMouseDown={this.handleMouseDown}
menuVisibility={this.state.visible}
/>
</div>
</div>
)
});
return (
<div>
{ mails }
</div>
);
}
}
export default Emails;
You can pass a function that will send a different parameter to the handler, depending on value of each element in the array.
Do something like this:
...
<div key={mail.id} onClick={() => this.handleOpenMenu(mail.id)}>
...
Then at the handler:
handleOpenMenu = id => {
// do different stuffs on the id you get here
this.setState({ visibleMenuId: id });
}
And then change the props you are passing to your menu component:
<Menu menuVisibility={this.state.visibleMenuId === mail.id} />
I need to filter AssessmentCards by Year. I made the method.
But I need to call clickAllCards and clickYearCard method in onClick event on other file. How can I do that?
This is my code with the methods, I'm using Pug.JS to render:
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import messages from './messages';
import { getAssessmentsCards } from '../../functions';
import template from './index.pug';
const cardsAssessments = getAssessmentsCards();
export default class CardAssessment extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function
constructor(props){
super(props);
this.state = {
listCards: [],
openCm: false,
}
}
componentWillMount(){
this.setState({listCards: cardsAssessments});
}
hover() {
this.setState({openCm: !this.state.openCm});
}
clickAllCards(e){
e.preventDefault();
this.setState({listCards: cardsAssessments});
}
clickYearCard(e){
e.preventDefault();
var filtered = cardsAssessments.filter((data) => {
return data.yearCard === '2018';
});
this.setState({listCards: filtered});
}
render() {
let cm = ["card-menu"];
if(this.state.openCm) {
cm.push('active');
}
return template.call(this, {
messages,
FormattedMessage,
Link,
cm
});
}
}
This is my pug file:
.card-adjust
div(href="" onClick="{this.clickYearCard.bind(this)}") 2018
div(href="" onClick="{this.clickAllCards.bind(this)}") All
Link.card.add-new(to="/add-assessment")
span
.add-icon
i.ti-plus
|
FormattedMessage(__jsx='{...messages.addAssessment}')
.card.card-materia(#for='data in this.state.listCards', key='{data.id}')
.card-body(id="{data.id}")
div(className="{cm.join(' ')}" onClick="{this.hover.bind(this)}")
i.fas.fa-ellipsis-v
.cm-floating
Link.cmf-agenda(to="/agendamento")
i.ti-agenda
|
FormattedMessage(__jsx='{...messages.scheduled}')
Link.cmf-copy(to="#")
i.pe-7s-copy-file
|
FormattedMessage(__jsx='{...messages.copy}')
Link.cmf-trash(to="#")
i.ti-trash
|
FormattedMessage(__jsx='{...messages.delete}')
.cm-icon
i(className='{data.icon}')
h2.cm-title {data.disciplineAbbreviation}
span.badge.badge-danger {data.status}
p.cm-questions {data.questionNumber}
FormattedMessage(__jsx='{...messages.questions}')
.cm-info
Link(to="#") {data.disciplineName}
Link(to="#") {data.year}
Link(to="#") {data.segment}
.cm-date
//- i.pe-7s-refresh-2
| {data.date}
And this is the file where I need to put the onClick event:
import React from 'react';
import { FormattedMessage } from 'react-intl';
import messages from './messages';
import template from './index.pug';
import '../../assets/scss/main.scss';
export default function (params = {}) {
const { messages, FormattedMessage } = params;
return (
<div>
<ul className="nav nav-tabs">
<li className="nav-item">
<a className="nav-link" href="#">
<FormattedMessage {...messages.all} />
</a>
</li>
<li className="nav-item">
<a className="nav-link" href="#">2018</a>
</li>
<li className="nav-item">
<a className="nav-link" href="#">2017</a>
</li>
</ul>
<div className="navigation-tabs display-none">
<a>
<i className="nt-icon ti-angle-left" />
</a>
1 de 3
<a>
<i className="nt-icon ti-angle-right" />
</a>
</div>
</div>
);
}
Thanks
You can pass any method for onClick event to any component like that:
App.js
class App extends React.Component {
handleClick = () => alert( "Clicked" );
render() {
return (
<div>
<Child click={this.handleClick}/>
</div>
)
}
}
or with a function component if you don't need lifecylce methods or "this" (here we don't need):
const App = () => {
const handleClick = () => alert( "Clicked" );
return (
<div>
<Child click={handleClick}/>
</div>
);
}
Child.js
const Child = ( props ) => (
<div>
<button onClick={props.click}>Click me!</button>
</div>
)