Here is my code:
app.component.ts
notifier$ = new BehaviorSubject<any>({});
notify() {
this.notifier$.next({});
}
app.component.html
<div (scroll)="notify()"></div>
<child-component [inp]="notifier$ | async" />
The problem is that when a user is scrolling, the notify() function is called repeatedly and I only want to call notify() once each time the user starts to scroll.
I can accomplish what I want this way:
scrolling = false;
scrollingTimer: NodeJS.Timer;
notify() {
clearTimeout(this.scrollingTimer);
if (!this.scrolling) {
this.notifier$.next({});
}
this.scrolling = true;
this.scrollingTimer = setTimeout(() => (this.scrolling = false), 1000);
}
but I would like to do this with rxjs. However debounceTime is the opposite of what I want, and neither throttleTime nor auditTime are what I want either. Is there a way to do this?
you could build an observable like so:
const scroll$ = fromEvent(document, 'scroll');
const scrollEnd$ = scroll$.pipe(
switchMapTo(
timer(1000) // on every scroll, restart a timer
)
);
const scrollStart$ = scrollEnd$.pipe( // the scroll end event triggers switch to scroll$
startWith(0), // but start it off
switchMapTo(
scroll$.pipe( // then just take the first scroll$ event
first()
)
)
);
scrollStart$.subscribe(v => console.log('scroll start'));
you could generalize it to an operator:
function firstTimeout(timeout: number) { // welcoming notes on a better name
return input$ => {
const inputTimeout$ = input$.pipe(
switchMapTo(timer(timeout))
);
return inputTimeout$.pipe(
startWith(0),
switchMapTo(input$.pipe(first()))
);
};
}
and use it like:
notifier$.pipe(firstTimeout(1000)).subscribe(v => console.log('took one'));
a good idea for this case might be to wrap it in a directive for easy reuse:
#Directive({
selector: '[scrollStart]'
})
export class ScrollStartDirective {
private scrollSource = new Subject();
#HostListener('scroll', ['$event'])
private onScroll(event) {
this.scrollSource.next(event);
}
#Output()
scrollStart = new EventEmitter();
constructor() {
this.scrollSource.pipe(firstTimeout(1000)).subscribe(this.scrollStart);
}
}
then you can use it like this:
<div (scrollStart)="notify()"></div>
When the user scrolls you want notify$ to emit for each scroll event. This provides a constant stream of emitted values. So you want notifier$ to emit once when the stream starts, and again when it's idle for 1 second.
notify$ = new Subject();
notifier$ = merge(
notify$.pipe(first()),
notify$.pipe(switchMap(value => of(value).pipe(delay(1000))))
).pipe(
take(2),
repeat()
);
<div (scroll)="notify$.next()"></div>
You merge two observables. The first emits immediately, and the second emits after a 1 second delay. You use a switchMap so that the delayed observable is always restarted.
We take the next 2 values which triggers the stream to complete, and we use repeat to start over.
you can use take(1)
this.inp.pipe(
take(1),
).subscribe(res => console.log(res));
take(1) just takes the first value and completes. No further logic is involved.
Of course you are supposed to use the above in your child component :)
Also since your observable is finishing...you can create a new subject and pass it to child component every time he scrolls
notify() {
this.notifier$ = new BehaviorSubject<any>({});
this.notifier$.next({});
}
Related
I am working with something like fullpage.js with React, and I need to remove the eventListener while the transition is ongoing.
Is it possible?
React code
function App() {
const wheelHandler = (event) => {
// I need to remove wheelHandler here
setTimeout(() => {
// I need to readd wheelHandler here
}, 1000); // Assume that the transition ends after 1000ms
};
return (
<div className="App" onWheel={wheelHandler} />
);
}
Vanilla JS equivalent
const wheelHandler = (event) => {
window.removeEventListener(wheelHandler);
setTimeout(() => {
window.addEventListener(wheelHandler);
}, 1000);
};
window.addEventListener(wheelHandler);
P.S. I tried the Vanilla JS solution on React but the event handler got triggered multiple times on one wheel scroll. Therefore I got no choice but React's SyntheticEvent.
With the way you're hooking it up, you can't without using a piece of state that tells you whether to hook up the handler and re-rendering, which is probably overkill.
Instead, I'd set a flag (perhaps on an object via a ref) telling the handler to ignore calls during the time you want calls ignored.
Something long these lines:
function App() {
const {current: scrolling} = useRef({flag: false});
const wheelHandler = (event) => {
// I need to remove wheelHandler here
if (scrolling.flag) {
// Bail out
return;
}
scrolling.flag = true;
// ...other logic if any...
setTimeout(() => {
// I need to readd wheelHandler here
scrolling.flag = false;
}, 1000); // Assume that the transition ends after 1000ms
};
return (
<div className="App" onWheel={wheelHandler} />
);
}
Or you can also do it like this, you don't need an extra object (I tend to prefer to use a single ref that holds all of my non-state instance data, but you don't have to):
function App() {
const scrolling = useRef(false);
const wheelHandler = (event) => {
// I need to remove wheelHandler here
if (scrolling.current) {
// Bail out
return;
}
scrolling.current = true;
// ...other logic if any...
setTimeout(() => {
// I need to readd wheelHandler here
scrolling.current = false;
}, 1000); // Assume that the transition ends after 1000ms
};
return (
<div className="App" onWheel={wheelHandler} />
);
}
As they say in the useRef documentation, refs are useful for non-state instance information:
However, useRef() is useful for more than the ref attribute. It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.
So im basically trying to emit and listen to a specific event on different typescript classes. The first event that is emitted is being listened properly on the other class but once I set a timeout to emit other event after 10 seconds for example, its like the listener is not listening anymore..
commonEmitter.ts
let events = require('events');
let em = new events.EventEmitter();
module.exports.commonEmitter = em;
network.ts
export class Network {
constructor() {
this.setConnection('connected');
setTimeout(() => {
commonEmitter.emit('connectionStatusChanged');
connection = 'disconnected';
}, 10000);
}
private setConnection(newConnection): void {
connection = newConnection
commonEmitter.emit('connectionStatusChanged');
}
public isConnected(): boolean {
return connection === 'connected';
}
}
export let connection = null;
view.ts
export class View {
private network: any;
constructor() { }
private test(){
console.log('Online? ' + this.network.isConnected());
}
public init(){
commonEmitter.on('connectionStatusChanged', this.test());
this.network = new Network();
}
At the end, both of the events are emitted but only the first one is being "listened".
Whats the reason for that and how I can do it in an ordered way?
In this line of code:
commonEmitter.on('connectionStatusChanged', this.test());
You're calling this.test() immediately, not passing a function reference that can be called LATER. Change it to this:
commonEmitter.on('connectionStatusChanged', this.test.bind(this));
So, you're properly passing a function reference that will also be properly bound to this.
Or, you could use a fat arrow callback function that will preserve the lexical value of this like this:
commonEmitter.on('connectionStatusChanged', () => {
this.test();
});
To answer the second question, you should've pass an object as a argument to emit
// network.ts
commonEmitter.emit('connectionStatusChanged', {data: true});
The arrow function combined with the test function call worked for me so thanks alot #jfriend000 :p The one with the bind method didn't work but it's ok.
I just have one more question, every time I try to emit and event with any type of arguments the listener is not listening to that. For example:
// view.ts
commonEmitter.on('connectionStatusChanged', (data: boolean) => {
console.log(data);
});
// network.ts
commonEmitter.emit('connectionStatusChanged', true);
If this is also related to the context, how should it be done?
By using this solution, now the first event that is emitted from the network class is never listened.. only after the timeout it works and refreshes the view but I don't know why the first one does not:/
// network.ts
commonEmitter.emit('connectionStatusChanged', {isOnline: true});
// view.ts
commonEmitter.on('connectionStatusChanged', (data: any) => {
console.log('Online? ' + data.isOnline);
});
I was trying to figure out RxJs. ShareReplay operator in particular. As per my understanding if there are two subscriptions to an observable then the observable should be executed twice. Unless there is a shareReplay involved. Obviously my understanding is incorrect because that is not what I is happening here. Could someone please help me understand this?
export class TestComponent implements OnInit {
constructor() {}
i = 0;
ngOnInit() {
console.clear();
let ob = this.httpcall().pipe(map(d => d));
let ob1$ = ob.pipe(map(d => d.toUpperCase()));
let ob2$ = ob.pipe(map(d => d.toLowerCase()));
ob1$.subscribe(d => {
console.log(d);
});
ob2$.subscribe(d => {
console.log(d);
});
}
httpcall() {
console.log("called");
this.i++;
return of("server cAlled:" + this.i);
}
}
Output:
called
SERVER CALLED:1
server called:1
The counter i did not get incremented to two even though there are two subscriptions and no shareReplay involved.
I was expecting(without shareReplay):
called
SERVER CALLED:1
called
server called:2
And with let ob = this.httpcall().pipe(map(d=>d),shareReplay()); I was expecting:
called
SERVER CALLED:1
server called:1
When you call subscribe, that will cause the observable to do everything it was defined to do. It was defined using of("server cAlled: 1");, which is then passed into a map operator. So since you subscribed twice, of will do its thing twice, and map will do its thing twice.
You happened to create the observable inside a function named httpcall, but the observable doesn't know anything about httpcall. httpcall will not be invoked an additional time.
If you want the incrementing of this.i to be part of what happens when subscribing, then you may need to use Observable.create. For example:
httpcall() {
return Observable.create((observer) => {
this.i++;
observer.next("server called: " + this.i);
observer.complete();
})
}
it seems that of return observable of it's own, so it's body doesn't get executed twice, so i recreate the example to show the difference between normal observable and of
export class AppComponent implements OnInit {
constructor(
) {
}
i = 0;
ngOnInit() {
console.clear();
const ob = this.httpCall1();
const ob1$ = ob.pipe(map(d => d.toUpperCase()));
const ob2$ = ob.pipe(map(d => d.toLowerCase()));
ob1$.subscribe(d => {
console.log(d);
});
ob2$.subscribe(d => {
console.log(d);
});
this.i = 0;
const ob2 = this.httpCall2();
const ob3$ = ob2.pipe(map(d => d.toUpperCase()));
const ob4$ = ob2.pipe(map(d => d.toLowerCase()));
ob3$.subscribe(d => {
console.log(d);
});
ob4$.subscribe(d => {
console.log(d);
});
}
httpCall1() {
return of('server1 called:' + ++this.i).pipe(
tap(d => console.log('tap1', d)),
);
}
httpCall2() {
return new Observable<string>((observer) => {
this.i++;
observer.next('server2 called: ' + this.i);
observer.complete();
}).pipe(
tap(d => console.log('tap2', d)),
);
}
}
That is because httpcall() is called only once via following line let ob = this.httpcall().pipe(map(d => d));. So it means that Observable that is returned by httpcall() will be reused thereafter.
If thats clear, now think about ob, which is source Observable. If you subscribe to this Observable you are going to get of("server cAlled:" + this.i); where this.i = 1. this.i is only going to increase if method httpcall() is executed once more, but it is not. Instead you are subscribing to the cold Observable ob which will just print what was used to create it. The value of this.i (which equals to 1) is already stored inside Observable ob and it is not going to be changed until new instance is created, no matter how many times you subscribe to ob.
Now lets look at the following code which is different:
let ob1$ = this.httpcall().pipe(map(d => d)).pipe(map(d => d.toUpperCase()));
let ob2$ = this.httpcall().pipe(map(d => d)).pipe(map(d => d.toLowerCase()));
In this situation httpcall() is called twice, thus this.i++; would happen twice and you would get what you were thinking to get.
Is there a good way to check if not completed Observable is empty at that exact time?
let cache = new ReplaySubject<number>(1);
...
// Here I want to know if 'cache' still empty or not. And, for example, fill it with initial value.
cache.isEmpty().subscribe(isEmpty => {
if (isEmpty) {
console.log("I want to be here!!!");
cache.next(0);
}
});
// but that code does not work until cache.complete()
Actually, it's not that simple and the accepted answer is not very universal. You want to check whether ReplaySubject is empty at this particular point in time.
However, if you want to make this truly compatible with ReplaySubject you need to take into account also windowTime parameter that specifies "time to live" for each value that goes through this object. This means that whether your cache is empty or not will change in time.
ReplaySubject has method _trimBufferThenGetEvents that does what you need. Unfortunately, this method is private so you need to make a little "hack" in JavaScript and extend its prototype directly.
import { ReplaySubject } from 'rxjs';
// Tell the compiler there's a isNowEmpty() method
declare module "rxjs/ReplaySubject" {
interface ReplaySubject<T> {
isNowEmpty(): boolean;
}
}
ReplaySubject.prototype['isNowEmpty'] = function() {
let events = this._trimBufferThenGetEvents();
return events.length > 0;
};
Then using this ReplaySubject is simple:
let s = new ReplaySubject<number>(1, 100);
s.next(3);
console.log(s.isNowEmpty());
s.next(4);
setTimeout(() => {
s.next(5);
s.subscribe(val => console.log('cached:', val));
console.log(s.isNowEmpty());
}, 200);
setTimeout(() => {
console.log(s.isNowEmpty());
}, 400);
Note that some calls to isNowEmpty() return true, while others return false. For example the last one returns false because the value was invalidated in the meantime.
This example prints:
true
cached: 5
true
false
See live demo: https://jsbin.com/sutaka/3/edit?js,console
You could use .scan() to accumulate your count, and map that to a boolean whether it's nonzero. (It takes a second parameter for a seed value which would make it start with a 0, so it always reflects the current count.)
I've also added a .filter() instead of an if statement to make it cleaner:
let cache = new ReplaySubject<number>(1);
cache
.map((object: T) => 1)
.scan((count: number, incoming: number) => count + incoming, 0)
.map((sum) => sum == 0)
.filter((isEmpty: boolean) => isEmpty)
.subscribe((isEmpty: boolean) => {
console.log("I want to be here!!!");
cache.next(0);
});
You could use takeUntil():
Observable.of(true)
.takeUntil(cache)
.do(isEmpty => {
if (isEmpty) {
console.log("I want to be here!!!");
cache.next(0);
}
})
.subscribe();
However this will just work once.
Another way would be to "null" the cache and initialize it as empty by using a BehaviorSubject:
let cache = new BehaviorSubject<number>(null as any);
...
cache
.do(content => {
if (content == null) {
console.log("I want to be here!!!");
cache.next(0);
}
})
.subscribe();
And of course you could initialize the cache with some default value right away.
startWith
let cache = new ReplaySubject<number>(1);
isEmpty$ = cache.pipe(mapTo(false), startWith(true));
This says:
Whatever the value is emitted by cache - map it to false. (because it isn't empty after an emission)
Start with true if nothing has been emitted yet (because that means it's empty)
I am learning MobX and cannot understand why autorun is only firing once...
const {observable, autorun} = mobx;
class FilterStore {
#observable filters = {};
#observable items = [1,2,3];
}
const store = window.store = new FilterStore;
setInterval(() => {
store.items[0] = +new Date
}, 1000)
autorun(() => {
console.log(store.filters);
console.log(store.items);
console.log('----------------');
});
jsFiddle: https://jsfiddle.net/1vmtzn27/
This is a very simple setup, and the setInterval is changing the value of my observable array every second but autorun is not fired... any idea why?
...and the setInterval is changing the value of my observable array every second...
No, it isn't. It's changing the contents of the array, but not the observable MobX is watching, which is store.items itself. Changing that would look like this:
store.items = [+new Date];
Since you didn't access store.items[0] in the autorun callback, it isn't watched for changes. (console.log did access it, but not in a way MobX could see.)
If you do access store.items[0], it will be watched for changes; if you add to or remove from the array, you might want to access length explicitly as well:
autorun(() => {
store.filters;
store.items.length;
store.items.forEach(function() { } );
console.log('Update received');
});
Updated Fiddle