folks!
I have a dashboard application which consists of a more or less long page. At the end of the page, there's a grid container showing some data. Inside this container, the user can switch between different views of this container, which then changes a property (_activeAlarmsListViewSet, can be either list or top).
The switching between the views works fine and as expected, the only thing I encounter is, that the page jumps to the top during the rendering.
I already tried catching the update() call and handle it by myself (and yes, no jumping anymore), but also there's no more view switching as no re-render happens:
update(changedProperties) {
if (changedProperties.has('_activeAlarmsListViewSet') && changedProperties.get('_activeAlarmsListViewSet') != null) {
console.log('No re-render');
} else {
super.update(changedProperties);
}
}
Is there any possibility I can trigger the re-rendering without "jumping" to the top of the page and keep the scrolling position?
Thanks!
I don't think the problem is LitElement - when render is called only the parts that have changed update, the rest of the DOM remains unchanged and wherever you have scrolled to remains as long as the content is tall enough.
I think the issue is that you have a reflow between the list and top states that has shorter height, so that the browser corrects the scroll to the intermediate max height.
I most commonly see this with loaders and async directives, as these replace the DOM while waiting for a Promise.
For instance:
render() {
return html`... ${until(getAndRenderData(), 'loading...')}`;
}
Will cause the DOM of the loaded data to be replaced with loading... every time any update happens, while we only want it to happen when getAndRenderData() might change.
There are couple of ways around this, but simplest is probable to to use a guard directive:
render() {
return html`... ${guard(this._activeAlarmsListViewSet,
() => until(getAndRenderData(this._activeAlarmsListViewSet), 'loading...'))}`;
}
Now the content in the guard only changes when this._activeAlarmsListViewSet changes, and you don't have to block the update call.
Another way I find works is to split your list or top views with internally consistent states - Lit doesn't re-render sub-components unless their properties change (in which case they handle it themselves). You can then have these maintain the consistent height as you switch between them.
Related
Describe the Problem
I am making a very simple ReactJS/Gatsby website for someone and I am having an issue with one of my functional styled components when it re-renders. The problem is that it is causing the window to jump (scroll) after the re-render is complete.
The re-render is triggered by the user clicking on a span element (enclosed in an li element) which fires a function.
The list of li elements is determined by the state of the component. The overall parent component has a fixed height which is why I am having trouble diagnosing the issue.
What I Expect to Happen
The component to re-render and the window's scroll position to remain where it was when the user initiated it.
What Actually Happens
When the user clicks the element the page appears to jump (scroll). Sometimes it does so and remains in the new position, sometimes it does so and then returns to the original scroll position.
What I've Tried
I've tried following advice from other questions which suggest using event.preventDefault() and others which suggest moving the styling out of the component itself and, instead, opting for using classes.
Neither of these solutions worked.
I have managed to definitively find that the issue is due to setActiveTabs -- which causes the re-render of the ul element -- as logging window.scrollY both prior to it firing and after it completes displays a different value.
Edit 2:
I have managed to figure out that the issue is with making the list items targetable. It seems that either adding the tabIndex="0" attribute or making the li child an interactive element causes this bug.
Does anyone know a way around this?
Edit
The full frontend source code can be found in the following GitHub repo: https://github.com/MakingStuffs/resinfusion
In order to solve the issue I needed to prevent the clicked element from being targeted on the re-render. In order to do this I edited the clickHandler so that it uses element.blur() after setting the state.
The click handler is as follows:
const forwardClickHandler = event => {
setLoading(true)
const clickedSlug =
event.target.closest("button") !== null
? event.target.closest("button").getAttribute("data-slug")
: event.target.children[0].getAttribute("data-slug")
const categoryObject = getNeedle(clickedSlug, categories, "slug")
const subCatObject = getNeedle(clickedSlug, subCategories, "slug")
const serviceObject = getNeedle(clickedSlug, services, "slug")
const associatedChildren = getAssociatedChildren(
categoryObject
? categoryObject
: subCatObject
? subCatObject
: serviceObject
)
setBgImage(associatedChildren[0].thumb.localFile.childImageSharp.fluid)
setActiveTabs(associatedChildren)
event.target.blur()
return setTimeout(() => setLoading(false), 1000)
}
I made a functional demo sandbox here
This is a basic array cycler with 3 elements. And these 3 elements are rendered as slides which move visually left/right depending on the direction you pick.
I don't think the approach I took to make this work is a good one, and if you have suggestions on that I'm open to it.
But the actual question, in order to make the sliding work "equivalently" in both direction i.e. left/right I had to delay the one "sliding" to the left. My naming convention is kind of confusing too because you click the button e.g. prev/next and the array cycling/sliding is flipped. I did use an anti-pattern with the external variable that holds the direction selected since I was having problems with multiple states affecting the slider/causing rendering issues.
But TL:DR this is the onClick handler for the prev/next buttons passing in boolean for direction.I'm using a CSS animation for the motion part. I'm also aware nested ternaries are bad.
const cycleArr = cyclePrev => {
if (!slideDone) {
return;
}
setSlideDone(false);
const newArrSort = cyclePrev ? cycleLeft(slides) : cycleRight(slides);
slideClassRef.current.classList = slideDir
? `App ${slideDir === "left" ? "slide-left" : "slide-right"}`
: "App";
if (slideDir === "left") {
setSlides(newArrSort);
setTimeout(() => {
slideClassRef.current.classList = "App";
setSlideDone(true);
}, 1050);
} else {
setTimeout(() => {
slideClassRef.current.classList = "App";
setSlides(newArrSort);
setSlideDone(true);
}, 1000);
}
};
I'm aware I could have just used something off the shelf eg. slick carousel but this is a good demo of my current problems with state planning in ReactJS. I'm trying to get better/think better at it.
I'm not sure I quite understand your question, if you are referring to the additional 50ms delay when sliding, my best guess is that the call to setSlides(newArrSort); sets the state and also triggers an immediate React re-render of the component. This probably takes some amount of time, hence the desynchronisation with the CSS transition.
Anti-patterns are not there to make your life difficult, they are there to stop you getting into a confusing mess :)
Components can re-render whenever React deems it necessary, which is why state should be properly stored, and pretty effects done in useEffect hooks. I would recommend a more React-based data flow, where you update the state at the top, and let it propagate downwards, reacting to the new state, applying the correct CSS transformations. It's declarative, like HTML and CSS. You don't tell the browser what to paint, you describe how to paint it.
I'm trying to figure out how to animate moving react component from one to another. For example take very simple, yet interesting card game: you may place any card to a deck, or take top card from the deck.
To make it work, I have 4 classes - <Board> which holds two <Card Collection>: "Deck" and "Hand" components. In constructor, they generate CardModel items and render them via <Card> component. <CardCollection> component has special onCardClick prop which takes callback function. In my case it's onCardClick={/*this is equal to <Board>*/ this.transferCard("Hand")}
Board.transferCard takes clicked CardModel from state of one component and pushes it to another.
The problem is animation - I want card to fly, preferably through center (optional!) from old place to new. I am able to place the newly created Card in "new place" to the same place as old component, but, since i jsut strated to learn React, I'm not sure where exactly I should start. Wrap in ReactCSSTransitionGroup? pass "animate: from{x,y} to{x,y}" property to <CardCollection>?
So the full question is what is the most generic, controllable and "react" way to animate this situation?
JSFiddle base question version: https://jsfiddle.net/fen1kz/6qxpzmm6/
JSFiddle first animation prototype version: https://jsfiddle.net/fen1kz/6qxpzmm6/1
I don't like <this.state.animations.map... here. Also the Animation component looks like an abomination for me, I'm not sure this is the good architecture style for React.
The main mistake I did is try to mix render function with animation. No! Render should not contain anything related to animation (preferably not even starter values), while all the animation should happen after render. The only thing that bothers me is that i still should have state of animations in CardCollection just to throw it into creation of Card
Working example: https://jsfiddle.net/fen1kz/6qxpzmm6/4/
let animation;
if (this.animations[cardModel.id] != void 0) {
animation = this.animations[cardModel.id];
this.animations[cardModel.id] = void 0;
}
...
return <Card
cardModel={cardModel}
onCardClick={onCardClick}
animation={animation}
position={this.getCardPosition(i)}
index={i}
key={cardModel.id}
ref={cardModel.id} // prolly not needed
/>
You can try to use a package I made called react-singular-component, which might actually do what you need.
The component allows you to render a component server times and by a given priority only the highest one will render. Mounting and uncounting the given component will cause the next highest priority to render instead with an animation moving and wrapping the component from its last place and size to the new one.
Here is the Github page: https://github.com/dor6/SingularComponent
Just an idea: You could try to use jQuery animate for animation of moving some HTML element from one place to another. Once animation is complete there is a complete function property only then you could trigger your onCardClick.
So I have a <Layout> component. Based on the page, it loads in different child components. In those, they might have tabs and they might not.
This changes the way I want scrolling to work, and therefore the markup of the scrolling.
I basically ended up making sure each child component had an element like <div id="scroll-container">
Then within my <Layout> component I did this:
componentDidMount() {
this._updateHeight();
window.addEventListener('resize', this._updateHeight, false);
},
componentDidUpdate() {
this._updateHeight();
},
_updateHeight() {
var container = document.getElementById('scroll-container');
if (container ) {
let height = window.innerHeight - container.getBoundingClientRect().top;
container.style.height = height + 'px';
}
},
I know there are helpers for things like getBoundintClientRec, which I am going to use later. Right now I am just wondering about the general flow.
I was thinking I can make a component that is scroll-container that wraps wherever I need that, but then I'd need to always do something like
<ScrollContainer {...this.props}>
<ChildIHadThereAnyway>
</ScrollContainer>
Meanwhile is a component "supposed" to know about window? <Layout> is currently my top level so I think it's fine there, but unsure about children and such.
Basically unsure what is best for connecting global window size with aspect of components position.
This method is not perfect but works fine for me.
My code has root component Application, if you're using react-router you probably have one too, otherwise it's easy do add it.
I have as well reducer and actions for managing window properties like screen resolution or orientation. Lets call them windowReducer and windowActions.
In root component I've register event handlers with something like:
componentDidMount() {
window.addEventListener('resize', this.onWindowResolutionChange, false);
}
onWindowResolutionChange() {
windowActions.onResolutionChange({
width: window.innerWidth,
height: window.innerHeight
});
}
As other redux actions onResolutionChange dispatches action to windowReducer where it sets new window width and height.
So, to get them in your component you can just pass them from container's state as other props.
The worst in this approach is the fact that state changes too frequently witch leads to extra re-rendering of the components. Usually you're using functions like debounce to avoid it. If one of you're component needs immediate changes and other debounced, you will probably end (as I do) with two different actions setting different values to state (width and debouncedWidth).
I am building the diagram component in JavaScript. It has two layers rendered separately: foreground and background.
To determine the required size of the background:
render the foreground
measure the height of the result
render the foreground and the
background together
In code it looks like this:
var foreground = renderForegroundIntoString();
parentDiv.innerHTML = foreground;
var height = parentDiv.children[0].clientHeight;
var background = renderBackgroundIntoString(height);
parentDiv.innerHTML = foreground + background;
Using IE7, this is a piece of cake. However, Firefox2 is not really willing to render the parentDiv.innerHTML right away, therefore I cannot read out the foreground height.
When does Firefox execute the rendering and how can I delay my background generation till foreground rendering is completed, or is there any alternative way to determine the height of my foreground elements?
[Appended after testing Dan's answer (thanx Dan)]
Within the body of the callback method (called back by setTimeout(...)) I can see, the rendering of the innerHTML is still not complete.
You should never, ever rely on something you just inserted into the DOM being rendered by the next line of code. All browsers will group these changes together to some degree, and it can be tricky to work out when and why.
The best way to deal with it is to execute the second part in response to some kind of event. Though it doesn't look like there's a good one you can use in that situation, so failing that, you can trigger the second part with:
setTimeout(renderBackground, 0)
That will ensure the current thread is completed before the second part of the code is executed.
I don't think you want parentDiv.children[0] (children is not a valid property in FF3 anyway), instead you want parentDiv.childNodes[0], but note that this includes text nodes that may have no height. You could try looping waiting for parentDiv's descendants to be rendered like so:
function getRenderedHeight(parentDiv) {
if (parentDiv.childNodes) {
var i = 0;
while (parentDiv.childNodes[i].nodeType == 3) { i++; }
//Now parentDiv.childNodes[i] is your first non-text child
return parentDiv.childNodes[i].clientHeight;
//your other code here ...
} else {
setTimeout("isRendered("+parentDiv+")",200);
}
}
and then invoke by: getRenderedHeight(parentDiv) after setting the innerHTML.
Hope that gives some ideas, anyway.