How to stop an emited value when another Observable emits? - javascript

Having two Observables, one emits a mouseover event (debounced by 500ms) and the other one a mouseout event, I'm looking for a possibility to stop the first Observable (mouseover) from emiting when the second Observable (mouseout) occurs.
let mouseOutObservable = Observable.fromEvent($('.row'), 'mouseout')
Observable.fromEvent($('.row'), 'mouseover')
.debounceTime(500)
// .stopEmitingWhen(mouseOutObservable) --> how? possible?
.subscribe(event => {
// show tooltip
mouseOutObservable.first()
.subscribe(() => {
// destroy tooltip
});
});

takeUntil does exactly what you want.

Matt Burnell's and Ivan Malagon's suggested solutions work fine if no neighbouring elements. But my row elements do occur within a table. I did write my question kinda interpretable. Applying their code suggestion will unsubscribe/dispose the subscription completely but I do need a solution to stop only current emited value from arriving in subscribe.
However, both answers do solve my question above. ;-) Therefore I accepted Matt Burnell's short answer.
In order to include my additional requirement, I came up with another solution which merges both observable to one, followed by using a debounce time and continue only if the last event is a mouseover event.
Observable.fromEvent($('.row'), 'mouseover')
.merge(mouseOutObservable)
.debounceTime(500)
.filter(event => event[ 'type' ] === 'mouseover')
.subscribe(event => {
// ....
});

You can get the subscription object for the mouseover event and then dispose that subscription within the mouseout function.
let mouseOutObservable = Rx.Observable.fromEvent($('.row'), 'mouseout')
let mouseOverObservable = Rx.Observable.fromEvent($('.row'), 'mouseover')
.debounce(500);
let mouseOverObservableSubscription = mouseOverObservable.subscribe(() => { $('#output').append('<p>mouseover</p>'); });
mouseOutObservable.subscribe(() => {
$('#output').append('<p>mouseout</p>');
mouseOverObservableSubscription.dispose();
})
.row {
min-height: 48px;
background-color: orange;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.0.6/rx.all.js"></script>
<div class="row">Mouse over me!</div>
<div id="output"></div>

Related

addEventListener only works when I use window infront of it

I would like to use a code when I press the space bar a shape appears and it disappears when I press it again. I'm trying to get the addEventListener to work with a sample:
hello = document.querySelector('#Player');
with player being the id of the shape that I want to control. I declared hello above and initialized it in setup (I am using JavaScript), the Player id has also been initialized in HTML and given a shape in CSS. When I use
hello.addEventListener('keypress', (event) => {
console.log(event.key)
})
nothing happens, but when I use
window.addEventListener('keypress', (event) => {
console.log(event.key)
})
it works. Is there anything that I am doing wrong?
It is because by default div is not selectable. In order to make it selectable you need to use tabindex attribute on your div. It will make your div selectable.
const hello = document.querySelector('#player');
hello.addEventListener("keypress", evt => {
console.log(evt.key)
})
<div id="player" tabindex="0">
Player Shape
</div>
It will show a boundary around your div which can be remove by using css -
outline: none;
That's what should happen.
You can add the keypress event to the window or document.
And, if you add it to both, the window wins over for some reason – someone else might clarify this to both of us.
const el = document.getElementById("el");
el.addEventListener("keypress", event => keyPressed(event, "blue"));
document.addEventListener("keypress", event => keyPressed(event, "green"));
window.addEventListener("keypress", event => keyPressed(event, "purple"));
function keyPressed(event, color) {
if (event.key = " ")
el.style.backgroundColor = color;
}
#el {
width: 100px;
height: 100px;
background: red;
}
<div id="el"></div>

Debouncing event handlers in AlpineJS x-init directive

