Lets say I have component "Post" which holds multiple components "Comment". I want to make that application scrolls down on comment with that anchor when I enter URL like this:
/post/:postId/#commentId
I have already working postId route /post/:postId
I tried to implement it with react-hash-link npm package but it's not working as intended.
Every comment has it's own ID which is set on component, like this:
<div class="post">
<div class="post-header">
<div class="post-header-avatar">
SOME TEXT
</div>
<div class="post-header-info">
SOME TEXT
</div>
</div>
<div class="post-content">
<span>POST CONTENT</span>
</div>
<div class="post-likers-container">
<div class="post-likers-header label">People who like this post</div>
<div class="post-likers">
SOME TEXT
</div>
</div>
<div class="post-comments">
<div class="comments ">
<div class="comments-all label">Comments</div>
<div class="comments">
<div class="comment" id="5d27759edd51be1858f6b6f2">
<div class="comment-content">
COMMENT 1 TEXT
</div>
</div>
<div class="comment" id="5d2775b2dd51be1858f6b720">
<div class="comment-content">
COMMENT 2 TEXT
</div>
</div>
<div class="comment" id="5d2775ecdd51be1858f6b753">
<div class="comment-content">
COMMENT 3 TEXT
</div>
</div>
</div>
</div>
</div>
</div>
So for example if I open URL like:
/post/postId/#5d2775ecdd51be1858f6b753
I want to open page of post and that it scrolls down to the comment with # anchor.
Is there any way to implement this?
I managed to find simple solution for my use case, without creating refs for comments, passing them, etc. Since my hierarchy of components is like this:
Post --> render component Comments
Comments --> render
multiple components Comment with props data passed from Post
In Post component I created function:
scrollToComment= () => {
let currentLocation = window.location.href;
const hasCommentAnchor = currentLocation.includes("/#");
if (hasCommentAnchor) {
const anchorCommentId = `${currentLocation.substring(currentLocation.indexOf("#") + 1)}`;
const anchorComment = document.getElementById(anchorCommentId);
if(anchorComment){
anchorComment.scrollIntoView({ behavior: "smooth" });
}
}
}
Then I render Comments like this:
<Comments limit={limit} post={post} scrollToComment={this.scrollToComment} />
In Comments I generate comments after some sorting like this:
{sortedComments.map((comment, i) => <Comment key={i} {...comment} scrollToComment={this.props.scrollToComment}/> )}
and finally in Comment component I execute scrollToComment in ComponentDidMount():
if(this.props.scrollToComment)
this.props.scrollToComment(this.props._id);
After that when I go to some URL I get nice smooth scrolling to the comment specified in hash part of URL.
I tried #Christopher solution but it didn't worked for me.
I really liked your solution #SaltyTeemooo. Inspired by it I found an even simpler way without any callbacks.
My setup is very similar so lets say I am dealing with posts and comments.
In Post I create the Comments (simpified) like this and pass the anchorId:
<Comments anchorId={window.location.href.slice(window.location.href.indexOf("#") + 1)} props... />
In Comments I pass the anchor id along into Comment.js
<Comment anchorId={props.anchorId} props.../>
And then in the Comment, I scroll the current element into view, if it is the linked one
import React, { useRef, useEffect } from 'react';
function Comment () {
const comment = useRef(null); //to be able to access the current one
useEffect(() => {
if(props.anchorId === props.commentData.id)
{
comment.current.scrollIntoView({ behavior: "smooth" });
}
}, []) //same as ComponentDidMount
return(
<div id={props.commentData.id} ref={comment}> //here is where the ref gets set
...
</div>
)
}
Took a pretty solid amount of time but try this sandbox: https://codesandbox.io/s/scrollintoview-with-refs-and-redux-b881s
This will give you a ton of insight on how to scroll to an element using a URL param.
import React from "react";
import { connect } from "react-redux";
import { getPost } from "./postActions";
class Post extends React.Component {
constructor(props) {
super(props);
this.state = {
activeComment: null
};
this._nodes = new Map();
}
componentDidMount() {
this.props.getPost(this.props.match.params.id);
const path = window.location.href;
const commentId = path.slice(path.indexOf("#") + 1);
if (commentId) {
this.setState({
activeComment: commentId
});
}
}
componentDidUpdate(prevProps, prevState) {
if (this.state.activeComment !== prevState.activeComment) {
this.scrollToComment();
}
}
scrollToComment = () => {
const { activeComment } = this.state;
const { comments } = this.props.posts.post;
const nodes = [];
//Array.from creates a new shallow-copy of an array from an array-like or iterable object
Array.from(this._nodes.values()) //this._nodes.values() returns an iterable-object populated with the Map object values
.filter(node => node != null)
.forEach(node => {
nodes.push(node);
});
const commentIndex = comments.findIndex(
comment => comment.id == activeComment
);
if (nodes[commentIndex]) {
window.scrollTo({
behavior: "smooth",
top: nodes[commentIndex].offsetTop
});
}
};
createCommentList = () => {
const { post } = this.props.posts;
const { activeComment } = this.state;
if (post) {
return post.comments.map((comment, index) => {
return (
<div
key={comment.id}
className={
"comment " + (activeComment == comment.id ? "activeComment" : "")
}
ref={c => this._nodes.set(comment.id, c)}
>
{comment.text}
</div>
);
});
}
};
displayPost = () => {
const { post } = this.props.posts;
if (post) {
return (
<div className="post">
<h4>{post.title}</h4>
<p>{post.text}</p>
</div>
);
}
};
render() {
return (
<div>
<div>{this.displayPost()}</div>
<div>{this.createCommentList()}</div>
</div>
);
}
}
const mapStateToProps = state => {
return {
posts: state.posts
};
};
const mapDispatchToProps = dispatch => {
return {
getPost: postId => {
dispatch(getPost(postId));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Post);
In my simple case where there is no async content loading, I got the desired scrolling behavior by just adding this at the top of the page:
useEffect(() => {
const href = window.location.href
if (href.includes("#")) {
const id = `${href.substring(href.indexOf("#") + 1)}`
const anchor = document.getElementById(id)
if(anchor){
anchor.scrollIntoView({ behavior: "smooth" })
}
}
}, [])
FYI, this was for some FAQ pages consisting of a bunch of FaqEntry objects, each with a question and answer. The code below allows linking to individual entries that initialize with the answer open.
export default function FaqEntry({title, history, children}) {
if(!history) console.log("OOPS, you forgot to pass history prop", title)
const createName = title => title.toLowerCase().replace(/[^\sa-z]/g, "").replace(/\s\s*/g, "_")
const id = createName(title)
const href = window.location.href
const isCurrent = href.includes("#") && href.substring(href.indexOf("#") + 1) === id
const [open, setOpen] = useState(isCurrent)
function handleClick() {
setOpen(!open)
if (history && !open) {
const pathname = window.location.pathname + "#" + id
history.replace(pathname)
}
}
return <div id={id} className={`faqEntry ${open ? "open" : "closed"}`}>
<div className="question" onClick={handleClick}>{title}</div>
<div className="answer">{children}</div>
</div>
}
I pass the history object from React Router so that I can update the browser history without triggering a page reload.
Mensure...
import React, { useEffect } from 'react';
const MainApp = () => {
const MyRef = React.createRef();
useEffect(() => { // Same like ComponentDidMount().
scrollTo();
})
const scrollTo = () => {
window.scrollTo({
top:myRef.offsetTop,
behavior: "smooth" // smooth scroll.
});
}
return (
<div ref={MyRef}>My DIV to scroll to.</div>
)
}
Related
I'm trying to remove a CSS class from a specific item when clicking on that item's button. Removing the CSS class will make a menu appear. How would I go about doing this with React? Here's the code.
import "./Homepage.css"
import React, { useState, useEffect, useRef } from "react"
// import { FontAwesomeIcon } from "#fortawesome/react-fontawesome"
// import { faArrowDown } from "#fortawesome/free-solid-svg-icons"
import { Link } from "react-router-dom"
import useFetch from "./useFetch"
import Axios from "axios"
export default function Homepage() {
const [body, setBody] = useState("")
const [sortedData, setSortedData] = useState("")
const [data, setData] = useState("")
const [css, setCss] = useState("")
const [flash, setFlash] = useState(null)
const posts = useFetch("http://localhost:5000/api/data")
const firstRender = useRef(true)
useEffect(() => {
let test = JSON.parse(window.localStorage.getItem("user"))
console.log(test)
setData(posts)
}, [posts])
useEffect(() => {
if (firstRender.current) {
firstRender.current = false
return
}
data.sort(function (a, b) {
return new Date(b.date) - new Date(a.date)
})
setSortedData(data)
}, [data])
const handleSubmit = (e) => {
e.preventDefault()
Axios.post("http://localhost:5000/api/react-create-post", { text: body }, { withCredentials: true })
.then((res) => {
setSortedData((prevArray) => [res.data.post, ...prevArray])
setFlash("Successfully created post.")
setCss("success-msg")
setBody("")
})
.catch((err) => {
setCss("error-msg")
setFlash("Field cannot be left blank.")
})
}
const handleClick = (e) => {
e.preventDefault()
e.target.parentElement.children[1]
}
return (
<div>
<center>
<div className="create-container">
<div className="posts-title">Create Post</div>
<form id="theForm" onSubmit={(e) => handleSubmit(e)}>
<textarea onChange={(e) => setBody(e.target.value)} value={`${body}`} id="theInput" className="post-input" name="text" type="text"></textarea>
<button className="submit-btn">POST</button>
</form>
</div>
<div id="postsContainer" className="posts-container">
<div className="posts-title">Latest Posts</div>
{flash ? <div className={css}>{flash}</div> : console.log()}
<div id="postInput">
{sortedData &&
sortedData.map((item) => {
return (
<div className="post-container" key={item._id}>
<Link className="a" to={`/user/${item.author}`}>
<h3 className="author">{item.author}</h3>
</Link>
<div className="date">{item.date.toLocaleString()}</div>
<div className="options-cont">
<button onClick={(e) => handleClick(e)} id="optionsBtn" className="options-btn">
<i className="fas fa-ellipsis-v"></i>
</button>
<button data-author={`${item.author}`} data-id={`${item._id}`} data-text={`${item.body}`} id="editBtn" className="edit inside-btn invisible">
Edit
</button>
<button data-author={`${item.author}`} data-id={`${item._id}`} id="deleteBtn" className="delete inside-btn invisible">
Delete
</button>
<br></br>
</div>
<p className="body-text">{item.body}</p>
</div>
)
})}
</div>
</div>
</center>
</div>
)
}
As far as I'm concerned using state as the className would remove or alter the CSS of each item in the "sortedData" array and make the menus for all items appear. I only want the menu for one of the items to appear.
As pilchard said, you probably want to make each of those its own component with its own "showing" state, or at least "showing" prop.
As far as I'm concerned using state as the className would remove or alter the CSS of each item in the "sortedData" array and make the menus for all items appear. I only want the menu for one of the items to appear.
That would be true if you used a single flag in state. But instead, use a set of flags, one flag for each menu, perhaps keyed by item._id.
Assuming you don't do the refactoring pilchard (and I) suggest:
You haven't shown us enough code for me to know whether you're using class components or function components, so I'm going to guess function components with hooks. If so, the initial state would be:
const [showing, setShowing] = useState(new Set());
Then when rendering, you'd assign the class:
<theElement className={showing.has(item._id) ? "class-if-any-to-show-it" : "class-if-any-to-not-show-it" ...
To toggle, in the button pass the ID:
<button onClick={(e) => handleClick(e, item._id)}
and then update state as appropriate:
const handleClick = (e, id) => {
e.preventDefault()
setShowing(showing => {
showing = new Set(showing);
if (showing.has(id)) {
showing.delete(id);
} else {
showing.add(id);
}
return showing;
});
};
I tried scrollIntoView and used .lastchild but I can't go to the latest comment. Used window.scrollTo too can't find any solution.
Can anyone have some suggestions on what will work?
const click = () => {
fetch('url')
.then(res => res.json())
.then(data => {
if (data.affectedRows > 0) {
var element = document.getElementById("txt").lastChild;
element.scrollIntoView({behavior: "smooth", block: "end", inline: "center"});
}
})
}
}
{getComment.map(cmnt =>
<div key={cmnt.comment_id} className="article-comments-reply">
<div className="text-box" id="txt">
<h3>{cmnt.username}</h3>
<div className="reply-box">
<h4>{cmnt.comment_text}</h4>
</div>
</div>
</div>
)}
You don't need to use last child. It is also not the best solution in React to use tools to get an element by selector.
Just set the parent element's scrollTop to its height.
It looks like this:
ref.current.scrollTop = ref.current.scrollHeight;
And this ref should point to your parent component in which this code is wrapped:
{getComment.map(cmnt =>
<div key={cmnt.comment_id} className="article-comments-reply">
<div className="text-box" id="txt">
<h3>{cmnt.username}</h3>
<div className="reply-box">
<h4>{cmnt.comment_text}</h4>
</div>
</div>
</div>
)}
If you want to add scrollBehavior via js, you can use something like this:
ref.current.style.scrollBehavior = 'smooth';
As a result, it can be easily transferred into a custom hook that can perform smooth scrolling on any element.
Custom hook useScrollToBottom.
export const useScrollToBottom = (ref) => {
const scrollToBottom = () => {
ref.current.style.scrollBehavior = 'smooth';
ref.current.scrollTop = ref.current.scrollHeight;
};
return {
scrollToBottom,
}
}
Using
// ...
const ref = useRef();
const {scrollToBottom} = useScrollToBottom(ref);
useEffect(() => {
// scroll when you need
scrollToBottom();
}, []);
return (
<div ref={ref} className="list">
{
// some elements
}
</div>
)
// ...
You can use this function from any component you want this code will target your last child.
const scrollToBottom = () => {
let targetLastElement = document.getElementsByClassName('classNameOfTileWhichYouWantToTarget');
targetLastElement = targetLastElement[targetLastElement.length - 1]
targetLastElement.scrollIntoView({ behavior: "smooth" });
}
I'm trying to create an onClick event, that will select the pressed mapped item. Could anyone help?
const renderExample= () => {
return example.map((arrayItem, i) => {
const example = arrayItem.example;
const song =
arrayItem.song ||
"urltosong";
...
then in return
<div key={i}>
<SELECTABLE>
{example}
</SELECTABLE>
<SONG>{song}</SONG>
</div>
render return <div>{renderData()}</div>;
At the moment I have a list of selectable'examples' rendering. But I want to know which example has been pressed by the user specifically.
You can pass the complete item to the handleClick to play around. The code would be
const App = () => {
function handleClick(item){
console.log('item,item);
}
function renderData(){
// assuming you have data in example array
return example && example.map(item=> {
const example = arrayItem.example;
const song =arrayItem.song || "urltosong";
return (
<div key={i} onClick={()=>{handleClick(item)}>
<SELECTABLE>
{example}
</SELECTABLE>
<SONG>{song}</SONG>
</RecentMessages>
</div>
)
})
}
return <div>{renderData()}</div>
}
Maintain state for selected song and update when selection change.
Here is minimal working sample with stackblitz
import React, { Component } from 'react';
import { render } from 'react-dom';
const songs = ["first song", "song 2", "hello song"];
class App extends Component {
constructor() {
super();
this.state = {
song: 'hello song'
};
}
render() {
return (
<div>
<select onChange={(ev) => this.setState({song: ev.target.value})} value={this.state.song}>
{songs.map(x => <option value={x}> {x} </option>)}
</select>
<p>
{this.state.song}
</p>
</div>
);
}
}
render(<App />, document.getElementById('root'));
You can play with it like this:
// Supposing you're using React Functional Component
// Click handler
const handleSelect = (elementIndex) => {
console.log(`You clicked on element with key ${elementIndex}`)
}
// render()
// ... some other to-render stuff
// implying that code below have access to element's index (i in your map)
<Selectable onClick={e => handleSelect(i)}>
{example}
</Selectable>
// ...
I have a JS class called "Cell.js" and I want to redirect to another JS Page called "Detail.js" when the user clicked on a button. But I do not know how to redirect AND pass a variable at the same time.
I am working on a Pokedex (Pokemon List) and when the user clicks on the f.e. first Pokemon which has ID = 1, the ID should get passed to the Detail.js page where it shows more details of the selected Pokemon.
Cell.js code =
import React from 'react';
import './Cell.css';
import {ClassDeclaration as pokemon} from "#babel/types";
function Cell({ pokemon }) {
let id = pokemon.name;
return (
<a href={"Detail.js?id= " + id } onclick="passID()">
<div className="Cell">
<div className="Cell_img">
<img src={pokemon.sprites.front_default} alt="" />
</div>
<div className="Cell_name">
{pokemon.name}
</div>
</div>
</a>
);
}
function passID(){
return(
pokemon.id
);
}
export default Cell;
And here is the target JS page "Detail.js":
import React, {useState} from 'react';
import './Detail.css';
const queryString = window.location.search;
console.log(queryString);
const urlParams = new URLSearchParams(queryString);
const id = urlParams.get('id');
Detail(id)
function Detail(pokemon) {
return (
<div className="Detail">
<div className="Detail_img">
<p>TEST</p>
</div>
<div className="Detail_name">
{pokemon.name}
</div>
</div>
);
}
async function getPoke(id) {
console.log(id);
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
const json = await res.json();
console.log(json);
}
export default Detail;
Detail.js is not done yet, I could test anything cuz I didn't know how to redirect and send the variable. Hope you can help me out
PS: I am very new to JS xD
Navigation in React will not happen that way!. React is intended for Single Page Application(SPA). ReactDOM.render() will load the container in which different views can be switched.
Recommended is react-router, but you could do this way for experiments.
You can pass any info as props to view component (in your case, id)
import React, { useState } from "react";
const App = () => {
const [pageNo, setPageNo] = useState(1);
let id = "Xyz"
return (
<div>
<header>
<span onClick={() => setPageNo(1)}>View1</span>
<span onClick={() => setPageNo(2)}>View2</span>
<span onClick={() => setPageNo(3)}>View3</span>
</header>
{loadView(pageNo, id)}
</div>
);
};
const loadView = (pageNo, id) => {
switch (pageNo) {
case 1:
return <View1 id={id}/>;
case 2:
return <View2 id={id}/>;
case 3:
return <View3 id={id}/>;
}
};
const View1 = ({id}) => <div>View 1 pokeman name = {id}</div>;
const View2 = ({id}) => <div>View 2 pokeman name = {id}</div>;
const View3 = ({id}) => <div>View 3 pokeman name = {id}</div>;
CSS
header {
border-bottom: 1px solid #c4c4c4;
}
header span {
margin: 8px;
cursor: pointer;
text-decoration: underline;
}
First watch this, so you can see the behavior going on.
Timing Issue (JS in one component relies on another component to exist first)
I need to be able to somehow check that another component exists before I apply this JS in this component's ComponentDidMount
const TableOfContents = Component({
store: Store('/companies'),
componentDidMount() {
const el = ReactDOM.findDOMNode(this);
console.log("table of contents mounted");
if(document.getElementById('interview-heading') && el) {
new Ink.UI.Sticky(el, {topElement: "#interview-heading", bottomElement: "#footer"});
}
},
it does hit my if statement and does hit the Sticky() function but I still think I have problems when I refresh the page whereas this JS isn't working on the interview-heading component for some reason.
Note the id="interview-heading" below.
const InterviewContent = Component({
componentDidMount() {
console.log("InterviewContent mounted");
},
render(){
var company = this.props.company;
return (
<div id="ft-interview-content">
<p className="section-heading bold font-22" id="interview-heading">Interview</p>
<InterviewContentMain company={company}/>
</div>
)
}
})
const InterviewContentMain = Component({
componentDidMount() {
console.log("InterviewContentMain mounted");
},
render(){
var company = this.props.company;
return (
<div id="interview-content" className="clear-both">
<div className="column-group">
<div className="all-20">
<TableOfContents company={company}/>
</div>
<div className="all-80">
<InterviewContainer company={company}/>
</div>
</div>
</div>
)
}
})
export default InterviewContent;
I realize TableOfContents is being rendered before InterviewContent because it's a child of TableOfContents and I believe in React children are rendered before their parents (inside-out)?
I think you need to rethink your component structure. I don't know your entire setup, but it looks like you should probably have a shared parent component pass the message from TableOfContents to InterviewContent:
const InterviewContentMain = Component({
getInitialState() {
return {
inkEnabled: false
}
},
componentDidMount() {
console.log("InterviewContentMain mounted");
},
enableInk() {
this.setState({ inkEnabled: true });
}
render(){
var company = this.props.company;
return (
<div id="interview-content" className="clear-both">
<div className="column-group">
<div className="all-20">
<TableOfContents inkEnabled={this.state.inkEnabled} company={company}/>
</div>
<div className="all-80">
<InterviewContainer enableInk={this.enableInk} company={company}/>
</div>
</div>
</div>
)
}
})
const TableOfContents = Component({
store: Store('/companies'),
componentDidMount() {
console.log("table of contents mounted");
this.props.enableInk();
},
...
const InterviewContent = Component({
enableInk() {
new Ink.UI.Sticky(el, {topElement: "#interview-heading", bottomElement: "#footer"});
},
// willReceiveProps isn't called on first mount, inkEnabled could be true so
componentDidMount() {
if (this.props.inkEnabled) {
this.enableInk();
}
},
componentWillReceiveProps(nextProps) {
if (this.props.inkEnabled === false && nextProps.inkEnabled === true) {
this.enableInk();
}
}
render(){
var company = this.props.company;
return (
<div id="ft-interview-content">
<p className="section-heading bold font-22" id="interview-heading">Interview</p>
<InterviewContentMain company={company}/>
</div>
)
}
})
Then have componentDidMount trigger this.props.enableInk().
Or better yet, why not just put the Ink.UI.Sticky call in componentDidMount of InterviewContent?