Detect component hover on scroll in React - javascript

As you may already know, mouseenter and mouseleave events are NOT triggered if the mouse doesn't move, which means that if you scroll over an element without moving the mouse, hover effects are ignored.
This answer describes the strategy to overcome this:
1: Add a scroll listener to the window.
2: In the handler, call document.elementsFromPoint.
3: Manually call the actual mouseover handler for those elements.
4: Manually call the actual mouseleave handler for elements no longer being hovered.
We also must take into account that registering a listener for each component we want to detect the hover of, is a waste of resource.
My idea is to create a singleton object and subscribe to it from each component.
I'm fairly new to react so I will try to write pseudo-react-code to describe my idea:
// A global singleton object that registers the listeners ONCE.
class Singleton {
subscribedElements = {}
subscribe(element, isHovered, setIsHovered) {
subscribedElements[element] = {
isHovered: isHovered,
setIsHovered: setIsHovered
}
}
unsubscribe(element) { ..... }
constructor() {
window.addEventListener('scroll', this.handleScroll);
window.addEventListener('mousemove', this.handleMove);
}
handleMove() {
//omitted
}
handleScroll() {
hoveredElements = document.elementsFromPoint(mousePosition)
// Iterate every element that is subscribed and check if they are in the list of elements that are hovered.
subscribedElements.map( e => {
if (hoveredElements.contains(e)) {
subscribedElements[e].setIsHovered(true)
} else {
subscribedElements[e].setIsHovered(false)
}
})
}
}
// a hook to be used in all elements that want to detect if they are hovered
function useHovered(element) {
[isHovered, setIsHovered] = useState(false);
Singleton.subscribe(element, isHovered, setIsHovered);
return isHovered;
}
// All components that want to check if they are being hovered would do this
function MyComponent(props) {
isHovered = useHovered(this);
return <div>{ isHovered ? 'hovered' : 'not hovered'</div>
}
Now, what is the most efficient and clean way to do something like this?
I have read about useContext but I'm not sure how to apply it to this solution.
I'm not sure how to create this singleton. Does it have to be a component? Can it be done in the new functional way?

Related

Lit element, close drop down when click outside of component

I'm working with Lit Element and I'm trying add an event listener on 'Click' that will a variable state that will set the dropdown to be expand or not. But once the drop down is 'closed' I want to remove that event to avoid unnecessary event calls on 'Click.
Adding the event works great but I cannot remove it.
Here is the idea:
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (changedProps.has("_tenantsExpanded")) {
document.removeEventListener("click", (ev) => this._eventLogic(ev, this));
if (this._tenantsExpanded)
document.addEventListener("click", (ev) => this._eventLogic(ev, this));
}
}
The fct logic:
private _eventLogic(e: MouseEvent, component: this) {
const targets = e.composedPath() as Element[];
if (!targets.some((target) => target.className?.includes("tenant"))) {
component._tenantsExpanded = false;
}
}
Code in my render's function:
${this._tenantsExpanded
? html` <div class="tenants-content">${this._tenantsContent()}</div> `
: html``}
Important note: I want the click event to be listened on all the window, not just the component itself. The same for removing the event.
PS: I don't know why e.currentTaget.className doesn't give me the actual className, but results to an undefined.
When you use removeEventListener you have to pass a reference to the same function you used when adding the listener.
In this example the function is stored in fn.
(You might have to change the this reference here, it depends a bit on your whole component).
const fn = (ev) => this._eventLogic(ev, this);
document.addEventListener("click", fn);
document.removeEventListener("click", fn);

In stencil js, how can i check if button has been clicked in another component from a different class

