If I call a Polymer debouncer from a button click it works perfectly. I click 5 times in less than 2 seconds, prints only one timestamp:
myProofOfConcept(){
this.__debouncer = Polymer.Debouncer.debounce(
.__debouncer,
Polymer.Async.timeOut.after(2000),
() => {
console.log("HEY " + Date.now());
});
}
But if I call the exact same method from a Polymer properties change observer, it will wait the required 2 second timeout, and then console print as many times as the observer calls it, even if only 1 millisecond apart.
Is there some external factor that I don’t know about, that is driving this difference in behavior?
Here's something to look for if this ever happens to you.
If you debounce a method within a web component...
and then you instantiate multiple instances of that component within your web app... it may appear to you that the debouncer is not working, when in fact it may be working, rather it is just working in multiple instances of the same web component.
I had a stray previous instance of the web component inside an entirely different page. Hadn't been used, or even noticed. I had just forgotten to remove it when I migrated it to a different page.
EDIT: Control for debouncing across all instances of the element and for property changed from multiple places (button clicks and setInterval).
Usage seems to be fine when applied like:
<dom-module id="my-element">
<template>
<style>
:host {
display: block;
}
</style>
[[prop]]
<button on-click="add">Test</button>
</template>
<script>
(function() {
let DEBOUNCED_METHOD;
HTMLImports.whenReady(function() {
class MyElement extends Polymer.Element {
static get is() { return 'my-element'; }
static get properties() {
return {
prop: {
value: 0,
type: Number,
observer: 'myProofOfConcept'
}
}
}
constructor() {
super();
let test = setInterval(() => {
this.add();
}, 400);
setTimeout(() => {
clearInterval(test);
}, 4500)
}
add() {
this.prop += 1;
}
myProofOfConcept(){
DEBOUNCED_METHOD = Polymer.Debouncer.debounce(
DEBOUNCED_METHOD,
Polymer.Async.timeOut.after(2000),
this.log);
}
log() {
console.log("HEY " + Date.now());
}
}
customElements.define(MyElement.is, MyElement);
});
}())
</script>
Hope that helps!
Related
I have been working on an application. There are multiple components on the page. The content inside them is scrollable. The expected functionality is when I scroll inside the component the hover effects on different elements should be disabled. After searching in the internet I have a working solution. I have created a HoverDisabler component like this,
import React, {useEffect} from 'react';
export const HoverDisabler = ({children}) => {
let timer = 0;
useEffect(() => {
document.addEventListener('scroll', () => {
clearTimeout(timer);
if(!document.body.classList.contains('hoverDisabled')){
document.body.classList.add('hoverDisabled');
}
timer = setTimeout(() => {
document.body.classList.remove('hoverDisabled');
},500);
}, true);
}, []);
return children;
}
The css for hoverDisabled is as follows,
.hoverDisabled {
pointer-events: 'none',
}
And I am wrapping my root App component with HoverDisabler like this,
<HoverDisabler><App /></HoverDisabler>
Now in all the component, if I start scrolling the hoverDisabled class is added in the body and it gets removed when I stop scrolling. Everything is working perfectly. I am curious if this is the correct approach of having this functionality? Is there any shortcomings of this approach or some problem I am overlooking because of my lack of experience?
Since scroll event is an expensive event you can add a debounce on your scroll event like this:
function debounce(method, delay) {
clearTimeout(method._tId);
method._tId= setTimeout(function(){
method();
}, delay);
}
function scrollFunction(){
clearTimeout(timer);
if(!document.body.classList.contains('hoverDisabled')){
document.body.classList.add('hoverDisabled');
}
timer = setTimeout(() => {
document.body.classList.remove('hoverDisabled');
},500);
}
document.addEventListener('scroll', function() {
debounce(scrollFunction, 100);
});
This will surely optimize your code, as it will only fire scroll function lesser number of times. Even though there may be other approaches to the problem you're trying to solve I'm just suggesting a way to optimize your current code.
I have just come accross with an issue related to event listening in Vue directives.
I have a component which holds following code inside:
function setHeaderWrapperHeight() { ... }
function scrollEventHandler() { ... }
export default {
...
directives: {
fox: {
inserted(el, binding, vnode) {
setHeaderWrapperHeight(el);
el.classList.add('header__unfixed');
window.addEventListener(
'scroll',
scrollEventListener.bind(null, el, binding.arg)
);
window.addEventListener(
'resize',
setHeaderWrapperHeight.bind(null, el)
);
},
unbind(el, binding) {
console.log('Unbound');
window.removeEventListener('scroll', scrollEventListener);
window.removeEventListener('resize', setHeaderWrapperHeight);
}
}
}
...
}
And this component is re-rendered everytime I change router path, I achieved this behaviour by assigning current route path to :key prop so whenever path changes it gets re-rendered. But the propblem is though event listeners are not being removed/destroyed causing terrible performance issues. So how do I remove event listeners?
Calling bind on a function creates a new function. The listeners aren't being removed because the function you're passing to removeEventListener is not the same function you passed to addEventListener.
Communicating between hooks in directives is not particularly easy. The official documentation recommends using the element's dataset, though that seems clumsy in this case:
https://v2.vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments
You could just store the listeners on the element directly as properties so that they're available in the unbind hook.
The code below takes a slightly different approach. It uses an array to hold all of the elements that are currently bound to the directive. The listener on window is only ever registered once, no matter how many times the directive is used. If the directive isn't currently being used then that listener is removed:
let foxElements = []
function onClick () {
console.log('click triggered')
for (const entry of foxElements) {
clickHandler(entry.el, entry.arg)
}
}
function clickHandler (el, arg) {
console.log('clicked', el, arg)
}
new Vue({
el: '#app',
data () {
return {
items: [0]
}
},
directives: {
fox: {
inserted (el, binding) {
console.log('inserted')
if (foxElements.length === 0) {
console.log('adding window listener')
window.addEventListener('click', onClick)
}
foxElements.push({
el,
arg: binding.arg
})
},
unbind (el, binding) {
console.log('unbind')
foxElements = foxElements.filter(element => element.el !== el)
if (foxElements.length === 0) {
console.log('removing window listener')
window.removeEventListener('click', onClick)
}
}
}
}
})
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<div id="app">
<button #click="items.push(Math.floor(Math.random() * 1000))">Add</button>
<hr>
<button
v-for="(item, index) in items"
v-fox:example
#click="items.splice(index, 1)"
>Remove {{ item }}</button>
</div>
However, all of this assumes that a directive is even the right way to go. If you can just do this at the component level then it may get a lot simpler because you have the component instance available to store things. Just remember that calling bind creates a new function, so you'll need to keep a reference to that function somewhere so you can pass it to removeEventListener.
Just for the records, and to help whoever passes through here as there is already an accepted answer, what one could do in this case (on Vue 3 at least, not tested on Vue 2) is to use binding.dir (which is a reference to the directive's own object) to host the function for adding the event listener on the directive object and take it back later when there's a need to remove this listener.
One simple example (not related to the original question) for binding one focus event:
export default {
...
directives: {
fox: {
handleFocus: () => { /* a placeholder to rewrite later */ },
mounted(el, binding) {
binding.dir.handleFocus = () => { /* do whatever */ }
el.addEventListener('focus', binding.dir.handleFocus);
},
beforeUnmount(el, binding) {
el.removeEventListener('focus', binding.dir.handleFocus);
}
}
}
...
}
A practical example of what I'm doing with this, in my case, is to have a focus/blur notifier for any input or textarea tag. I made a Gist here of this, it is on a project built on Vue 3 with TypeScript.
I have an element that being conditionally rendered with v-if="isLogged", if a user is logged in:
<div
v-if="isLogged"
class="chatBlock"
ref="chat"
></div>
I'm trying to get scroll height of the chat reference in a mounted () function - this.$refs.logged.scrollHeight, but it's not possible, because if a user is not logged in, then this div won't be rendered on a mounting stage, so even if a user logs in - it won't work, because mounted stage already passed.
Is it possible to track element appearance in a DOM using watch method?
UPDATE
Added watcher, as Steven suggested below in a mounted ():
this.$store.watch(
(state) => {
return this.$store.getters.isLogged
},
(newValue, oldValue) => {
if (newValue) {
this.chatHeight = this.$refs.chat.scrollHeight
}
}
)
The accepted answer did not work for me. The watch does not guarantee that the element has already been rendered.
To make sure that the element is available, I had to use $nextTick
if (newValue) {
this.$nextTick(function () {
this.chatHeight = this.$refs.chat.scrollHeight
})
}
This will cause the code to be executed after the next DOM update cycle.
Add a watch to isLogged. When it is active, get your chat ref. You will also have to check on your mounted, so put your logic in a common function.
So in your component:
val componentOptions = {
methods: {
checkDiv() {
const chatDiv = this.$refs.chat
if (chatDiv) {
// your code here...
}
}
},
mounted() {
this.checkDiv();
},
watch: {
isLogged(value) {
if (value) {
this.checkDiv()
}
}
}
}
—-
You can also use v-show instead of v-if. That way your element will be rendered but hidden.
Switching to v-show, as suggested by Steven, worked for me. It was actually the better option because v-show is cheap to toggle, unlike v-if, see this answer: https://stackoverflow.com/a/44296275/2544357
I have used Polymer 2 to create two custom elements:
a button
a overlay element
i am importing and using them both on another "main element".
What i would like to do is to toggle a class on the "overlay element" with each "button" click.
Is it possible to do this? Can i share data-bindings between elements?
Using the adage "events up, props down," sibling elements share data through their parent. There are various solutions for this. I'm using Redux to share data across several elements, but simple parent-to-n-children approach works:
In main-element.html
<button-element id="button"></button-element>
<overlay-element id="overlay"></overlay-element>
...
ready() {
super.ready();
this.$.button.addEventListener('toggle', this._onUpdate);
}
_onUpdate(event) {
this.$.overlay.toggle = !this.$.overlay.toggle;
}
In button-element.html
<button on-click="_doClick">Ok</button>
...
_doClick(event) {
this.dispatchEvent(new CustomEvent('toggle'));
}
In overlay-element.html
<div>[[status]]</div>
...
static get properties() {
return {
toggle: {
type: Boolean,
observer: '_toggleChanged'
},
status: {
type: String
}
...
_toggleChanged(newValue, oldValue) {
this.status = newValue ? 'I\'m hit.' : 'Missed me.';
}
See this pen.
Here's a js fiddle showing the question in action.
In the render function of a component, I render a div with a class .blah. In the componentDidMount function of the same component, I was expecting to be able to select the class .blah and append to it like this (since the component had mounted)
$('.blah').append("<h2>Appended to Blah</h2>");
However, the appended content does not show up. I also tried (shown also in the fiddle) to append in the same way but from a parent component into a subcomponent, with the same result, and also from the subcomponent into the space of the parent component with the same result. My logic for attempting the latter was that one could be more sure that the dom element had been rendered.
At the same time, I was able (in the componentDidMount function) to getDOMNode and append to that
var domnode = this.getDOMNode();
$(domnode).append("<h2>Yeah!</h2>")
yet reasons to do with CSS styling I wished to be able to append to a div with a class that I know. Also, since according to the docs getDOMNode is deprecated, and it's not possible to use the replacement to getDOMNode to do the same thing
var reactfindDomNode = React.findDOMNode();
$(reactfindDomNode).append("<h2>doesn't work :(</h2>");
I don't think getDOMNode or findDOMNode is the correct way to do what I'm trying to do.
Question: Is it possible to append to a specific id or class in React? What approach should I use to accomplish what I'm trying to do (getDOMNode even though it's deprecated?)
var Hello = React.createClass({
componentDidMount: function(){
$('.blah').append("<h2>Appended to Blah</h2>");
$('.pokey').append("<h2>Can I append into sub component?</h2>");
var domnode = this.getDOMNode();
$(domnode).append("<h2>appended to domnode but it's actually deprecated so what do I use instead?</h2>")
var reactfindDomNode = React.findDOMNode();
$(reactfindDomNode).append("<h2>can't append to reactfindDomNode</h2>");
},
render: function() {
return (
<div class='blah'>Hi, why is the h2 not being appended here?
<SubComponent/>
</div>
)
}
});
var SubComponent = React.createClass({
componentDidMount: function(){
$('.blah').append("<h2>append to div in parent?</h2>");
},
render: function(){
return(
<div class='pokey'> Hi from Pokey, the h2 from Parent component is not appended here either?
</div>
)
}
})
React.render(<Hello name="World" />, document.getElementById('container'));
In JSX, you have to use className, not class. The console should show a warning about this.
Fixed example: https://jsfiddle.net/69z2wepo/9974/
You are using React.findDOMNode incorrectly. You have to pass a React component to it, e.g.
var node = React.findDOMNode(this);
would return the DOM node of the component itself.
However, as already mentioned, you really should avoid mutating the DOM outside React. The whole point is to describe the UI once based on the state and the props of the component. Then change the state or props to rerender the component.
Avoid using jQuery inside react, as it becomes a bit of an antipattern. I do use it a bit myself, but only for lookups/reads that are too complicated or near impossible with just react components.
Anyways, to solve your problem, can just leverage a state object:
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="https://fb.me/react-0.13.3.js"></script>
</head>
<body>
<div id='container'></div>
<script>
'use strict';
var Hello = React.createClass({
displayName: 'Hello',
componentDidMount: function componentDidMount() {
this.setState({
blah: ['Append to blah'],
pokey: ['pokey from parent']
});
},
getInitialState: function () {
return {
blah: [],
pokey: []
};
},
appendBlah: function appendBlah(blah) {
var blahs = this.state.blah;
blahs.push(blah);
this.setState({ blah: blahs });
},
render: function render() {
var blahs = this.state.blah.map(function (b) {
return '<h2>' + b + '</h2>';
}).join('');
return React.createElement(
'div',
{ 'class': 'blah' },
{ blahs: blahs },
React.createElement(SubComponent, { pokeys: this.state.pokey, parent: this })
);
}
});
var SubComponent = React.createClass({
displayName: 'SubComponent',
componentDidMount: function componentDidMount() {
this.props.parent.appendBlah('append to div in parent?');
},
render: function render() {
var pokeys = this.props.pokeys.map(function (p) {
return '<h2>' + p + '</h2>';
}).join('');
return React.createElement(
'div',
{ 'class': 'pokey' },
{ pokeys: pokeys }
);
}
});
React.render(React.createElement(Hello, { name: 'World' }), document.getElementById('container'));
</script>
</body>
</html>
Sorry for JSX conversion, but was just easier for me to test without setting up grunt :).
Anyways, what i'm doing is leveraging the state property. When you call setState, render() is invoked again. I then leverage props to pass data down to the sub component.
Here's a version of your JSFiddle with the fewest changes I could make: JSFiddle
agmcleod's advice is right -- avoid JQuery. I would add, avoid JQuery thinking, which took me a while to figure out. In React, the render method should render what you want to see based on the state of the component. Don't manipulate the DOM after the fact, manipulate the state. When you change the state, the component will be re-rendered and you'll see the change.
Set the initial state (we haven't appended anything).
getInitialState: function () {
return {
appended: false
};
},
Change the state (we want to append)
componentDidMount: function () {
this.setState({
appended: true
});
// ...
}
Now the render function can show the extra text or not based on the state:
render: function () {
if (this.state.appended) {
appendedH2 = <h2>Appended to Blah</h2>;
} else {
appendedH2 = "";
}
return (
<div class='blah'>Hi, why isn't the h2 being appended here? {appendedH2}
<SubComponent appended={true}/> </div>
)
}