How to restart auto-dismissing <b-alert>? - javascript

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.

Related

Why is the value on display not in sync with value logged to console?

Can anyone tell me why at button click, the value outputted to the console is always one unit smaller than displayed on the screen?
The values are not in sync as expected.
Example below in React
In Child:
import React, {useState } from "react";
export const ChildComp = ({getNumProps}) => {
const [num, setNum] = useState(0);
const onPlusClick = () => {
if (num< 12) {
setNum(num + 1);// num does not increase immediately after this line, except when focus reenters here on second method call
}
getNumProps(num);
}
return(
<div>
<button onClick={onPlusClick}>
Click to increment
</button>
{num}
</div>
);
}
In parent
import { ChildComp } from "./ChildComp"
export const ParentComp = () => {
const getNum= (num) => {
console.log(num);
}
return (<ChildComp getNumProps={getNum}/>)
}
The page initially shows 0
When I click once the number increments to 1, but console displays 0
When I click once the number increments to 2, but console displays 1
I should see in the console the same as the page display
Appreciate if you can leave a commen on how the question can be improved.
This is a child to parent communication example. Also, any objections about standards used, please let me know.
Thanks.
Update: I notice the values would be in sync if
instead of getNumProps(num);
I did getNumProps(num + 1); but that doesn't change the fact that previously on this line
setNum(num + 1);, as already pointed out in the comment, num does not increase immediately after this line, except when focus reenters here on second method call. Not sure why.
The prop function getNumProps is a side effect, and should be put into a hook, instead of inside of that onPlusClick function.
Instead, do this:
useEffect(() => {
getNumProp(num);
}, [num]);
Alternatively, to avoid the error: "React Hook useEffect has a missing dependency: 'getNumProps'. See this doc on using the useCallback hook
const callback = useCallback(() => {
getNumProp(num);
}, [num]);
function onPlusClick(...) {
...
callback();
}
The change to the state of num will cause a re-render of the child component, not the parent.

VueJS component does not update data

I know there is many similar questions about data not being updated in VueJS component but I still could not find the answer. I have the following component:
<template>
<div class="mt-5">
[...]
<div v-for="dataSource in dataSources" v-bind:key="dataSource.id">
{{ dataSource}}
</div>
[...]
</div>
</template>
<script>
import { chain, find, merge, toUpper } from "lodash";
import Mixins from "../utils/Mixins.vue";
import Pagination from "../utils/Pagination.vue";
import DataSourceTable from "./DataSourceTable.vue";
export default {
mixins: [Mixins],
components: {
"data-source-table": DataSourceTable,
"data-source-pagination": Pagination
},
data: function() {
return {
dataSources: [],
//[...]
};
},
methods: {
getAllDataSources(page) {
//[...]
},
search() {
//[...]
},
setSortAttribute(attribute) {
//[...]
},
updateDataSource(updatedDataSource){
for (let i = 0; i < this.dataSources.length; i++) {
if (this.dataSources[i].id == updatedDataSource.id) {
this.dataSources[i] = updatedDataSource;
break; // Stop this loop, we found it!
}
}
}
},
created: function() {
this.getAllDataSources(this.currentPage);
// Capture updated data source via websocket listener
this.$options.sockets.onmessage = function(data) {
let message = JSON.parse(data.data);
if (message.id == "dataSource" && message.type == "data") {
let updatedDataSource = message.payload.data.listen.relatedNode;
this.updateDataSource(updatedDataSource);
}
}
}
};
</script>
In the created hook, I capture changes coming from a websocket and I update the corresponding item in the array dataSources. I can see the item is properly updated in Vue Dev Tools but it is still not updated in the component template. Example below:
This is a common mistake with vuejs.
You can check the document here: Why isn’t the DOM updating?
In Your case, you can use push or $set to achieve your purpose.
When you modify an Array by directly setting an index (e.g. arr[0] = val) or modifying its length property. Similarly, Vue.js cannot pickup these changes. Always modify arrays by using an Array instance method, or replacing it entirely. Vue provides a convenience method arr.$set(index, value) which is syntax sugar for arr.splice(index, 1, value).
Example:
Replace this.dataSources[i] = updatedDataSource;
By this.dataSources.splice(i, 1, updatedDataSource);
When you modify an Array by directly setting an index (e.g. arr[0] = val) or modifying its length property. Similarly, Vue.js cannot pickup these changes. Always modify arrays by using an Array instance method, or replacing it entirely. Vue provides a convenience method arr.$set(index, value) which is syntax sugar for arr.splice(index, 1, value).
updateDataSource(updatedDataSource){
for (let i = 0; i < this.dataSources.length; i++) {
if (this.dataSources[i].id == updatedDataSource.id) {
//this.dataSources[i] = updatedDataSource;
this.dataSources.$set(i, updatedDataSource);
break; // Stop this loop, we found it!
}
}
}

React componentWillReceiveProps not updating state

