ReactJS: how to hide element based on parent's props? - javascript

I have a component that is showing message box. It's called from the parent element something like that:
<AlertMessage status={this.state.status} text={this.state.text} />
I want to change it to take one more parameter hide and it would be hiding itself in 5 seconds:
<AlertMessage status={this.state.status} text={this.state.text} hide={true} />
The problem this alert message is based on props, so its render function looks like:
render(){
const statusName = this.props.status || 'info';
const alertStyle = 'alert-box ' + alertClasses[statusName] + (this.props.text ? '' : ' d-none');
return (
<div className={alertStyle}>
<span>{this.props.text}</span>
</div>
);
}
So, I see 2 ways to implement hiding after 5 seconds:
A parent element does it just setting up the text to ""
The alert component hides itself.
I don't want to involve the parent component here but I don't know how I would implement it inside alert component. Add more state like "hidden" to the alert component but how I would handle it after?
Update. I think I found the solution. I use static function getDerivedStateFromProps and the component's visibility is based on its state hide, not on the text:
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.text != nextProps.message && nextProps.message){
return {
text: nextProps.message,
hide: false
};
} else {
return {};
}
}
render(){
const statusName = this.props.status || 'info';
const alertStyle = this.state.hide ? ' d-none' : 'alert-box ' + alertClasses[statusName];
if (!this.state.hide){
setTimeout(() => this.setState({hide: true}), 5000);
}
}
There is still bug: when I show several messages in a row the total timeout will start on the first, not on the last, but it's okay for now.

It will be difficult to handle it properly without involving the parent element. If you clear the text prop on parent, the AlertMessage component will still be mounted, but will be no visible due to no content inside. The same situation if you would drop the logic inside AlertMessage - you can't "unmount" the component directly from inside it.
The disadvantage is that AlertMessage remains mounted, you may have problems with applying animations to it. Also it may take the space in your app (depends on css), so users can accidently click on it or may cause problems with clicking elements placed under it.
What I would suggest - use your hide prop. Inside that function, where you set hide to false and the alert appears - use setTimeout, so the hide prop goes true after n seconds.
const someFn = () => {
this.setState({ hide: false }); // alert appears
setTimeout(() => {
this.setState({ hide: true });
}, 5000); // alert disappears (gets unmounted)
};
Then inside your render:
{!hide && <AlertMessage status={this.state.status} text={this.state.text} />}

