How to make an icon clickable so it renders data below - reactjs

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.

Related

Ckeditor disable auto inline won't disable inline from being selected on page load

I'm trying to develop a simple CMS for my page. I want it to where I can edit and delete a users reply on click of a button. I got the delete functionality done so I figured for the reply functionality I would use CKeditor. What I'm struggling with is not being able to disable the autoinline feature. I can still select my div on load of the page rather than clicking a button to enable the inline feature but I don't know what I am doing wrong?
I have tried setting the feature directly in my index.html file, a custom js script file and the config.js ckeditor file but none worked. I am using Ckeditor 4.
this is the snippit of code I'm trying to use to disable inline on my div element but it's not working and I don't know why, i currently have it placed in a custom.js script file and I'm calling it from my index.html file
CKEDITOR.disableAutoInline = true;
Here is my code for my replies page:
import React from 'react';
import CKEditor from 'react-ckeditor-component';
import ForumpageService from '../../services/forumService';
import appController from '../../controllers/appController';
class Forumreplies extends React.Component {
constructor(props){
super(props);
this.elementName = "editor_" + this.props.id;
this.editReply = this.editReply.bind(this);
this.state = {
reply: '',
errorMsg: '',
isLoggedin: false,
// Ck Editor State
reply: '',
}
}
async componentDidMount(){
const topicId = this.props.match.params.topicid
const postDetails = await ForumpageService.replyDetails({topicId: topicId})
this.setState({
postDetails: postDetails[0],
topicId: topicId
})
await this.postReplies();
}
// Edit the reply
async editReply(id, e){
//CKEDITOR.inline(this.elementName);
}
async postReplies(){
const repliesData = await ForumpageService.postreplyDetails({topicId: this.state.topicId})
await this.setState({repliesData});
}
render(){
const repliesData = currentReply.map((row, index) => {
return (
<div className="row" id="reply-messages" key={index}>
<div className="col-md-8" suppressContentEditableWarning contentEditable="true" id={this.elementName}>
<p dangerouslySetInnerHTML={{__html: row.reply_message }} />
</div>
<div className="col-md-2">
{this.state.isloggedinuserId == row.reply_user_id && this.state.isLoggedin == true ? <i className="far fa-edit" onClick={this.editReply.bind(this, row.reply_id)} title="Edit this reply?"></i> : null }
</div>
</div>
})
return (
<div className="container" id="forum-replies">
<div className="row">
{repliesData}
</div>
</div>
)
}
}
export default Forumreplies;
Instead of creating a div and calling CKEDITOR.inline, you should try to use the react-ckeditor-component as its own way (not as an inline editor).
You could do something like: if the button wasn't pressed, render a div with the text content, but after the button was pressed, render a <CKEditor /> component as in the documentation
There is no documented way to set the editor as inline in this package that you are using.
EDIT:
I can see you are not using the react-ckeditor-component features, but following what you've done so far, you could set the contentEditable="true" property of the div only when the button is pressed:
<div className="col-md-8" suppressContentEditableWarning contentEditable={this.state.isEditing} id={this.elementName}>
<p dangerouslySetInnerHTML={{__html: row.reply_message }} />
</div>
And then set the isEditing to true on the onClick handler. Your component will update and then re-render with the contentEditable property set to true

React content rendering based on map output not working

I have a component which is going to rely on the results of an array.map to determine it's output. In this case the array contains an element called "name", and one of the names is 'Welcome.' What needs to happen is the component spits out a particular div for the 'Welcome' instance and different content (an Accordion component) for every other instance. I've used a ternary operator in the render which I'm then calling in the return, but it's outputting the same text for every instance of the component (the text specified for only the 'Welcome' instance.) I can't for the life of me figure out what I'm doing wrong. Here's the code:
export default class Back extends React.Component {
constructor(props) {
super(props)
}
render() {
const CheckDomains = this.props.totaldomains.map((domain, index) => {
<div>
{
domain.name === 'Welcome' ?
<div>This is welcome text.</div>
:
<div>This is accordion text.</div>
}
</div>
)
});
return(
<div className='back'>
{CheckDomains}
</div>
);
}
}
You need to return something in your map callback function.
Like so:
const CheckDomains = this.props.totaldomains.map((domain, index) => {
return (
<div>
{
domain.name === 'Welcome' ?
<div>This is welcome text.</div>
:
<div>This is accordion text.</div>
}
</div>
)
});