I have a component called button.tsx, this components holds a function that does certain things when the button is clicked, this.saveSearch triggers the saveSearch() function.
button.tsx
{((this.test1) || this.selectedExistingId) &&
(<button class="pdp-button primary"
onClick={this.saveSearch}>{this.langSave}</button>)
}
In sentence.tsx i want to be able to see when this button is clicked and show a certain div if the user has clicked it.
sentence.tsx
{onClick={saveSearch} && (<div class="styles-before-arrow">{this.langConfirmSearchSaved}</div>)}
You have a few options:
You can attach a click event listener for the button component in sentence.tsx. Take note that this may be trickier if you are working with elements which are encapsulated in Shadow DOM:
addButtonLister(): void {
document.querySelector('.pdp-button')
.addEventListener('click'), (e) => {
// add your logic here.
});
}
You can use EventEmitter (https://stenciljs.com/docs/events#events). In your button.tsx, you can add this:
#Event({eventName: 'button-event'}) customEvent: EventEmitter;
Then add something like this on button's onClick:
emitEvent() {
customEvent.emit('clicked');
}
render () {
return <button onClick={this.emitEvent}>{this.langSave}</button>
}
then from your sentence.tsx, add an event listener to your button component:
// say your button component's tag is <button-component>
document.querySelector('button-component')
.addEventListener('button-event', (e) => {
// your logic here.
});
You can use Stencil Store, but depending on your overall use-case, I am not sure if this may be an overkill - https://stenciljs.com/docs/stencil-store#store-state

Stimulus controller: event listened multiple times; how do I remove event listeners and retain the Context?

I've got the following controller on my HTML page:
...
<div data-controller="parent">
<div data-target="parent.myDiv">
<div data-controller="child">
<span data-target="child.mySpan"></span>
</div>
</div>
</div>
...
This child controller is mapped to the following child_controller.js class:
export default class {
static targets = ["mySpan"];
connect() {
document.addEventListener("myEvent", (event) => this.handleMyEvent(event));
}
handleMyEvent(event) {
console.log(event);
this.mySpanTarget; // Manipulate the span. No problem.
}
}
As you can see, there is an event listener on the connect() of the Stimulus controller, and when it detects that the event was fired up, it logs the event and manipulates the span target.
The problem arises when I replace the contents of the target myDiv from my parent_controller.js:
...
let childControllerHTML = "<div data-controller="child">...</div>"
myDivTarget.innerHTML= childControllerHTML;
...
Now that the myEvent gets fired up, the event listener picks it not once, but twice (because the same event got logged twice). With every subsequent replacement of the child HTML, the event gets logged one more time than it did before.
I know that one can make use of document.removeEventListener to prevent the old controller from still listening to the events:
export default class {
static targets = ["mySpan"];
connect() {
this.myEventListener = document.addEventListener("myEvent", (event) => this.handleMyEvent(event));
}
disconnect() {
document.removeEventListener("myEvent", this.myEventListener);
}
handleMyEvent(event) {
console.log(event);
this.mySpanTarget; // FAILS. Can't find span.
}
}
But doing it like this makes the handleMyEvent method lose the context as it no longer finds the mySpanTarget under this.
How can I remove the listener from the child controller to which I already got no access as it is no longer in the DOM, while retaining the context?
I found the answer on StimulusJS's Discourse page.
One has to make use of the bind method when initializing the controller:
export default class {
static targets = ["mySpan"];
initialize() {
this.boundHandleMyEvent = this.handleMyEvent.bind(this);
}
connect() {
document.addEventListener("myEvent", this.boundHandleMyEvent);
}
disconnect() {
document.removeEventListener("myEvent", this.boundHandleMyEvent);
}
handleMyEvent(event) {
console.log(event);
this.mySpanTarget; // Manipulate the span. No problem.
}
...
}
Now, the event is only listened once, and the context is not lost inside the handleMyEvent method.

How to stop onMouseOut event after onClick event (ReactJS)?

I need to make a hidden button visible when hovering over an adjacent text. This is being done through onMouseEnter and onMouseLeave events.
But when clicking on the text besides, I need to make the button completely visible and stop the onMouseLeave event.
So far what I have tried: trying to remove the onMouseLeave event using removeEventListener.
I know we could do this with the help of a variable like:
const mouseOut = (divId) => {
if (!wasClicked)
document.getElementById(divId).style.visibility = "hidden";
}
But since I have way too may different buttons and text, I do not want to use variables either.
<div className="step-buttons" onMouseEnter={mouseOver.bind(this, 'feature')}
onMouseLeave={mouseOut.bind(this, 'feature')}
onClick={showButtonOnClick.bind(this, 'feature')}>Test Suite
</div>
Is there anything that could help me here?
Any suggestion would be welome,
Thanks in advance :)
Way 1:
You can mount an eventlistener for leaving in the MouseEnter function and remove it in the onClick eventhandling with refs like so:
constructor(props) {
super(props);
this.text;
this.onMouseLeave = this.onMouseLeave.bind(this);
}
onMouseEnter(){
//apply your styles
this.text.addEventListener('mouseleave', this.onMouseLeave);
}
onMouseLeave(){
//remove your styles
}
showButtonOnClick(){
//remove the eventlistener
this.text.removeEventListener('mouseleave', this.onMouseLeave);
}
render(){
return(
<div ref={(thiscomponent) => {this.text = thiscomponent}}
onMouseEnter={this.onMouseEnter.bind(this, 'feature')}
onClick={this.showButtonOnClick.bind(this, 'feature')}>
Test Suite
</div>);
}
The ref ist just a reference for your div, so that you can mount the eventlistener to it.
You have to bind the function in the constructor, as .bind(this) will create a new function when you call it, to prevent mounting the eventhandler multiple times.
Way 2(probably better):
Another method would be to save the click on the text in the state, and conditionaly change what your onMouseLeave function does based on that:
constructor(props) {
super(props);
this.state={
clicked: false,
}
}
onMouseEnter(){
//apply your styles
}
onMouseLeave(){
if(!this.state.clicked){
//remove styles
}
}
showButtonOnClick(){
this.setState({clicked: true});
}
render(){
return(
<div
onMouseEnter={this.onMouseEnter.bind(this)}
onMouseLeave={this.onMouseLeave.bind(this)}
onClick={this.showButtonOnClick.bind(this)}>
Test Suite
</div>);
}
You can use removeEventListener to remove the mouseleave event in your showButtonOnClick function
showButtonOnClick(){
...your function...
document.getElementById('step-button').removeEventListener('mouseleave',this.callback());
}
callback(){
console.log("mouseleave event removed");
}
As you are doing this many times, it seems like creating these as components and using its state to store visibility without creating tons of global variables might be worthwhile.