I'm trying to add scroll event listeners to x-refs (I don't want to add them in the DOM with #scroll.debounce, because I want it to be as portable as possible).
Codepen here: https://codepen.io/skttl/pen/vYXowBY?editors=1111
<div x-data="xOverflow()" x-init="init()" style="width:50%;border:1px solid red;position:relative;overflow:hidden;">
<div x-ref="wrapper" style="overflow-x:auto;">
<div x-ref="content" style="width:1000px;border:1px solid blue; height:500px;">
<div>wrapper.clientWidth: <span x-text="$refs.wrapper.clientWidth"></span></div>
<div>content.clientWidth: <span x-text="$refs.content.clientWidth"></span></div>
<div>overflow: <span x-text="overflow"></span>
</div>
</div>
<div x-show="overflow" style="position:absolute;top:0;left:90%;right:0;bottom:0;background:rgba(0,0,0,.15);"></div>
</div>
function xOverflow() {
return {
overflow:false,
// methods
setFromResize() {
this.overflow = this.$refs?.wrapper?.clientWidth < this.$refs?.content?.clientWidth
console.log(`resize: overflow is ${this.overflow}`);
},
setFromScroll(e) {
this.overflow = !(e.target.scrollLeft == this.$refs?.content?.clientWidth - this.$refs?.wrapper?.clientWidth);
console.log(`scroll: overflow is ${this.overflow}`);
},
init() {
window.addEventListener("resize", event => _.debounce(this.setFromResize(), 250));
this.$refs?.wrapper.addEventListener("scroll", event => _.debounce(this.setFromScroll(event)));
}
}
}
What I am trying to detect if the clientWidth of $refs.content is larger than $refs.wrapper. When the window resizes, this should be detected again.
In addition to that, I want to show an overlay (if content is overflowing), that should be removed when scrolled to the end.
I could do it by just slapping #resize.window.debounce on the root element, and #scroll.debounce on the wrapper element. But I want this component to be portable, by just adding x-data, x-init and x-refs, without worrying about attaching event listeners.
I tried adding lodash debounce to the eventlisteners, but the functions gets called for each event, and not debounced. Check the console.log for proof.
Can anyone help me get this right?
So turns out I am an idiot, and I just needed to move things around a bit :)
From
window.addEventListener("resize", event => _.debounce(this.setFromResize(), 250));
this.$refs?.wrapper.addEventListener("scroll", event => _.debounce(this.setFromScroll(event)));
to
window.addEventListener("resize", _.debounce(event => this.setFromResize(), 250));
this.$refs?.wrapper.addEventListener("scroll", _.debounce(event => this.setFromScroll(event)));

React Hooks: useState with onClick only updating the SECOND time button is clicked?

Edit: forgot an important part - this is noticeable if you click the button next to Jeff A. Menges and check the console log.
The important part of the code is the "setFullResults(cardResults.data.concat(cardResultsPageTwo.data))" line in the onClick of the button code. I think it SHOULD set fullResults to whatever I tell it to... except it doesn't work the first time you click it. Every time after, it works, but not the first time. That's going to be trouble for the next set, because I can't map over an undefined array, and I don't want to tell users to just click on the button twice for the actual search results to come up.
I'm guessing useEffect would work, but I don't know how to write it or where to put it. It's clearly not working at the top of the App functional component, but anywhere else I try to put it gives me an error.
I've tried "this.forceUpdate()" which a lot of places recommend as a quick fix (but recommend against using - but I've been trying to figure this out for hours), but "this.forceUpdate()" isn't a function no matter where I put it.
Please help me get this button working the first time it's clicked on.
import React, { useState, useEffect } from "react";
const App = () => {
let artistData = require("./mass-artists.json");
const [showTheCards, setShowTheCards] = useState();
const [fullResults, setFullResults] = useState([]);
useEffect(() => {
setFullResults();
}, []);
let artistDataMap = artistData.map(artistName => {
//console.log(artistName);
return (
<aside className="artist-section">
<span>{artistName}</span>
<button
className="astbutton"
onClick={ function GetCardList() {
fetch(
`https://api.scryfall.com/cards/search?unique=prints&q=a:"${artistName}"`
)
.then(response => {
return response.json();
})
.then((cardResults) => {
console.log(cardResults.has_more)
if (cardResults.has_more === true) {
fetch (`https://api.scryfall.com/cards/search?unique=prints&q=a:"${artistName}"&page=2`)
.then((responsepagetwo) => {
return responsepagetwo.json();
})
.then(cardResultsPageTwo => {
console.log(`First Results Page: ${cardResults}`)
console.log(`Second Results Page: ${cardResultsPageTwo}`)
setFullResults(cardResults.data.concat(cardResultsPageTwo.data))
console.log(`Full Results: ${fullResults}`)
})
}
setShowTheCards(
cardResults.data
.filter(({ digital }) => digital === false)
.map(cardData => {
if (cardData.layout === "transform") {
return (
//TODO : Transform card code
<span>Transform Card (Needs special return)</span>
)
}
else if (cardData.layout === "double_faced_token") {
return (
//TODO: Double Faced Token card code
<span>Double Faced Token (Needs special return)</span>
)
}
else {
return (
<div className="card-object">
<span className="card-object-name">
{cardData.name}
</span>
<span className="card-object-set">
{cardData.set_name}
</span>
<img
className="card-object-img-sm"
alt={cardData.name}
src={cardData.image_uris.small}
/>
</div>
)
}
})
)
});
}}
>
Show Cards
</button>
</aside>
);
});
return (
<aside>
<aside className="artist-group">
{artistDataMap}
</aside>
<aside className="card-wrapper">
{showTheCards}
</aside>
</aside>
);
};
export default App;
CodesAndBox: https://codesandbox.io/embed/compassionate-satoshi-iq3nc?fontsize=14
You can try refactoring the code like for onClick handler have a synthetic event. Add this event Listener as part of a class. Use arrow function so that you need not bind this function handler inside the constructor. After fetching the data try to set the state to the result and use the state to render the HTML mark up inside render method. And when I run this code, I have also seen one error in console that child elements require key attribute. I have seen you are using Array.prototype.map inside render method, but when you return the span element inside that try to add a key attribute so that when React diffing algorithm encounters a new element it reduces the time complexity to check certain nodes with this key attribute.
useEffect(() => {
// call the functions which depend on fullResults here
setFullResults();
}, [fullResults])
// now it will check whether fullResults changed, if changed than call functions inside useEffect which are depending on fullResults

