How can I pause and resume an observable on document click? - javascript

I have an observable that emits certain values to the console on every second. I show an example below and a StackBlitz as well.
My dilemma is I need to able to pause it on the next document click and resume it (not restart it) it on the next one again. I was looking into switchMap and I couldn't figure out how to implement it.
export class AppComponent implements OnInit {
title = "my-app";
pageClickObservable: Observable<Event> = fromEvent(document, "click");
ngOnInit(): void {
new Observable();
const watchForClick = () => {
setTimeout(() => {
console.log("A");
}, 1000);
setTimeout(() => {
console.log("BB");
console.log("10");
}, 2000);
setTimeout(() => {
console.log("CCC");
}, 3000);
setTimeout(() => {
console.log("DDDD");
console.log("20");
}, 4000);
setTimeout(() => {
console.log("EEEEE");
}, 5000);
setTimeout(() => {
console.log("FFFFFF");
console.log("30");
}, 6000);
};
this.pageClickObservable.pipe().subscribe(() => {
watchForClick();
});
}
}

I think you want something like this:
const $myContinuousObs = ...... // This is your observable that keeps emitting
const $isOn = new BehaviourSubject(true); // BehaviorSubject to store the current on/off state
combineLatest([$myContinuousObs, $isOn]).pipe( //combine the two
filter(res => res[1]), // only continue when $isOn is true
map(res => res[0]) // return only the myContinuousObs and not the $isOn
).subscribe(console.log) // log the value
To change $onOff:
$isOn.next(true)
$isOn.next(false)
To toggle $onOff
$isOn.pipe(take(1)).subscribe(val => $onOff.next(!val))

you need to turn your page click into some stream that can track the state of the switch using scan like so:
const clickStream$ = fromEvent(document, 'click').pipe(
scan((isOn, click) => !isOn, true), // on every click, toggle the switch
startWith(true) // initially emit true
)
then you can just switchMap:
clickStream$.pipe(
switchMap(isOn => isOn ? obs$ : EMPTY)
).subscribe(v => console.log(v, 'got value'))
where obs$ is whatever you want to start and stop listening to. now this method with switchMap will resubscribe on each clickStream$ emission, which depending on the nature of the observable of interest, may not be what you're after, but the concept of the clickStream$ observable may still be useful to you combined with the other answer here using filter

Check this out:
import { Component, OnInit } from "#angular/core";
import { fromEvent, Observable } from "rxjs";
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnInit {
title = "my-app";
pageClickObservable: Observable<Event> = fromEvent(document, "click");
pause = false; // creating a variable to track the pause
ngOnInit(): void {
new Observable();
this.pageClickObservable.pipe().subscribe(() => {
this.pause = !this.pause; // change the pause status - true / false at alternative clicks
});
// mimicking observable sending data every second
setInterval(() => {
if (!this.pause) {
// if not pause then execute the complete functions
console.log("Click");
} else {
console.log("Do nothing");
}
}, 1000);
}
}
This will simply console "Click" and "Do nothing" based on clicks.
For example, you have an observable sending data at every second, let's say, setInterval
If they share a common variable, such as this.pause in this case, then you can pause and resume easily.

What I've done is to create an operator that delays the value based on a numerical property value.
delayByProperty.ts
import {of, NEVER} from 'rxjs';
import {concatMap, delay} from 'rxjsoperators';
export function delayByProperty(propertyNameOfSecondsDelay:string) {
return concatMap(obj => {
const seconds:any = obj[propertyNameOfSecondsDelay];
if (!seconds) {
return of(obj);
} else if (!Number.isFinite(seconds)) {
throw new TypeError(`object property "${propertyNameOfSecondsDelay}" expected to be falsy or a number, was ${typeof(secondsDelay)} with value of ${secondsDelay}.`);
} else if (seconds === -1) {
return NEVER;
} else if (seconds > 0) {
return of(obj).pipe(
delay(seconds * 1000)
)
}
});
}
Now you can do
from([1,2,3,2,5,2,1]).pipe(
map(n => {value:n, seconds:Number.parseFloat(n)}),
delayByProperty('seconds')
)