constructor(props) {
super(props);
this.state = {
hide: false
};
componentDidMount() {
this.timer = setTimout(() => this.setState({ hide: true }), 5000)
}
componentWillUnmount() {
this.timer && clearTimeout(this.timer);
render(){
const statusName = this.props.status || 'info';
const alertStyle = 'alert-box ' + alertClasses[statusName] + (this.props.text ? '' : ' d-none');
return (
<div className={alertStyle}>
<span>{this.state.hide ? "" : this.props.text}</span>
</div>
);
}
Just get a state variable hide. setTimeout will turn it to false after 5 seconds. Don't forget to clear it in componentWillUnmount.

Related

Can't access to a 'this' value in a function outside of constructor

I'm trying to learn how to make an app like reactjs but not really using it. I'm following a tutorial but I have some challenges. I have a function called 'update' which fires when there is a change in the state. I have a 'menu' object that I import as a child. The thing is, I can't seem to access to this child object in the 'update' function. Please have a look at the following:
import onChange from 'on-change';
import Menu from './menu';
class App {
constructor(){
const state = {
showMenu: false
}
this.state = onChange(state, this.update);
this.el = document.createElement('div');
this.el.className = 'todo';
// create an instance of the Menu
this.menu = new Menu(this.state);
// create a button to show or hide the menu
this.toggle = document.createElement('button');
this.toggle.innerText = 'show or hide the menu';
this.el.appendChild(this.menu.el);
this.el.appendChild(this.toggle);
// change the showMenu property of our state object when clicked
this.toggle.addEventListener('click', () => { this.state.showMenu = !this.state.showMenu; })
}
update(path, current, previous) {
if(path === 'showMenu') {
> // show or hide menu depending on state
> console.log (app.menu); // undefined
> this.menu[current ? 'show' : 'hide'](); // not working cause 'this.menu' is undefined
}
}
}
const app = new App();
> console.log (app.menu); // here it console logs correctly the object
document.body.appendChild(app.el);
Can someone help me to figure out what is going on here? thank you!
In the update() method in your App class, you have to use this.menu instead of app.menu.
It should look like this:
update(path, current, previous) {
if(path === 'showMenu') {
console.log (this.menu);
this.menu[current ? 'show' : 'hide']();
}
}
You can't use app within the App class because app is not defined there. You have to use the this keyword to access the members in the class itself.
Hope this helps.

How to add displays without re-rendering the whole component in VueJS

I'm just new to Laravel and Vuejs. And I have this problem wherein the whole component is re-rendering when the "Load More" button is clicked or when scrolled down to the bottom. The button or scroll is just acting like a pagination, but all you can do is to load more or add more displays. My problem is how can i render the new displays without re-rendering the whole component.
I tried creating a variable wherein it will pass how many paginate will be displayed. Yes it does the work but the component is re-rendering and the size of the reply from the server gets larger and larger.
here's my script on my Vue component:
<script>
export default {
props: ['user','review_count'],
data(){
return{
reviews: {},
limit: 2,
scrolledToBottom: false,
}
},
created(){
this.getReviews();
this.scroll();
},
methods: {
getReviews: function(page){
axios.get('/datas/reviews?user='+ this.user + '&limit='+ this.limit)
.then((response) =>{
this.reviews = response.data.data;
})
.catch(()=>{
});
},
countType: function(data, type) {
return data.filter(function(value) { return value.type === type }).length;
},
loadMore: function(){
this.limit+=6;
this.getReviews();
},
scroll () {
window.onscroll = () => {
let bottomOfWindow = Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop) + window.innerHeight === document.documentElement.offsetHeight
if (bottomOfWindow&&this.review_count>this.limit) {
this.loadMore();
}
}
}
}
}
</script>
here's my controller:
public function reviews()
{
if($users = \Request::get('user')){
if($limit = \Request::get('limit')){
$reviews = Review::select(\DB::raw('id, product_id, review_rating, review_content'))
->where('user_id', $users)
->with('products:product_image,id,product_name')
->with('activities')
->orderBy('reviews.created_at', 'DESC')
->paginate($limit);}}
return $reviews;
}
I just solved my own Question, the solution is using pagination. I just pushed all of the data from the server in an object every time it scrolled down.
here's my code for scroll down:
scroll () {
window.onscroll = () => {
let bottomOfWindow = Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop) + window.innerHeight === document.documentElement.offsetHeight
if (bottomOfWindow&&this.review_count>this.reviews.length) {
this.getReviews();
}
}
}
here's my codes for getReviews():
getReviews: function(){
let vm = this;
axios.get('/datas/reviews?user='+ this.user + '&page='+ this.page)
.then((response) =>{
$.each(response.data.data, function(key, value) {
vm.reviews.push(value);
console.log((vm.reviews));
});
})
.catch(()=>{
});
this.page+=1;
},
I've come up with this idea so that I will not use pagination anymore to view the next posts. It's more like a infinite scroll pagination component.
In my opinion, there is two things you need to do.
1/ in stead of increasing the limit eveytime, you should look into paging your results on the server, so you can ask the next page from your server. Now, on each consecutive call you are fetching what you already had again, which will eliminate your goal of making this less stressful on your server
2/ in your client code obviously you need to support the paging as well, but also make sure you properly set your key on the looped elements. VueJS used the key to determine whether it should rerender that particular element in the loop.
Let me know if this helps!

PrimeNg TabView with ConfirmDialog

I'm trying to use PrimeNg TabView component along with confirmDialog unsuccessfully, here is my code:
<p-tabView (onChange)="onTabChange($event)" [(activeIndex)]="index">...</p-tabView>
onTabChange(event){
this.confirmationService.confirm({
message: 'Do you confirm ?',
accept: () => {
this.index = event.index;
},
reject:() =>{ }
});
}
Do you have an idea on how to prevent or allow tab change using confirm dialog ?
Thanks
Based on similar solution for material design tabs, here is the solution for my issue:
in html Declare a local variable referencing TabView DOM object:
<p-tabView #onglets>...</p-tabView>
in component.ts, change default function called when click on tab with specific
function to match your case:
#ViewChild('onglets') onglets: TabView;
this.onglets.open = this.interceptOngletChange.bind(this);
...
interceptOngletChange(event: Event, tab: TabPanel){
const result = confirm(Do you really want to leave the tab?);
return result && TabView.prototype.open.apply(this.onglets, argumentsList);
});
}
I had similar problem. Needed show dialog before tab change.
My solution:
HTML
<p-tabView #tabView (onChange)="onChange($event)" />
TS
#ViewChild('tabView') tabView: TabView;
onChange(event: any) {
const previoustab = this.tabView.tabs[this.prevIndex]; //saved previous/initial index
previoustab.selected = true;
const selectedTab = this.tabView.tabs[event.index];
selectedTab.selected = false;
this.tabView.activeIndex = this.prevIndex;
this.nextIndex= event.index;
}
GoToNextTab() {
this.tabView.activeIndex = this.nextIndex;
this.prevIndex= this.nextIndex;
this.tabView.open(undefined, this.tabView.tabs[this.nextIndex]);
}
With this code you will stay on the selected tab without tab style changes.