Can you pass an element to a function within the template in Vue?

I'm trying to calculate and set an element's max-height style programmatically based on the number of children it has. I have to do this on four separate elements, each with a different number of children, so I can't just create a single computed property. I already have the logic to calculate the max-height in the function, but I'm unable to pass an element from the template into a function.
I've tried the following solutions with no luck:
<div ref="div1" :style="{ maxHeight: getMaxHeight($refs.div1) }"></div>
This didn't work because $refs is not yet defined at the time I'm passing it into the function.
Trying to pass this or $event.target to getMaxHeight(). This didn't work either because this doesn't refer to the current element, and there was no event since I'm not in a v-on event handler.
The only other solution I can think of is creating four computed properties that each call getMaxHeight() with the $ref, but if I can handle it from a single function called with different params, it would be easier to maintain. If possible, I would like to pass the element itself from the template. Does anyone know of a way to do this, or a more elegant approach to solving this problem?
A cheap trick I learned with Vue is that if you require anything in the template that isnt loaded when the template is mounted is to just put a template with a v-if on it:
<template v-if="$refs">
<div ref="div1" :style="{ maxHeight: getMaxHeight($refs.div1) }"></div>
</template>
around it. This might look dirty at first, but the thing is, it does the job without loads of extra code and time spend and prevents the errors.
Also, a small improvement in code length on your expandable-function:
const expandable = el => el.style.maxHeight =
( el.classList.contains('expanded') ?
el.children.map(c=>c.scrollHeight).reduce((h1,h2)=>h1+h2)
: 0 ) + 'px';
I ended up creating a directive like was suggested. It tries to expand/compress when:
It's clicked
Its classes change
The element or its children update
Vue component:
<button #click="toggleAccordion($event.currentTarget.nextElementSibling)"></button>
<div #click="toggleAccordion($event.currentTarget)" v-accordion-toggle>
<myComponent v-for="data in dataList" :data="data"></myComponent>
</div>
.....
private toggleAccordion(elem: HTMLElement): void {
elem.classList.toggle("expanded");
}
Directive: Accordion.ts
const expandable = (el: HTMLElement) => el.style.maxHeight = (el.classList.contains("expanded") ?
[...el.children].map(c => c.scrollHeight).reduce((h1, h2) => h1 + h2) : "0") + "px";
Vue.directive("accordion-toggle", {
bind: (el: HTMLElement, binding: any, vnode: any) => {
el.onclick = ($event: any) => {
expandable($event.currentTarget) ; // When the element is clicked
};
// If the classes on the elem change, like another button adding .expanded class
const observer = new MutationObserver(() => expandable(el));
observer.observe(el, {
attributes: true,
attributeFilter: ["class"],
});
},
componentUpdated: (el: HTMLElement) => {
expandable(el); // When the component (or its children) update
}
});
Making a custom directive that operates directly on the div element would probably be your best shot. You could create a directive component like:
export default {
name: 'maxheight',
bind(el) {
const numberOfChildren = el.children.length;
// rest of your max height logic here
el.style.maxHeight = '100px';
}
}
Then just make sure to import the directive in the file you plan on using it, and add it to your div element:
<div ref="div1" maxheight></div>