I've got this React parent component here. The children components at this point are just returning dropdown menus. I expected that componentWillReceiveProps would update the state here, which in turn should be passed to StopList as props. However, when state.selectedSub is changed through handleSubSelect, nothing happens and StopList doesn't receive any props.
Is my mistake with the asynchronous nature of componentWillReceiveProps? Is it in the wrong place in my code? Am I using the wrong lifecycle method?
// We're controlling all of our state here and using children
// components only to return lists and handle AJAX calls.
import React, { Component } from 'react';
import SubList from './SubList';
import StopList from './StopList';
class SubCheck extends Component {
constructor (props) {
super(props);
this.state = {
selectedSub: '--',
selectedStop: null,
stops: ['--'],
};
this.handleSubSelect.bind(this);
this.handleStopSelect.bind(this);
}
// We want the user to be able to select their specific subway
// stop, so obviously a different array of stops needs to be
// loaded for each subway. We're getting those from utils/stops.json.
componentWillReceiveProps(nextProps) {
var stopData = require('../utils/stops');
var stopsArray = [];
var newSub = nextProps.selectedSub
for(var i = 0; i < stopData.length; i++) {
var stop = stopData[i];
if (stop.stop_id.charAt(0) === this.state.selectedSub) {
stopsArray.push(stop.stop_name);
}
}
if (stopsArray.length !== 0 && newSub !== this.state.selectedSub) {
this.setState({stops: stopsArray});
}
}
handleSubSelect(event) {
this.setState({selectedSub:event.target.selectedSub});
}
handleStopSelect(event) {
this.setState({selectedStop:event.target.selectedStop})
}
render() {
return (
<div>
<SubList onSubSelect={this.handleSubSelect.bind(this)}/>
<StopList stops={this.state.stops} onStopSelect={this.handleStopSelect.bind(this)}/>
</div>
);
}
}
export default SubCheck;
You are duplicating data, and causing yourself headaches that aren't necessary.
Both selectedSub and selectedStop are being stored as props and as state attributes. You need to decide where this data lives and put it in a singular location.
The problem you are encountering entirely revolves round the fact that you are changing the state attribute and expecting this to trigger a change to your props. Just because they share a name does not mean they are the same value.

Flux/Alt setTimeout not updating store

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

this.myService.myEvent.toRx().subscribe() called but no DOM refresh (Zone trigger)

I'm playing with angular2 alpha 40 with ng2-play starter from pawel.
Examples are in typescript.
I have a service MovieList like this:
export class Movie {
selected: boolean = false
constructor(public name:string, public year:number, public score:number) {}
}
export class MovieListService {
list: Array<Movie>
selectMovie = new EventEmitter();
constructor() {
this.list = [new Movie('Star Wars', 1977, 4.4)];
}
add(m:Movie) {
this.list.push(m);
}
remove(m:Movie) {
for(var i = this.list.length - 1; i >= 0; i--) {
if(this.list[i] === m) {
if(m.selected) this.selectMovie.next();
this.list.splice(i, 1);
}
}
}
select(m:Movie) {
this.list.map((m) => m.selected = false);
m.selected = true;
this.selectMovie.next(m);
}
}
I have a component showing the movies list and make possible to select one by clicking on it, which call select() in the service above.
And I have another component (on the same level, I don't want to use (selectmovie)="select($event)") which subscribe to the movie selection event like this:
#Component({
selector: 'movie-edit',
})
#View({
directives: [NgIf],
template: `
<div class="bloc">
<p *ng-if="currentMovie == null">No movie selected</p>
<p *ng-if="currentMovie != null">Movie edition in progress !</p>
</div>
`
})
export class MovieEditComponent {
currentMovie:Movie
constructor(public movieList: MovieListService) {
this.movieList.selectMovie.toRx().subscribe(this.movieChanged);
setTimeout(() => { this.movieChanged('foo'); }, 4000);
}
movieChanged(f:Movie = null) {
this.currentMovie = f;
console.log(this.currentMovie);
}
}
The event is subscribed using .toRx().subscribe() on the eventEmitter.
movieChanged() is called but nothing happen in the template..
I tried using a timeout() calling the same function and changes are refleted in the template.
The problem seems to be the fact that subscribe expects an Observer or three functions that work as an observer while you are passing a normal function. So in your code I just changed movieChanged to be an Observer instead of a callback function.
movieChanged: Observer = Observer.create(
(f) => { this.currentMovie = f; }, // onNext
(err) => {}, // onError
() => {} // onCompleted
);
See this plnkr for an example. It would have been nice to see a minimal working example of your requirement so my solution would be closer to what you are looking for. But if I understood correctly this should work for you. Instead of a select I just used a button to trigger the change.
Update
You can avoid creating the Òbserver just by passing a function to the subscriber method (clearly there's a difference between passing directly a function and using a class method, don't know really why is different)
this.movieList.selectMovie.toRx().subscribe((m: Movie = null) => {
this.currentMovie = m;
});
Note
EventEmitter is being refactored, so in future releases next will be renamed to emit.
Note 2
Angular2 moved to #reactivex/rxjs but in the plnkr I'm not able to use directly those libs (didn't find any cdn). But you can try in your own project using these libs.
I hope it helps.
The movieChanged function expects the movie object and not the String. Try changing below code
setTimeout(() => { this.movieChanged('foo'); }, 4000);
to
setTimeout(() => { this.movieChanged(new Movie('Troy', 2000 , 8)); }, 4000);

Categories