Set a single setTimeout in React

I'm building a multiplication testing app, and I want each question to have an 8-second countdown that resets when an answer is ended. My problem is that when my component re-renders another instance of the setTimeout is created, meaning I get a two-second countdown, then 3 second etc.
What would be the correct way to handle this?
My component:
class Question extends Component {
constructor(props){
super(props);
this.state = {
answer: "",
correct: false,
timer: 8
}
this.countDownTrack = null;
this.checkAnswer = this.checkAnswer.bind(this);
}
countDown() {
let number = this.state.timer;
if (number > 0 && this.state.correct === false){
number--
this.setState({
timer: number
})
} else {
this.props.sendAnswer(this.state.answer, this.props.randIndex);
this.setState({
timer: 8
})
}
}
//what we want is for there to be a single setTimeout
CountDownHandle = () => {
this.countDownTrack = setTimeout(()=>{
this.countDown();
},1000)
}
checkAnswer = (e) => {
e.preventDefault();
this.props.sendAnswer(this.state.answer, this.props.randIndex);
this.setState({
answer: "",
timer: 8
})
}
handleChange = (event) => {
this.setState({
answer: event.target.value
})
}
render(){
this.CountDownHandle();
let countShow;
countShow = this.state.timer;
return (
<div className="Question">
<h1>What is</h1>
<h1><span id="table1">{parseInt(this.props.no1)}</span> X <span id="table2">{parseInt(this.props.no2)}</span>?</h1>
<form action="" onSubmit={this.checkAnswer}>
<input autoComplete="off" type="text" name="answer" onChange={this.handleChange} value={this.state.answer}/>
</form>
<p>{countShow}</p>
</div>
);
}
}
export default Question;
you need to clear timeout re-rendering the component using this function:
clearTimeout(this.trackingTimer);
It maybe a part of else code block in countDown().
Remember to clear the timeout when the component is unmount via ComponentWillUnmount.
That will make sure there is no unwanted timer running on the background.
2 issues with your code structure:
If you want to show a visible countdown (eg. 8,7,6,...)
use "setInterval"
rather than "setTimeout"
Place the timer event trigger function outside of the render function, like in the componentDidMount, a button click, input focus, or a response to a singular action.
Also, to control the timeout to be singular, check if an instance exists 1st.
if(!this.countDownTrack)
this.countDownTrack = setInterval(
()=>{
this.countDown();
if(this.state.timer < 1)
clearInterval(this.trackingTimer);
},1000)

Trigger onClick event for a ReactJS element

I have a list of elements that are loaded with reactjs and at the end of that list there is a button that loads more items via onclick event using reactjs.
I want to create a function that using javascript or jquery, trigger the onclick event to load all the items instead of clicking one by one on the load more items.
I tried to do it using a interval in jquery but the $element.trigger('click') is not working, does nothing.
Can anyone help me with this? please.
ReactJS:
var ConversationShowMore = React.createClass({
getInitialState: function(){
return {show: false, next_comments: ""};
},
loadMoreComments: function(){
this.setState({show: true});
},
render: function(){
var obj = this.props.next_comments || "";
if (obj != "" && requesturl != obj) {
if (this.state.show) {
return (
<ConversationBox url={this.props.next_comments} />
)
}else{
return (
<a onClick={this.loadMoreComments} className="showmoreconversations" href="#" role="button"><span>Load more conversations...</span></a>
)
}
}else{
return (
<div></div>
)
}
}
});
Javascript/jQuery:
var tid = setInterval(myCode, 5000);
function myCode() {
if($("#conversationContainer a.showmoreconversations").length){
$("#conversationContainer a.showmoreconversations").trigger('click');
}else{
abortTimer();
}
}
function abortTimer() {
clearInterval(tid);
}
When component is mounted, you will trigger request to load more comments. When this request is complete, you schedule another request in X miliseconds.
loadMoreComments(){
console.log('loaded more comments');
// Probably, you will trigger async request. Call following line when this request is complete.
this.timeout = window.setTimeout(this.loadMoreComments, 5000);
},
componentDidMount() {
this.loadMoreComments();
},
Remember to cancel scheduled request when unmounting component. Otherwise, it will run virtually forever (and will surely case exception to be thrown)
componentWillUnmount() {
window.clearTimeout(this.timeout);
},
Working example over here: https://jsfiddle.net/69z2wepo/34030/

Categories

Resources