Can't get button component value onClick

I'm sure this is something trivial but I can't seem to figure out how to access the value of my button when the user clicks the button. When the page loads my list of buttons renders correctly with the unique values. When I click one of the buttons the function fires, however, the value returns undefined. Can someone show me what I'm doing wrong here?
Path: TestPage.jsx
import MyList from '../../components/MyList';
export default class TestPage extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.handleButtonClick = this.handleButtonClick.bind(this);
}
handleButtonClick(event) {
event.preventDefault();
console.log("button click", event.target.value);
}
render() {
return (
<div>
{this.props.lists.map((list) => (
<div key={list._id}>
<MyList
listCollection={list}
handleButtonClick={this.handleButtonClick}
/>
</div>
))}
</div>
);
}
}
Path: MyListComponent
const MyList = (props) => (
<div>
<Button onClick={props.handleButtonClick} value={props.listCollection._id}>{props.listCollection.title}</Button>
</div>
);
event.target.value is for getting values of HTML elements (like the content of an input box), not getting a React component's props. If would be easier if you just passed that value straight in:
handleButtonClick(value) {
console.log(value);
}
<Button onClick={() => props.handleButtonClick(props.listCollection._id)}>
{props.listCollection.title}
</Button>
It seems that you are not using the default button but instead some sort of customized component from another libray named Button.. if its a customezied component it wont work the same as the internatls might contain a button to render but when you are referencing the event you are doing it throug the Button component

Render Component Only Once in a Div Below an LI When In-Line Button is Clicked

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>
);
});

Force stop container subscribtion (Meteor+React+CreateContainer)