Delegating Draggable Events to Parent Elements

I have draggable li elements nested in a ul in turn nested in a div, as seen below:
<div class='group'>
<div class='header'>
// some stuff here
</div>
<ul>
<li draggable='true'>
Stuff I want to drag and drop to another div.group
</li>
</ul>
</div>
There are multiple of these div elements and I am trying to implement a drag & drop functionality to move the li elements of one div group to another.
I have hooked up the ondragenter, ondragleave callbacks here:
// controller using mithril.js
ctrl.onDragLeave = function () {
return function (event) {
var target;
// Using isDropzone to recursively search for the appropriate div.group
// parent element, as event.target is always the children inside it
if ((target = isDropzone(event.target)) != null) {
target.style.background = "";
}
}
};
ctrl.onDragEnter = function () {
return function (event) {
var target;
if ((target = isDropzone(event.target)) != null) {
target.style.background = "purple";
}
};
};
function isDropzone(elem){
if(elem == null){
return null;
}
return elem.className == 'group' ? elem: isDropzone(elem.parentNode)
}
The problem comes when the event.target of the callbacks are always the nested child elements inside the div, such as li, and thus the callbacks are constantly fired. In this case I'm changing the color of the div.group with my callbacks, resulting in the div.group blinking undesirably.
Is there a way to delegate events and only allow the div grand parent of li to handle the events? Or any other way to work around this?
EDIT: Would still love to find out if there's a way to do this, but right now I'm using the workaround I found here.
So this is going to fit into the "you need to approach this from a different angle" category of answers.
Avoid- as much as possible- manipulating the DOM from event.target/event.currentTarget in your attached handlers.
A couple things differently:
Your ondragleave and ondragenter handlers should simply set some appropriate "state" attributes in your controller/viewModel/stores
When the handler is resolved, this generally triggers a redraw in Mithril. Internally m.startComputation() starts, your handler is called, then m.endComputation()
Your "view function" runs again. It then reflects the changed models. Your actions don't change the views, your views call actions which affect the models, and then react to those changes. MVC, not MVVM
Model
In your controller, set up a model which tracks all the state you need to show your drag and drop ui
ctrl.dragging = m.prop(null)
ctrl.groups = m.prop([
{
name: 'Group A',
dragOver: false,
items: ['Draggable One', 'Draggable Two']
},
...
// same structure for all groups
])
View
In your view, set up a UI that reflects your models state. Have event handlers that pass sufficient information about the actions to the controller- enough that it can properly respond to the actions an manipulate the model accordingly
return ctrl.groups.map(function (group, groupIdx) {
return m('.group',[
m('.header', group.name),
m('ul',
{
style: { background: (group.dragOver ? 'blue' : '')},
ondragleave: function () {ctrl.handleDragLeave(groupIdx)},
ondragenter: function () {ctrl.handleDragEnter(groupIdx)},
ondrop: function () {ctrl.handleDrop(groupIdx)},
ondragover: function (e) {e.preventDefault()}
},
group.items.map(function (item, itemIdx) {
return m('li',
{
draggable: true,
ondragstart: function () {ctrl.handleDragStart(itemIdx, groupIdx)}
},
item
})
)
])
})
Now its set up so that the group can properly display by reacting to state/model changes in your controller. We don't need to manipulate the dom to say a group has a new item, a group needs a new background color, or anything. We just need to attach event handlers so the controller can manipulate your model, and then the view will redraw based on that model.
Controller
Your controller therefore can have handlers that have all the info from actions needed to update the model.
Here's what some handlers on your controller will look like:
ctrl.handleDragStart = function (itemIdx, groupIdx) {
ctrl.dragging({itemIdx: itemIdx, groupIdx: groupIdx})
}
ctrl.handleDragEnter = function (groupIdx) {
ctrl.groups()[groupIdx].dragOver = true
}
ctrl.handleDragLeave = function (groupIdx) {
ctrl.groups()[groupIdx].dragOver = false
}
ctrl.handleDrop = function (toGroupIdx) {
var groupIdx = ctrl.dragging().groupIdx
var itemIdx = ctrl.dragging().itemIdx
var dropped = ctrl.groups()[groupIdx].items.splice(itemIdx, 1)[0]
ctrl.groups()[toGroupIdx].items.push(dropped)
ctrl.groups()[toGroupIdx].dragOver = false
ctrl.dragging(null)
}
Try to stick with Mithril's MVC model
event handlers call actions on your controller, which manipulates the model. The view then reacts to changes in those models. This bypasses the need to get entangled with the specifics of DOM events.
Here's a full JSbin example showing what you're trying to get to:
https://jsbin.com/pabehuj/edit?js,console,output
I get the desired effect without having to worry about event delegation at all.
Also, notice that in the JSbin, the ondragenter handler:
ondragenter: function () {
if (ctrl.dragging().groupIdx !== groupIdx) {
ctrl.handleDragEnter(groupIdx)
}
}
This is so the droppable area doesn't change color on its own draggable, which is one of the things I think you're looking for in your answer.

Categories