I'm trying to create a basic "Toast" like service in my React app using Alt.
I've got most of the logic working, I can add new items to the array which appear on my view when triggering the add(options) action, however I'm trying to also allow a timeout to be sent and remove a toast item after it's up:
onAdd(options) {
this.toasts.push(options);
const key = this.toasts.length - 1;
if (options.timeout) {
options.timeout = window.setTimeout(() => {
this.toasts.splice(key, 1);
}, options.timeout);
}
}
On add, the toast appears on my page, and the timeout also gets triggered (say after a couple of seconds), however manipulating this.toasts inside of this setTimeout does not seem to have any effect.
Obviously this is missing the core functionality, but everything works apart from the setTimeout section.
It seems that the timeout is setting the state internally and is not broadcasting a change event. It might be as simple as calling forceUpdate(). But the pattern I use is to call setState() which is what I think you might want in this case.
Here is an example updating state and broadcasting the change event.
import alt from '../alt'
import React from 'react/addons'
import ToastActions from '../actions/ToastActions'
class ToastStore {
constructor() {
this.toasts = [];
this.bindAction(ToastActions.ADD, this.add);
this.bindAction(ToastActions.REMOVE, this.remove);
}
add(options) {
this.toasts.push(options);
this.setState({toasts: this.toasts});
if (options.timeout) {
// queue the removal of this options
ToastActions.remove.defer(options);
}
}
remove(options) {
const removeOptions = () => {
const toasts = this.toasts.filter(t => t !== options);
this.setState({toasts: toasts});
};
if (options.timeout) {
setTimeout(removeOptions, options.timeout);
} else {
removeOptions();
}
}
}
module.exports = alt.createStore(ToastStore, 'ToastStore');
Related
I have created a vuejs2 component that I want to do the following:
Receive an "event" using $on and display a <b-alert> for 6 seconds.
When the same "event" message is received restart the b-alert timeout
Display multiple <b-alert> if there are different messages received
This is what I've tried:
<template>
<div>
<b-alert
v-for="message in bannerMessages"
:key="message.messageId"
:show="message.secondsLeft"
:variant="message.level"
dismissible
fade
#dismissed="bannerMessageDismissed(message)"
#dismiss-count-down="bannerCountDown(message)"
>
{{ message.message }}
</b-alert>
</div>
</template>
<script>
import EventBus from "#/eventBus"
export default {
name: "EventBusNotifications",
data() {
return {
bannerMessages: []
};
},
methods: {
showMessage(message) {
for (var i = 0; i < this.bannerMessages.length; i++) {
if (this.bannerMessages[i].message === message.message) {
this.bannerMessages[i].secondsLeft = 6;
return;
}
}
this.bannerMessages.push({
...message,
messageId: Date.now() + `-${message.message}`,
secondsLeft: 6,
});
},
bannerMessageDismissed(message) {
const index = this.bannerMessages.indexOf(message);
if (index !== -1) {
this.bannerMessages.splice(index, 1);
}
},
bannerCountDown(message) {
const index = this.bannerMessages.indexOf(message);
console.log(index);
if (index !== -1) {
this.bannerMessages[index].secondsLeft -= 1;
if (this.bannerMessages[index].secondsLeft === 0) {
this.bannerMessages.splice(index, 1);
}
}
},
},
mounted() {
EventBus.$on("notification", this.showMessage);
},
destroyed() {
EventBus.$off("notification", this.showMessage);
},
};
</script>
And here is the eventBus file:
import Vue from "vue";
const EventBus = new Vue();
export default EventBus;
THen, to use it, in any vue.js component or page, import EventBus from "#/eventBus" and then send an $emit like this:
EventBus.$emit("notification", { level: "warning", message: "This is the message", secondsLeft: 5});
The idea here is to reset the number of seconds on the <b-alert> if a matching message came in, but instead the message only appears very briefly (less than half a second). The bannerCountDown() method gets called rapidly instead of every second.
What would be the best approach to making a "restart-able" <b-alert>?
The problem is v-model/show implementation in <b-alert> was not designed for your use-case. The dismiss-count-down is emitted on any component update.
In your case, this creates a loop which brings the counter down immediately, hiding the alert (because you're mutating the objects in the dismiss-count-down, thus triggering another emit). The alerts are only visible because they have fade. Without it, they'd be removed on $nextTick, so they'd probably not be visible for the human eye.
The proper way to circumvent this issue would be to keep counters outside of the component and not rely on the component's counter at all, using some form of debounce. You'll need this solution if you want to display the counter values.
If you only want to reset the counter, without actually displaying its value in real time, here's a working demo, reusing most of your existing logic. I moved the timers in an external object to avoiding additional updates on <b-alert>s.
The key to why the above works is I'm not replacing the timers object when I'm changing a timer. if I replaced it (e.g: in tick i'd do:
this.timers = { ...this.timers, [message]: this.timers[message] - 1 }
it would behave exactly like yours.
I'm making a wrapper component so I need to add all the events in mounted() methods. However the thing is, as it's another component, whenever I open that component, event is triggered. I'm not sure how to block it. Even I made it to be triggered when the component is clicked, but it didn't work. It only works for the first mount. After re-open it(from second mount), it just keep triggers all the event and I have to block it.
Is there a way that I can block to not to trigger events in mounted() hook for vuejs?
EDITED:
I'm making leaflet-draw wrapper. all the events are from leaflet-draw doc.
this.addnew() is the one being triggered.
objectLayer.on("layeradd", (e) => {
let layer = e.layer;
layer.on("click", onClickFeatureSelct, layer);
if (typeof layer.options.id === "undefined") {
layer.options.id = L.Util.stamp(layer);
}
if (!layer.feature) {
let json = layer.toGeoJSON();
layer.feature = L.GeoJSON.asFeature(json);
}
let properties = layer.feature.properties;
let keyvalue = L.stamp(layer);
if (layer instanceof L.NodeCircle) {
let latlng = layer.getLatLng();
itemType = "node";
let nodes = this.$store.getters.nodeList;
let result = false;
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].keyvalue == keyvalue) {
result = true;
} else {
result = false;
}
}
if (!result) {
console.log('layer added')
// this.addNew(latlng, itemType, keyvalue);
}
if (!properties.NODE_ID) {
properties.NODE_ID = parseInt(this.newNodeId);
properties.NODE_NAME = "-";
this.addedNodes.push(properties.NODE_ID);
layer.bindTooltip(properties.NODE_NAME + "<br>(" + properties.NODE_ID.toString() + ")");
nodeObj[keyvalue.toString()] = layer;
}
// console.log('added nodes', this.addedNodes)
if (!nodeLayer.hasLayer(layer)) nodeLayer.addLayer(layer);
}
});
Well, As this question got 5 ups, to people who's facing same issue just like me. Here is How I did...
Vue.js mount order when components are related.
Child Component -> Parent Component
Adding this.$nextTick() didn't work.
Even it's a SPA Web application. There is no way to NOT to trigger events when they're in the child component. So I just made it to reload..... I know it's not a good idea to do it but I couldn't find the any other way to fix it. However, I think adding flags to parent component and trigger that event when parent is ready might gonna work.
I will re-try this logic once again and let you know how I've done afterwards. It won't be that soon. Sorry.
In my web application, I want to prompt user when he/she tries to close the browser/tab based upon Redux state using event handlers.
I am using the below code to prompt user before exiting based upon 'isLeaving' state.
function mapStateToProps(state) {
const {isLeaving} = state.app.getIn(['abc']);
return {
isLeaving
};
}
#connect(mapStateToProps, {}, undefined, {withRef: true})
export default class MyClass extends React.component {
#autobind
stayOnPage(event) {
if (this.props.isLeaving) {
const message = 'Are you sure you want to leave';
event.returnValue = message;
return message;
}
return false;
}
componentDidMount() {
window.addEventListener('beforeunload', (event) => {
this.stayOnPage(event);
});
}
componentWillUnmount() {
window.removeEventListener('beforeunload', (event) => {
this.stayOnPage(event);
});
}
componentWillReceiveProps(nextProps) {
if (this.props.prop1 !== nextProps.prop2) {
// do something
}
}
render() {
//
}
}
This code works fine. But whenever there is a change in prop1, I see that this.props.isLeaving does not have updated value.
Can somebody help? What is I'm doing wrong here?
You aren't cleaning up correctly in componentWillUnmount. The event handler you're trying to remove is a brand new function closure, not the same instance that you added. You should just attach the actual handler, rather than using an arrow function, like so:
componentDidMount() {
window.addEventListener('beforeunload', this.stayOnPage);
}
componentWillUnmount() {
window.removeEventListener('beforeunload', this.stayOnPage);
}
Possibly what you are seeing is the event triggering on a stale component instance, so it has old state.
React uses synthetic events, basically events are recycled to be more performant. You can read more on that here
What I normally do is pass the value I need on invocation
Not sure if this will work in your specific case because I define my event handlers in JSX instead of accessing the window object (which you might want to do as well but to be honest I'm not sure) but I use this pattern all the time to handle e.target.value properly
Pretty new to React and ES6 conventions. I am trying to call an action from within a function that is inside of a componentWillMount(). This is resulting in an Uncaught TypeError: Cannot read property 'signoutUser' of undefined. Not quite sure how to resolve it, tried binding this, which did resolve the problem.
This my code in its current form:
// left_site.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { signoutUser } from '../actions/authentication';
export default function(ComposedComponent) {
class LeftSite extends Component {
constructor(props, context) {
super(props, context);
}
componentWillMount() {
var timerLeft = setInterval(timer, 1000);
function timer() {
if ((sessionStorage.getItem('timer') > 0) &&
(new Date().getTime() - sessionStorage.getItem('timer') > 5000)) {
this.props.signoutUser();
} else {
sessionStorage.setItem('timer', new Date().getTime());
}
}
}
render() {
return <ComposedComponent {...this.props} />
}
}
return connect( null, { signoutUser })(LeftSite);
}
To explain what is going on, the company wants the user to automatically be logged out if they navigate away from any of the protected routes on the domain to another domain. One idea was to create a "heartbeat" that committed the time to sessionStorage every second so long as the user is on a protected route on the domain. The idea is if the navigate to another domain, then try to come back, if the difference in the last stored time and the current time is greater than 5000ms, it will automatically signout.
There may be a better way to do this, but I couldn't think of one that 1) didn't violate privacy or 2) wouldn't trigger the logout with a refresh, unbind for example.
The left_site.js is a HOC--I also have a required_login.js HOC to reroute to the login page if someone tries to access the protected route without authentication--so my protected routes are wrapped in component={LeftSite(RequireAuth(Home))}.
LeftSite is running fine, however when the conditional evaluates to true and it tries to trigger the this.props.signoutUser(); the error comes up.
Function timer is not bound to class. When it is executed at regular interval, the execution context changes. You have to bind the function before use. Also make sure you clear the interval at proper time, or when the component unmounts. I suggest you write this way
timer = () => {
if ((sessionStorage.getItem('timer') > 0) &&
(new Date().getTime() - sessionStorage.getItem('timer') > 5000)) {
this.props.signoutUser();
} else {
sessionStorage.setItem('timer', new Date().getTime());
}
}
componentWillMount() {
this.timerLeft = setInterval(this.timer, 1000)
}
componentWillUnmount() {
clearInterval(this.timerLeft);
}
You need to bind this to the timer function. The easier and recommended way to do this is by defining an arrow function for your timer, like this:
export default class MyComponent extends React.Component {
componentWillMount() {
const timer = () => {
console.log(this.props); // should be defined
};
const timerLeft = setInterval(timer, 1000);
}
}
This works because with arrow functions, this is set to the this of the enclosing context.
I want to avoid typing the same lines of code. Currently, I have an app that is supposed to make an API call, like so.
render: function(){
var processappkey = localStorage.getItem('yourid');
var weather = new XMLHttpRequest();
var deesfault = "Houston, Texas";
weather.open("GET", "http://apidatafromsomewebsiteq="+deesfault+"&units=imperial&appid="+processappkey, false);
weather.send(null);
var r = JSON.parse(weather.response);
var check = r.main.temp;
var theunicorn = r.weather[0].icon;
return (<p>{theunicorn}</p>)
}
I would like to split this up to something like this:
somecontainer: function(){
var processappkey = localStorage.getItem('yourid');
var weather = new XMLHttpRequest();
var deesfault = "Houston, Texas";
weather.open("GET", "http://apidatafromsomewebsiteq="+deesfault+"&units=imperial&appid="+processappkey, false);
weather.send(null);
var r = JSON.parse(weather.response);
var check = r.main.temp;
var theunicorn = r.weather[0].icon;
},
render: function() {
{this.somecontainer()}
return (
<p>{theunicorn}</p>
)
}
I will be calling the API from different areas in my app. Not to mention including a setInverval, which will have me repeating the code again.
As a matter of fact, while I am at it I would also like to know how to go about something like this.
render: function() {
this.somecontainer();
setInterval(function() {
this.somecontainer();
}, 5000);
}
However, that is a different question, and I'll be happy for insight on the first issue.
Good question, pretty easy answer. Just have a function that goes and gets the data you want, and returns the result via a callback function. This utility function would sit in another file somewhere and you can import it and call it from any component. Then, take the data that the function returns and put it in your components state.
You should almost certainly not be calling an API in the render method. React can run the render() method a lot depending on your app. If you want it to fire when the component first loads, use componentDidMount (this will only fire on the client, handy if you're using server-side rendering).
let counter = 0;
// separate utility
function goGetAUnicorn(callback) {
// replicate async for demonstration...
setTimeout(() => {
callback(`I am unicorn picture #${counter++}`);
}, 100)
}
class Unicorn extends React.Component {
constructor(props) {
super(props);
this.state = {
unicornPicture: '',
};
}
componentDidMount() {
// runs once, client side only
goGetAUnicorn(unicornPicture => {
this.setState({unicornPicture});
});
// to simulate reusing the same function elsewhere at some other time
setInterval(() => {
goGetAUnicorn(unicornPicture => {
this.setState({unicornPicture});
});
}, 1000)
}
render() {
return (
<div>Here is your unicorn: {this.state.unicornPicture}</div>
);
}
}
ReactDOM.render(<Unicorn />, document.getElementById('app'));
I'm using setTimeout just to indicate that you must wait for the response before carrying on. I'd actually use a promise, not a callback, but they both work.
Here's a jsbin to play with: https://jsbin.com/lohojo/1/edit?html,js,output