When dealing with Observables, I've found it useful to think less like a programmer and more like a plumber. :-) Consider this:
function getObservableOfMsgs(click$) {
//Array of objects that have whatever values you want in them, plus a property of "seconds" that contains the number of seconds you want to delay.
const objs = [{msg:'1', seconds:1},{msg:'2', seconds:2},{msg:'TWO', seconds:2}];
// observable we want per click.
const perClick$ = from(objs).pipe(
// map each object into an observable that delays, then emits the msg... and combine all of those observables to happen simultaneously.
mergeMap(({msg, seconds}) => of(msg).pipe(delay(seconds * 1000)))
);
return click$.pipe(
mapTo(perClick$)
);
}
and now you'll have an observable that emits a pattern of messages per time you click.
So if you have a function called doSomethingWithMessage that takes a msg as a single argument and returned either null or a message you wanted to log as a debug message, you could do...
const unsubscribeFunctionInCaseINeedIt = getObservableOfMsgs(click$).pipe(
tap(doSomethingWithMessage)
).subscribe(
(d) => if (d !== null) console.debug(d),
(e) => console.error(e),
() => console.log('Complete!')
)
and then, until you call the returned function, it will all just "happen" as events stream through.
from and of are part of rxjs, so you should be able to import {from, of} from 'rxjs' Hopefully this automatically resolves because rxjs is part of Angular. Likewise, you might also have to import {mergeMap, delay, mapTo, tap} from 'rxjs/operators' or something, but I'm unable to access StackBlitz to confirm for you.

Related

Wait x seconds for new emitted value before triggering a function

I have a child component that emits a value, and in the parent I perform an axios call with this value each time it is emitted. My problem is that I want to trigger the axios call only if in x ms (or seconds) the child has not emmited another value in order to reduce the amount of calls I do.
Code here :
<script>
import axios from "axios";
import DataTable from './DataTable.vue';
export default {
name: 'Test',
data() {
return {
gamertags: [],
// Utils
timeout: 500,
delay: 500
}
},
methods: {
// API calls
async getGamerTags(string='') {
const path = `http://localhost:5000/gamertags?string=${string}`
await axios.get(path)
.then((res) => {
this.gamertags = res.data;
})
.catch((error) => {
console.error(error);
});
},
// DataTable
handleFilters(filters) {
clearTimeout(this.timeout);
this.timeout = setTimeout(this.getGamerTags(filters.find(o => o.field == "playerGamerTag").filter), this.delay);
}
}
components: {
DataTable
}
};
</script>
<template>
<DataTable
#filters="handleFilters"
/>
</template>
Thanks in advance.
What you need is debouncing. Here is an example:
var timeout, delay = 3000;
function func1() {
clearTimeout(timeout);
timeout = setTimeout(function(){
alert("3000 ms inactivity");
}, delay);
}
<input type="text" oninput="func1()">
When emitted, simply call func1(), and if there are no new emissions after 3000 ms, the function in timeout will be executed.
It would be better to understand the problem and use case if you add the code also.
but As I could understand the problem these is two way
if you using inside input and triggering based #changed event you can add #change.lazy this not trigger on each change.
second solution is to use setTimeout(function,delayInMs) inside parent
vuejs Docs link
By simply changing the handleFilters function to :
handleFilters(filters) {
clearTimeout(this.timeout);
this.timeout = setTimeout(
this.getGamerTags,
this.delay,
filters.find(o => o.field == "playerGamerTag").filter
);
},
the problem is solved.

setState causing infinite loop in custom hook

