I've created a TabHeader which creates a Tab Menu. I want to show table contents based on months, and I've created a loop that runs through the current month and back to January. It is rendered correctly, but the content is changed out dynamically by the API based on the current URL. So, when I change the link/content, then the correct content is loaded, but the current marked Tab is still the first link. I'm using Material UI and would like to use the indicator that's packaged with the Tab component. I'm also coding in Typescript.
I've tried to do some Google searches, but can't seem to find someone with somewhat the same problem as I've got.
This is the TabHeader:
function TabHeader(props) {
const [value, setValue] = React.useState(0);
const { children, header, classes } = props;
function handleChange(event: React.ChangeEvent<{}>, newValue: number) {
setValue(newValue);
}
return (
// Removed for simplicity
<Tabs
value={value}
onChange={handleChange}
indicatorColor="primary"
variant="scrollable"
scrollButtons="auto"
>
{children}
</Tabs>
// Removed for simplicity
);
}
export default withStyles(styles)(TabHeader);
This is the Content Page. Here I've got the Months class which is the render object for my Tab:
class Months extends React.Component {
render() {
const { data } = this.props;
const months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
return (
<Tab
value={data+1}
label={months[new Date(0, 0).getMonth()+data]}
component={Link}
to={`/monthlyTable/${data+1}`}
/>
);
}
}
Then this is the main content that is render on the page:
class MonthlyTable extends React.Component {
state = {
monthlyTable: [],
isLoading: false,
}
componentDidMount() {
// Fetch content from API
}
componentDidUpdate(prevProps) {
if (prevProps.match.params.monthId != this.props.match.params.monthId && this.state.isLoading != true) {
// Re-fetch content from API
}
}
render() {
const month = new Date().getMonth();
const rows = [];
for (var i = month; i >= 0; i--) {
rows.push(<Months key={i} data={i} />);
}
return (
// Removed for simplicity
<TabHeader header={this.state.monthlyTable.Title}>
{rows}
</TabHeader>
{this.state.isLoading ? '' : this.state.monthlyTable.Id && this.state.monthlyTable.Id.length > 0 && <ListTable data={this.state.monthlyTable.Id} />}
{this.state.isLoading ? '' : this.state.monthlyTable.Dates && this.state.monthlyTable.Dates.length > 0 && <ListTable data={this.state.monthlyTable.Dates} />}
// Removed for simplicity
);
}
}
It would be nice to mark the current Tab for user experience, so that you know what content is being showed.
put condition on window.location
if(window.location.pathname === "") then set style or className equal whatever you want to set.
for navbar example:
<ul class="navbar-nav">
<li class={window.location.pathname === "Home" : "nav-item active" ? "nav-item">
<a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
</li>
</ul>
Related
I am trying to build a react page that shows a list of "messages subjects" received and when you click the down icon the messages relating to that subject appear directly below. (Imagine to help explain, when the user clicks the down icon on the line with 'Christmas' a white space needs to appear directly below and BEFORE the line with the 'New Year' text, so I can then display the message body, etc for each message relating to that subject.
Here is my code
import React from "react";
import "./Messages.css";
import { ReactComponent as DownIcon } from "../images/down-chevron.svg";
import Moment from 'moment';
class Messages extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
isLoading: false,
};
}
componentDidMount() {
this.setState({ isLoading: true });
const proxyurl = "https://cors-anywhere.herokuapp.com/";
const url =
"<my url>" +
this.props.location.state.userID;
fetch(proxyurl + url)
.then((res) => res.json())
.then((data) => this.setState({ data: data, isLoading: false }));
}
render() {
const { data, isLoading } = this.state;
if (isLoading) {
return <p>Loading ...</p>;
}
if (data.length === 0) {
return <p> no data found</p>;
}
return (
<div>
<div className="messageSubjectHeader">
<div className="innerMS">Message Subject</div>
<div className="innerMS">Number of linked messages</div>
<div className="innerMS">Latest message Date and Time</div>
<div className="innerMS">View Messages</div>
</div>
{data.message_Subjects.map((ms) => (
<div className="messageSubject">
<div className="innerMS">{ms.subject}</div>
<div className="innerMS">{ms.message_Chain.length}</div>
<div className="innerMS">{this.getLatestMessageDateTime(ms.message_Chain)}</div>
<div className="innerMS">
<DownIcon className="innerMSDownIcon" />
</div>
</div>
))}
</div>
);
}
getLatestMessageDateTime(messageChain){
const lastmessage = messageChain.length -1;
Moment.locale('en');
var dt = messageChain[lastmessage].dateTime;
return(Moment(dt).format('ddd DD MMM YYYY hh:mm:ss'))
}
}
export default Messages;
You have to define the selected record id in the state and Update the selected id on the click of view messages button. And also add a Content Panel inside the loop and toggle the visibility based on the selected recorded Id in the state.
{data.message_Subjects.map((ms) => (
<>
<div className="messageSubject">
<div className="innerMS">{ms.subject}</div>
<div className="innerMS">{ms.message_Chain.length}</div>
<div className="innerMS">{"12/08/2020"}</div>
<div className="innerMS">
<button onClick={() => this.handleClick(ms.id)}>
{this.state.selectedId === ms.id ? "hide" : "Show"}
</button>
</div>
</div>
// show/hide the content based on the selection --> Content Panel
{this.state.selectedId === ms.id && (
<div className="content">{ms.description}</div>
)}
</>
))}
I have created a sample Demo - https://codesandbox.io/s/vigorous-hertz-x89p8?file=/src/App.js
Let me know if your use case is different.
You should have an onClick() handler if you want something to happen when user clicks an element, and then define handleOnClick() elsewhere in the component.
And you have no sub-component for the messages of a particular dated group, so, you'll need to code that, too.
Sub-Messages Component
I see that you have not defined any sub-components for messages. I don't know how your API works, so, I'll be general in that regard, but you'll want a <DateMessages/> component. This should have a constructor and render like...
constructor(props) {
super(props);
this.state = {'messages':[]};
}
render() {
return (
this.state.map((message) => {
return message.date + ' ' message.text;
});
);
}
Then, you'll need to populate this. So, add it as a ref in your <Messages/> component, as a child of <div className="messageSubject">. Since it starts out with no messages, it should come out as blank when appended to each date group. That'll look like datekey = ms.subject; <DateMessages ref={(instance) => {this.$datekey = instance}} />.
onClick Handler
So, your onClick handler would look like: <div className="messageSubject" onClick={(e, ms.subject) => this.handleOnClick(e, ms.subject)}>. You might have a handleOnClick() like...
handleOnClick(e, ms.subject) {
var datekey = ms.subject;
this.$datekey.setState(this is where an array of messages for datekey would be stored);
}
Advantages
Why do it this way? By having the state accurately reflect the data that the user is seeing, you'll be taking advantage of all the speedups of using ReactJS.
I'm fairly new to React. I have built a component that adds an input field onClick. What I need to do is add functionality to a button underneath each new input that deletes that specific input. For example, if 3 inputs are created 1,2,3 and input 2 is deleted 1 and 3 remain.
My code contains a function named onRemoveChild() that has some commented out code of my initial attempt at solving the problem using closest(). The issue with this is that state isn't correctly updated so after an input is removed the field label numbers are out of sync.
Thanks in advance and let me know if more explanation is needed.
import React from 'react'
import {
Button,
TextField,
Typography,
} from '#material-ui/core'
class TextFieldAddNew extends React.Component {
state = {
numChildren: 0
}
render () {
const children = [];
for (var i = 0; i < this.state.numChildren; i += 1) {
children.push(<ChildComponent key={i} number={i+2} removeChild={this.onRemoveChild} />);
};
return (
<ParentComponent addChild={this.onAddChild} theCount={this.state.numChildren}>
{children}
</ParentComponent>
);
}
onAddChild = () => {
this.setState({
numChildren: this.state.numChildren + 1
});
}
onRemoveChild = () => {
document.getElementById('removeInput').closest('div').remove();
}
}
const ParentComponent = (props) => (
<div className="card calculator">
<div id="childInputs">
{props.children}
</div>
{
props.theCount >= 4 ? (
<div className="warning">
We recommend adding no more that 5 opt-in's
</div>
) : ''
}
<Button
className="addInput"
onClick={props.addChild}>
+ Add another option
</Button>
</div>
);
const ChildComponent = (props) => (
<>
<TextField
id={'opt-in-' + props.number}
label={'Opt-in ' + props.number}
name={'opt-in'}
variant="outlined"
size="small"
margin="normal"
color="secondary"
className="halfInput"
/>
<Typography id="removeInput" className="removeInput"
onClick={props.removeChild}>
- Remove option
</Typography>
</>
);
export default TextFieldAddNew;
You can pass the index as part of calling removeChild like below:
children.push(<ChildComponent key={i} number={i+2} removeChild={()=>{this.onRemoveChild(i)}}
Also instead of keeping the numChildren in the state, you should keep the children. This way it would be easy to remove and add nodes to it. Then you can easily do:
children.splice(i,1) and then update the state, this way auto render will update the dom for you.
I have a react component that allows a user to click a button in the header and add different input types.
export default class ElementContainer extends Component {
render() {
return (
<div className="append-header">
<Headline buttonCheck={this.props.buttonCheck} />
<SubHeadline buttonCheck={this.props.buttonCheck} />
<URLButton={this.props.buttonCheck} />
</div>
)
}
}
I'm trying to implement react-beautiful-dnd into the project so that those inputs can be repositioned like a list in Trello.
Looking at that library (and other similar libraries), they use the data as lists in order to perform the drag and drop function.
The items in my app are added to the view by conditional rendering:
export default class InputShow extends Component {
render() {
const { node } = this.props;
return (
<div className="editor-div" >
{
(node.type === 'buttonA') ?
<textarea
//omitted code
id={node.id}
onChange={this.props.inputContentHandler} />
:
(node.type === 'buttonB')
?
<URLButton
url={this.url}
id={node.id}
title={this.title}
/>
:
""
}
</div >
)
}
}
I've tried to map the items by creating a state for items (additions)
export default class InputShow extends Component {
constructor(props) {
super(props)
this.state = {
additions: []
}
}
render() {
const { node } = this.props;
return (
<div className="editor-div" >
{this.state.additions.map(addition => (
<div key={addition.id}>
{
(node.type === 'buttonA') ?
<textarea
//omitted code
id={node.id}
onChange={this.props.inputContentHandler} />
:
(node.type === 'buttonB')
?
<URLButton
url={this.url}
id={node.id}
title={this.title}
/>
:
""
}
</div>
))}
</div >
)
}
}
I didn't get any errors, however now when I click on the buttons, no data is being displayed in the view. I've done simple maps in the past with API and local data but I've never tried it with ternary statements. Thanks for any feedback on a solution to this problem.
What you can do is separate the view logic from the code and create a functional component. Pass the values from the main as below:
{this.state.additions.map(addition => (
return <CustomTemplate id={addition.id}
nodeId={node.id} changeEvent={this.props.inputContentHandler}
nodeType={node.Type} url={this.url} title={this.title}/>))}
Create CustomTemplate something like this
const CustomeTemplate =(props)=>(
use props to get the values in the templates
)
Currently, this will render a component below each of the list items when the img is clicked by keeping an array of shown components per index in local state. Eg. (state.showItems ==[true,false,false,true]).
I would like to restrict the values in this array to only one 'true' at a time so that the <SuggestStep /> component is rendered only once in the div under the button that was clicked. I'm not using CSS because the list can grow very large and don't want to render and hide a component for each one. Also considered using a radio button displayed as an image, but don't know if that would involve mixing forms with LI's and if that is bad. Feedback on the question of restricting the showItems array items to only one true at a time, and general patterns to approaching the component rendering problem I'm describing are welcome.
class CurrentSteps extends Component {
constructor(props) {
super(props)
this.state = {
toggleOnSuggestInput: false,
showItems: []
}
this.clickHandler = this.clickHandler.bind(this)
}
clickHandler(index){
let showItems = this.state.showItems.slice();
showItems[index] = !showItems[index]
this.setState({showItems})
this.setState(prevState => ({
toggleOnSuggestInput: !prevState.toggleOnSuggestInput
}))
}
render() {
let steps = this.props.currentGoalSteps.map((step, index) => {
return (
<div key={`divKey${index}`}>
<li key={index}>{step}</li>
<img key={`imageKey${index}`} onClick={this.clickHandler.bind(this,index)} alt="" src={plus}/>
{this.state.showItems[index] ? <SuggestStep /> : null}
</div>
)
});
return (
<div>
<ul> {steps} </ul>
</div>
)
}
Try making the following modifications to your code...
Change your this.state like so.
this.state = {
toggleOnSuggestInput: false,
activeIndex: null
};
Change your clickHandler to this.
clickHandler(event, index) {
this.setState({ activeIndex: index })
}
Change your map to like the one below. Notice the onClick prop change.
let steps = this.props.currentGoalSteps.map((step, index) => {
return (
<div key={`divKey${index}`}>
<li key={index}>
{step}
</li>
<img
key={`imageKey${index}`}
onClick={e => this.clickHandler(e, index)}
alt=""
src={plus}
/>
{this.state.activeIndex === index ? <SuggestStep /> : null}
</div>
);
});
I'm implementing some autocomplete in my app. When there is new text in an I get http data as a string[] in . When I enter D DIOE and DAKT is rendered as an item. If I enter DI then, the DIOE rerenders. This is exactly what I want to prevent, because it looks ugly if the item disappears and reappears for a blink.
How does one prevent the whole array rerendering in my context, if only some items change?
The render
...
<nav>
{similars && similars.map((val, index) => {
return <Similar key={`similar-${val}-link`} similar={val} className={index == this.state.currentSimilarIndex ? transactionStyle.active : ''} onClick={() => this.clickSimilar(val)} />
})}
</nav>
...
An item for render
class Similar extends React.PureComponent<Similar.Props, Similar.State> {
render(): JSX.Element {
const { similar, className, onClick } = this.props
return (
<a key={`similar-${similar}-link`} className={className} onClick={onClick()} tabIndex={1}>
{similar}
</a>
)
}
}