How can I manually stop the subscription and clean the collection on the client when you switch route via react-router?
The fact is that if I go to the page where there is a component subscribed to, such as "top 10 news" page, where published all the news, I see, as a container of news component performing a normal search the collection first finds objects with subscription last page.
export default createContainer((props)=>{
let {limitCount, userId} = props;
let query = (userId)?{'userId':userId}:{};
var handle = Meteor.subscribe('news_preview', query,limitCount);
var posts = Post.find(query, {sort:{createdAt:-1}}).fetch(); //Here, the container finds documents to which another component has been signed.
var needWait = !!handle.ready()&&!!posts;
return {
needWait:!needWait,
posts:posts
}
}, PostList)
After a moment, the container will complete its membership and will give us the relevant objects ...
How can I check that when linking the previous container has stopped its subscription and deleted objects?
In details. With the logic description
PostListContainer
This is a sample of NewsList component, with infinity scroll and subscribtion container. Is just detect scroll, and pass limitCount to child component.
export default class PostListContainer extends Component{
constructor(props){
super(props)
this.state={
limitCount:10,
}
this.handleScroll = this.handleScroll.bind(this);
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll, false);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll, false);
}
handleScroll(event) {
let documentHeight = $(document).height(); //jquery is bad idea, i know
let windowHeight = $(window).height();
let scrollPosition = $(window).scrollTop();
let isLoading = this.refs.PostList.data.needWait;
if(scrollPosition+windowHeight>=documentHeight && !isLoading){
this.uppendSkipCount()
}
}
uppendSkipCount(){
let limitCount = this.state.limitCount;
this.setState({
limitCount:limitCount+5
});
}
render(){
let userId = (this.props.userId)?this.props.userId:undefined;
return(
<div>
<PostList ref="PostList" limitCount={this.state.limitCount} userId={userId} />
</div>
)
}
}
PostList
This component get properties, subscribe and render child components.
export class PostList extends Component {
constructor(props){
super(props);
}
postList(){
return(
<Masonry>
{this.props.posts.map((post)=>(
<div className="col-xs-12 col-sm-6 col-md-6 col-lg-4" key={post._id}>
<PostPreview post={post}/>
</div>
))}
</Masonry>
)
}
render() {
let content = (this.props.posts)?this.postList():undefined;
let loading = (this.props.needWait)?<Loading />:undefined;
return(
<div>
{content}
{loading}
</div>
)
}
}
export default createContainer((props)=>{
let {limitCount, userId} = props;
let query = (userId)?{'userId':userId}:{};
var handle = Meteor.subscribe('news_preview', query,limitCount);
var posts = Post.find(query, {sort:{createdAt:-1}}).fetch();
var needWait = !!handle.ready()&&!!posts;
return {
needWait:!needWait,
posts:posts
}
}, PostList)
And it work goood. But if i will come on the page from page that contain for example BestNews component i will get Posts object from that in first iteration:(
export class BestNews extends Component {
constructor(props){
super(props);
}
componentWillUnmount(){
this.props.handle.stop(); / looks like it help
}
goToPage(post){
browserHistory.push(`/news/item/${post._id}`);
}
bestPosts(){
return bestPosts = this.props.posts.map((post)=>{
return(
<div key={post._id}>
<ListItem
onTouchTap={()=>this.goToPage(post)}
leftAvatar={<Avatar src={post.author().getAvatar().link()} />}
rightIcon ={<ActionChromeReaderMode />}
primaryText={post.title}
secondaryText={post.description}
/>
<Divider />
</div>
)
});
}
render() {
let content = (this.props.needWait)?<Loading />:this.bestPosts();
return (
<div className="box-margin">
<Paper zDepth={1}>
<div className="row middle-xs center-xs">
<div className="col-xs-12">
<h2 style={{fontWeight:'300'}}>Интересные новости</h2>
</div>
</div>
<div className="row start-xs">
<div className="col-xs-12">
<List>
<Divider />
{content}
</List>
</div>
</div>
</Paper>
</div>
)
}
}
export default createContainer((props)=>{
var handle = Meteor.subscribe('best_news');
var posts = Post.find({}, {sort:{likes:-1}, limit:5}).fetch();
var needWait = !handle.ready() && !posts;
return {
handle:handle,
needWait:needWait,
posts:posts
}
}, BestNews)
I found a solution.
It was obvious. But somehow it seems to me that there is a better one.
In this case, we need to use a child component of the container. With method componentWillUnmount() we can manually stop the subscription.
What is negative? The fact that we need to transfer the subscription object in a child component, and then manually stop the subscription. And to do it in each component, which can cause a similar problem.
The answer to the question - why can not we just wait for the completion of the subscription of the container, and then - to display the content. The answer is - it is possible, but as long as you have not implemented infinite scrolling logic. Otherwise, each time the user scrolls down the content will be lost. As long as the subscription is completed.
It's funny, but I always thought that the container so stops the subscription. I was wrong?
Solution with manualy stop subscribtion is not good. You understand why.
Take a look at the pretty good solution.
We need to protect themselves from the fact that when you visit a component we can get irrelevant data with other components. Yes type have the same data, but - with the other components. It can play a cruel joke with s. I think you understand why.
At the same time we can not put a cap "loading" every time a user scrolls down and loaded data. Because at the time of subscription, all data will disappear. Option with named subscriptions you too are not satisfied. For example, if you are using Astronomy ORM by Jagi.
So. We need simply to intercept getting values ​​from the container and once finished to record the fact of the first subscription in the state of the component.
export class PostList extends Component {
constructor(props){
super(props);
this.state={
firstSubscribtion:false
}
}
componentWillReceiveProps(nextProps){
if(!this.state.firstSubscribtion && nextProps.handleReady){
this.setState({
firstSubscribtion:true
});
}
}
postList(){
return(
<Masonry>
{this.props.posts.map((post)=>(
<div className="col-xs-12 col-sm-6 col-md-6 col-lg-4" key={post._id}>
<PostPreview post={post}/>
</div>
))}
</Masonry>
)
}
render() {
let content = (this.props.posts && this.state.firstSubscribtion)?this.postList():undefined;
let loading = (this.props.needWait)?<Loading />:undefined;
return(
<div>
{content}
{loading}
</div>
)
}
}
export default createContainer((props)=>{
let {limitCount, userId} = props;
let query = (userId)?{'userId':userId}:{};
var handle = Meteor.subscribe('news_preview', query,limitCount);
var posts = Post.find(query, {sort:{createdAt:-1}}).fetch();
var needWait = !!handle.ready()&&!!posts;
return {
handleReady:!!handle.ready(),
needWait:!needWait,
posts:posts
}
}, PostList)

Resources