How to check if element has focused child using javascript?

I'm trying to remove all jQuery from my code. Until now I used
if ($(selector).find(':focus').length === 0) {
// focus is outside of my element
} else {
// focus is inside my element
}
to distinguish wether the focus is inside of one of my elements. Can you show me a jQuery-free way of doing it?
You can use Node.contains native DOM method for this.
el.contains(document.activeElement);
will check if activeElement is a descendant of el.
If you have multiple elements to check, you can use a some function to iterate.
It is possible with Element's matches() method and with a simple selector string as follows:
let hasFocused = elem.matches(':focus-within:not(:focus)');
let focusedOrHasFocused = elem.matches(':focus-within');
Use CSS :focus pseudo-class in querySelectorAll()
setTimeout(function(){
if (document.querySelectorAll("div :focus").length === 0)
console.log("not focused");
else
console.log("focused")
}, 2000);
<div>
<input type="text">
</div>
Depending on your situation, using events might be more performant.
You can use the focusin and focusout events in that case.
const el = document.getElemen
el.addEventListener("focusin", () => console.log("focus!"));
el.addEventListener("focusout", () => console.log("blur!"));
Note that during focusout events the document.activeElement will be the document body. To work around this issue, you can make use of FocusEvent.relatedTarget.
If you have issue where document.activeElement is returning <body> element after blur event, you just need to wrap it with setTimeout() and it will return correct element.
handleBlur() {
setTimeout(() => {
console.log(document.activeElement); // this actually return active/focused element
});
}
if you are using it standalone without timeout
handleBlur() {
console.log(document.activeElement); // this is returning <body> element
}
Combined some of answers posted here. Using a combination of focusin, focusout, contains and relatedTarget, you should be able to know when focus is on the children of a particular element.
const elm = document.getElementById('check-focus-here')
elm.addEventListener('focusin', (event) => {
console.log(event.target, event.relatedTarget)
// console.log(elm.contains(event.relatedTarget))
})
elm.addEventListener('focusout', (event) => {
console.log(event.target, event.relatedTarget)
console.log(elm.contains(event.relatedTarget))
})
#check-focus-here {
border: 2px solid;
padding: 8px;
margin: 8px;
}
<div id="check-focus-here">
<input id="first-name" type="text" />
<input id="middle-name" type="text" />
<input id="last-name" type="text" />
<button type="button">Save Name</button>
</div>
<button type="button">Tab to this for outside focus</button>
Here's a working example following #Northern and #Adam Šipický answers...
const tr = document.querySelector("table tbody tr");
tr.addEventListener('blur', () => {
setTimeout(() => {
if (!tr.contains(document.activeElement)) {
// Execute your condition code here...
}
}, 200);
}, true);
In 2021 you can probably avoid javascript altogether to check if an element or any of the element's child nodes have focus – unless you are manipulating DOM elements outside of a parent element.
For example:
<div class="parent">
<button>foo</button>
<button>food</button>
<button>foosh</button>
</div>
.parent { background: white }
.parent:focus-within { background: red }
.parent:focus-within button:not(:focus) { opacity: .5 }
None of these existing non CSS based solutions account for the situation where the JavaScript context does not match the frame the node was rendered in. To account for this you would want to do something like the following:
el.contains(el.ownerDocument.activeElement)
To retrieve the selected element you can use:
let activeElement = document.activeElement
To check a specific element:
let elem = document.getElementById('someId');
let isFocused = (document.activeElement === elem);

Categories

Resources