I've created a custom hook within my React app, but for some reason when I update the internal state via an event listener, it causes an infinite loop to be triggered (when it shouldn't). Here's my code:
// Note that this isn't a React component - just a regular JavaScript class.
class Player{
static #audio = new Audio();
static #listenersStarted = false;
static #listenerCallbacks = {
playing: [],
paused: [],
loaded: []
};
static mount(){
const loaded = () => {
this.removeListenerCallback("loaded", loaded);
};
this.addListenerCallback("loaded", loaded);
}
// This method is called on the initialization of the React
// app and is only called once. It's only purpose is to ensure
// that all of the listeners and their callbacks get fired.
static startListeners(){
const eventShorthands = {
playing: "play playing",
paused: "pause ended",
loaded: "loadedmetadata"
};
Object.keys(eventShorthands).forEach(key => {
const actualEvents = eventShorthands[key];
actualEvents.split(" ").forEach(actualEvent => {
this.#audio.addEventListener(actualEvent, e => {
const callbacks = this.#listenerCallbacks[key];
callbacks.forEach(callback => {
callback(e)
});
});
});
});
}
static addListenerCallback(event, callback){
const callbacks = this.#listenerCallbacks;
if(callbacks.hasOwnProperty(event)){
// Remember this console log
console.log(true);
this.#listenerCallbacks[event].push(callback);
}
}
static removeListenerCallback(event, callback){
const listenerCallbacks = this.#listenerCallbacks;
if(listenerCallbacks.hasOwnProperty(event)){
const index = listenerCallbacks[event].indexOf(callback);
this.#listenerCallbacks[event].splice(index, 1);
}
}
}
const usePlayer = (slug) => {
// State setup
const [state, setState] = useReducer(
(state, newState) => ({ ...state, ...newState }), {
mounted: false,
animationRunning: false,
allowNextFrame: false
}
);
const _handleLoadedMetadata = () => {
// If I remove this _stopAnimation, the console log mentioned
// in the player class only logs true to the console 5 times.
// Whereas if I keep it, it will log true infinitely.
_stopAnimation();
};
const _stopAnimation = () => {
setState({
allowNextFrame: false,
animationRunning: false
});
}
useEffect(() => {
Player.addListenerCallback("loaded", _handleLoadedMetadata);
return () => {
Player.removeListenerCallback("loaded", _handleLoadedMetadata);
};
}, []);
return {
mounted: state.mounted
};
};
This makes me think that the component keeps on re-rendering and calling Player.addListenerCallback(), but the strange thing is, if I put a console.log(true) within the useEffect() at the end, it'll only output it twice.
All help is appreciated, cheers.
When you're hooking (pun unintended) up inner functions in React components (or hooks) to external event handlers, you'll want to be mindful of the fact that the inner function's identity changes on every render unless you use useCallback() (which is a specialization of useMemo) to guide React to keep a reference to it between renders.
Here's a small simplification/refactoring of your code that seems to work with no infinite loops.
instead of a class with only static members, Player is a regular class of which there is an app-wide singletonesque instance.
instead of hooking up separate event listeners for each event, the often-overlooked handleEvent protocol for addEventListener is used
the hook event listener callback is now properly useCallbacked.
the hook event listener callback is responsible for looking at the event.type field to figure out what's happening.
the useEffect now properly has the ref to the callback it registers/unregisters, so if the identity of the callback does change, it gets properly re-registered.
I wasn't sure what the state in your hook was used for, so it's not here (but I'd recommend three separate state atoms instead of (ab)using useDispatch for an object state if possible).
The same code is here in a Codesandbox (with a base64-encoded example mp3 that I didn't care to add here for brevity).
const SMALL_MP3 = "https://...";
class Player {
#audio = new Audio();
#eventListeners = [];
constructor() {
["play", "playing", "pause", "ended", "loadedmetadata", "canplay"].forEach((event) => {
this.#audio.addEventListener(event, this);
});
}
play(src) {
if (!this.#audio.parentNode) {
document.body.appendChild(this.#audio);
}
this.#audio.src = src;
}
handleEvent = (event) => {
this.#eventListeners.forEach((listener) => listener(event));
};
addListenerCallback(callback) {
this.#eventListeners.push(callback);
}
removeListenerCallback(callback) {
this.#eventListeners = this.#eventListeners.filter((c) => c !== callback);
}
}
const player = new Player();
const usePlayer = (slug) => {
const eventHandler = React.useCallback(
(event) => {
console.log("slug:", slug, "event:", event.type);
},
[slug],
);
React.useEffect(() => {
player.addListenerCallback(eventHandler);
return () => player.removeListenerCallback(eventHandler);
}, [eventHandler]);
};
export default function App() {
usePlayer("floop");
const handlePlay = React.useCallback(() => {
player.play(SMALL_MP3);
}, []);
return (
<div className="App">
<button onClick={handlePlay}>Set player source</button>
</div>
);
}
The output, when one clicks on the button, is
slug: floop event: loadedmetadata
slug: floop event: canplay

How to handle only one observer and dont call others?

I have the following usage of rxjs streams:
ngOnInit() {
combineLatest(
this.eventsService.subjectSearchDistribution.pipe(
tap((querySearch) => {
this.paginationService.setQuery(querySearch);
this.paginationService.reset();
}),
),
this.eventsService.subjectSortingDistribution.pipe(
tap((sortedList: ListItem[]) => {
this.paginationService.setSortBy(getSortingString(sortedList));
}),
),
this.eventsService.subjectFilterDistribution.pipe(
tap((filterUrl) => {
const page = 1;
this.paginationService.setFilterBy(filterUrl);
this.paginationService.setCurrentPage(page);
this.paginationService.calculateOffsetLimit(page);
}),
),
this.eventsService.subjectFilterDistributionReset.pipe(tap(() => this.paginationService.reset())),
).subscribe(() => {
this.loadPage();
});
}
Problem is I need to handle only one case, onle one stream and dont call others, as result call this.loadPage();.
Now when I send message to this.eventsService.subjectSearchDistribution, this.eventsService.subjectSortingDistribution, this.eventsService.subjectFilterDistribution.
I see that calling of this.loadPage(); increases from fist time +1 each event.
SO, ONLY one observer can be active, not all torgether.
How to fix it?
It seems the reason your loadPage method is called twice due to your event listeners, but without sharing the code for those methods I cannot confirm that issue. The simplest way to fix your double call of the loadPage method would be this:
class A {
constructor() {
this.pageLoadCalled = false;
this.loadPage();
this.events.filter.listen().subscribe((res) => this.loadPage());
this.events.search.listen().subscribe((res) => this.loadPage());
}
loadPage() {
if (this.pageLoadCalled) {
// Exit early (will not call anything below the return)
return;
}
// Mark this method as being called
this.pageLoadCalled = true;
return new Promise((resolve) => {
// do stuff
resolve();
});
}
}
If you want to call loadPage only once, don't execute it when events.filter and events.search trigger:
class A {
constructor() {
// Call pageLoad in the constructor only once
this.loadPage();
// Remove call to pageLoad when events fire.
// this.events.filter.listen().subscribe((res) => this.loadPage());
// this.events.search.listen().subscribe((res) => this.loadPage());
}
}
I solved this using rxjs:
ngOnInit() {
combineLatest(
this.eventsService.subjectSearchDistribution.pipe(
tap((querySearch) => {
this.paginationService.setQuery(querySearch);
this.paginationService.reset();
}),
),
this.eventsService.subjectSortingDistribution.pipe(
tap((sortedList: ListItem[]) => {
this.paginationService.setSortBy(getSortingString(sortedList));
}),
),
this.eventsService.subjectFilterDistribution.pipe(
tap((filterUrl) => {
const page = 1;
this.paginationService.setFilterBy(filterUrl);
this.paginationService.setCurrentPage(page);
this.paginationService.calculateOffsetLimit(page);
}),
),
this.eventsService.subjectFilterDistributionReset.pipe(tap(() => this.paginationService.reset())),
)
.pipe(takeUntil(this._onDestroy))
.subscribe(() => {
this.loadPage();
});
}

How to start iterableDiffer only after OnInit?

My problem is that the iterableDiffer detect a change when I only what to inialize the data from the database. I dont want to get this first change detection. I only want to get a change detection when the user edits the array.
I tryed to put the database request in the constructor. Didnt change anything.
constructor(
private route: ActivatedRoute,
private StoreService: StoreService,
private iterableDiffers: IterableDiffers
) {
this.iterableDiffer =
this.iterableDiffers.find([]).create(null);
}
myArray: MyClass[] = [];
iterableDiffer: IterableDiffer<unknown>;
ngOnInit() {
const id = this.route.snapshot.params.id;
this.StoreService.getData(id)
.subscribe(data => this.myArray = data);
// on this subscribe, the Differ falsely gets triggered
}
ngAfterViewChecked() {
const changes = this.iterableDiffer.diff(this.myArray);
if (changes) {
changes.forEachAddedItem((record: IterableChangeRecord<MyClass>) => {
console.log('Added Changes detected');
this.StoreService.addToDatabase(record.item);
});
}
}
// gets called by User Click, here i want to have the Differ called
// This works fine
addElmt(elmt: MyClass) {
this.myArray.push(elmt);
}
The code snippet is a simplified version of the real code. I can not call the StoreService.addToDatabase() in the function addElmt(). This is just for better explanation.
Thank you for your help!
Always can has a variable 'myObserver' and do
myObserver:any=null
ngAfterViewChecked() {
const changes = this.iterableDiffer.diff(this.myArray);
if (changes) {
changes.forEachAddedItem((record: IterableChangeRecord<MyClass>) => {
console.log('Added Changes detected');
this.StoreService.addToDatabase(record.item);
});
}
if (!myObserver)
{
const id = this.route.snapshot.params.id;
myObserver=this.StoreService.getData(id)
.subscribe(data => this.myArray = data);
}
}

How do I use axios response in different components without using export?

As the tittle says, I would like to be able to use the same axios response for differents components.
I have some restrictions like, I'm onlyl able to use react by adding scripts tags to my html so things like exports or jsx are impossible for me.
This is my react code:
class User extends React.Component {
state = {
user: {}
}
componentWillMount() {
console.log(localStorage.getItem("user"))
axios.get('http://localhost:8080/dashboard?user=' + localStorage.getItem("user"))
.then(res => {
const userResponse = res.data
setTimeout(() =>
this.setState({user: userResponse.user}), 1000);
})
}
render () {
const {user} = this.state
if (user.fullName === undefined)
return React.createElement("div", null, 'loading..');
return React.createElement("span", {className: "mr-2 d-none d-lg-inline text-gray-600 small" }, user.fullName);
}
}
ReactDOM.render( React.createElement(User, {}, null), document.getElementById('userDropdown') );
class Roles extends React.Component{
state = {
user: {}
}
componentWillMount() {
console.log(localStorage.getItem("user"))
axios.get('http://localhost:8080/dashboard?user=' + localStorage.getItem("user"))
.then(res => {
const userResponse = res.data
setTimeout(() =>
this.setState({user: userResponse.user}), 1000);
})
}
render () {
const {user} = this.state
const roles = user.user.roles.map((rol) => rol.roleName)
if (user.fullName === undefined)
return React.createElement("div", null, 'loading..');
return React.createElement("a", {className: "dropdown-item" }, user.fullName);
}
}
ReactDOM.render( React.createElement(Roles, {}, null), document.getElementById('dropdownRol') );
I would like to be able to manage different components(rendering each one) with data of the same axios response.
Is this possible considering my limitations?
Thanks in advance
Here's a working example of how you might do it. I've tried to annotate everything with comments, but I'm happy to try to clarify if you have questions.
// Fake response object for the store's "load" request
const fakeResponse = {
user: {
fullName: "Carolina Ponce",
roles: [
{ roleName: "administrator" },
{ roleName: "editor" },
{ roleName: "moderator" },
{ roleName: "generally awesome person" }
]
}
};
// this class is responsible for loading the data
// and making it available to other components.
// we'll create a singleton for this example, but
// it might make sense to have more than one instance
// for other use cases.
class UserStore {
constructor() {
// kick off the data load upon instantiation
this.load();
}
// statically available singleton instance.
// not accessed outside the UserStore class itself
static instance = new this();
// UserStore.connect creates a higher-order component
// that provides a 'store' prop and automatically updates
// the connected component when the store changes. in this
// example the only change occurs when the data loads, but
// it could be extended for other uses.
static connect = function(Component) {
// get the UserStore instance to pass as a prop
const store = this.instance;
// return a new higher-order component that wraps the connected one.
return class Connected extends React.Component {
// when the store changes just force a re-render of the component
onStoreChange = () => this.forceUpdate();
// listen for store changes on mount
componentWillMount = () => store.listen(this.onStoreChange);
// stop listening for store changes when we unmount
componentWillUnmount = () => store.unlisten(this.onStoreChange);
render() {
// render the connected component with an additional 'store' prop
return React.createElement(Component, { store });
}
};
};
// The following listen, unlisten, and onChange methods would
// normally be achieved by having UserStore extend EventEmitter
// instead of re-inventing it, but I wasn't sure whether EventEmitter
// would be available to you given your build restrictions.
// Adds a listener function to be invoked when the store changes.
// Called by componentWillMount for connected components so they
// get updated when data loads, etc.
// The store just keeps a simple array of listener functions. This
// method creates the array if it doesn't already exist, and
// adds the new function (fn) to the array.
listen = fn => (this.listeners = [...(this.listeners || []), fn]);
// Remove a listener; the inverse of listen.
// Invoked by componentWillUnmount to disconnect from the store and
// stop receiving change notifications. We don't want to attempt to
// update unmounted components.
unlisten = fn => {
// get this.listeners
const { listeners = [] } = this;
// delete the specified function from the array.
// array.splice modifies the original array so we don't
// need to reassign it to this.listeners or anything.
listeners.splice(listeners.indexOf(fn), 1);
};
// Invoke all the listener functions when the store changes.
// (onChange is invoked by the load method below)
onChange = () => (this.listeners || []).forEach(fn => fn());
// do whatever data loading you need to do here, then
// invoke this.onChange to update connected components.
async load() {
// the loading and loaded fields aren't used by the connected
// components in this example. just including them as food
// for thought. components could rely on these explicit fields
// for store status instead of pivoting on the presence of the
// data.user object, which is what the User and Role components
// are doing (below) in this example.
this.loaded = false;
this.loading = true;
try {
// faking the data request. wait two seconds and return our
// hard-coded data from above.
// (Replace this with your network fetch.)
this.data = await new Promise(fulfill =>
setTimeout(() => fulfill(fakeResponse), 2000)
);
// update the loading/loaded status fields
this.loaded = true;
this.loading = false;
// call onChange to trigger component updates.
this.onChange();
} catch (e) {
// If something blows up during the network request,
// make the error available to connected components
// as store.error so they can display an error message
// or a retry button or whatever.
this.error = e;
}
}
}
// With all the loading logic in the store, we can
// use a much simpler function component to render
// the user's name.
// (This component gets connected to the store in the
// React.createElement call below.)
function User({ store }) {
const { data: { user } = {} } = store || {};
return React.createElement(
"span",
{ className: "mr-2 d-none d-lg-inline text-gray-600 small" },
user ? user.fullName : "loading (User)…"
);
}
ReactDOM.render(
// Connect the User component to the store via UserStore.connect(User)
React.createElement(UserStore.connect(User), {}, null),
document.getElementById("userDropdown")
);
// Again, with all the data loading in the store, we can
// use a much simpler functional component to render the
// roles. (You may still need a class if you need it to do
// other stuff, but this is all we need for this example.)
function Roles({ store }) {
// get the info from the store prop
const { data: { user } = {}, loaded, loading, error } = store || {};
// handle store errors
if (error) {
return React.createElement("div", null, "oh noes!");
}
// store not loaded yet?
if (!loaded || loading) {
return React.createElement("div", null, "loading (Roles)…");
}
// if we made it this far, we have user data. do your thing.
const roles = user.roles.map(rol => rol.roleName);
return React.createElement(
"a",
{ className: "dropdown-item" },
roles.join(", ")
);
}
ReactDOM.render(
// connect the Roles component to the store like before
React.createElement(UserStore.connect(Roles), {}, null),
document.getElementById("dropdownRol")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="userDropdown"></div>
<div id="dropdownRol"></div>

Categories