I have a react application which contains a react component of publication cards. They represent a publication that has a title, some authors, a summary, date, and some other fields. I want each of the elements to have the same card size and each of the elements within the card to line up so the title sections are all the same height as the tallest title, the authors are the same height as the tallest of the authors and so on.
To do this, I have have an inner div that contains the content shouldn't change it's height and and outer div that I set to the highest clientHeight of all inner div elements.
I have a resize function that works, however only for when you drag and move the browser by resizing the window. When I click the maximize, restore, or snap the browser to the screen it ignores the height of the inner div and sets it to weird values. Ie, the rendered size of the content will be 85px in the chrome debugger, however clientWidth if I trace that value before I set it shows all of them for each of the inner divs as like 28 and so the outer div is being set to that value.
render() {
return (
<PubContainer>
<TitleSection
style={{ height: this.state.pubTitleHeight }}>
<InnerDiv className="innerPubTitleDiv">
{this.props.publicationData.header}
<SubtitleSection>
{this.props.publicationData.subtitle}
</SubtitleSection>
</InnerDiv>
</TitleSection>
</PubContainer>
);
}
SubtitleSection is a div and InnerDiv is the container that should have its height set to whatever is the value within. Here the rest of the code, along with the resize function.
componentDidUpdate() {
this.updatePublicationCardSizes();
}
componentDidMount() {
this._isMounted = true;
this.updatePublicationCardSizes();
window.addEventListener('resize', this.updatePublicationCardSizes.bind(this));
}
componentWillUnmount() {
this._isMounted = false;
window.removeEventListener('resize', this.updatePublicationCardSizes.bind(this));
}
updatePublicationCardSizes() {
this.setContainerHeightByClassName('innerPubTitleDiv', 'pubTitleHeight');
}
setContainerHeightByClassName(innerDivClassName, stateName) {
var classNameList = document.getElementsByClassName(innerDivClassName);
if (classNameList != undefined) {
var maxHeight = 0;
for (var index = 0; index < classNameList.length; index++) {
var divHeight = classNameList[index].clientHeight;
if (maxHeight < divHeight) {
maxHeight = divHeight;
}
}
if (this._isMounted) {
if (maxHeight > 0) {
if (this.state[stateName] != maxHeight) {
this.setState({
[stateName]: maxHeight
});
}
} else {
if (this.state[stateName] != '100%'){
this.setState({
[stateName]: '100%'
});
}
}
}
}
}
I've verified that resize is being called on all changes to the browser (drag, snap, restore, max, min). And again, the normal resizing by dragging the browser to change width does update and resize it correctly, it's only when I click the maximize button for example do I get the weird values. I'm trying to find out why just those cases don't work and how to make them work.
EDIT
I've refactored my code to use hooks, but I'm still getting the same issue when I click maximize/restore. Apparently the inner div clientHeight is still being calculated from a pre-render size, instead of setting it after the resize event has finished and rendered. Any ideas?
function PublicationCard(props) {
const [pubTitleHeight, setTitleHeight] = useState('100%');
useLayoutEffect(() => {
updatePublicationCardSizes();
window.addEventListener("resize", updatePublicationCardSizes);
return () => window.removeEventListener("resize", updatePublicationCardSizes);
});
const updatePublicationCardSizes = () => {
setTitleHeight(getContainerMaxHeightByClassName('innerPubTitleDiv'));
};
const getContainerMaxHeightByClassName = (innerDivClassName) => {
var classNameList = document.getElementsByClassName(innerDivClassName);
if (classNameList != undefined) {
var maxHeight = 0;
for (var index = 0; index < classNameList.length; index++) {
var divHeight = classNameList[index].clientHeight;
if (maxHeight < divHeight) {
maxHeight = divHeight;
}
}
if (maxHeight > 0) {
return maxHeight;
} else {
return '100%';
}
}
};
return (
<PubContainer>
<a href={`/publications/${props.publicationData.id}`} target="blank">
<div
style={{ height: pubTitleHeight }}>
<div className="innerPubTitleDiv">
{props.publicationData.header}
<span>
{props.publicationData.subtitle}
</span>
</div>
</div>
</a>
</PubContainer>
);
}
Related
i want to implement collapse and hidden in vuejs
but i think, ref it does not work in vue3
i am getting this error Header.vue?76f0:68 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'myText')
this is my code
<button class="border-0 navbar" #click="toggle()">
<Links
class="color-dark"
ref="myText"
:style="[isActive ? { height: computedHeight } : {}]"
/>
function toggle() {
this.isActive = !this.isActive
}
function initHeight() {
this.$refs.myText.style.height = 'auto'
this.$refs.myText.style.position = 'absolute'
this.$refs.myText.style.visibility = 'hidden'
this.$refs.myText.style.display = 'block'
const height = getComputedStyle(this.$refs['myText']).height
this.computedHeight = height
this.$refs.myText.style.position = null
this.$refs.myText.style.visibility = null
this.$refs.myText.style.display = null
this.$refs.myText.style.height = 0
}
watchEffect(async () => {
initHeight()
})
i was copying this code to vuejs3 (this worked but i need to vuejs3)
https://jsfiddle.net/rezaxdi/tgfabw65/9/
Looks like there's something more to it than what's in the code. A simple vue2=>vue3 conversion from example works just fine
example:
Vue.createApp({
data() {
return {
isActive: false,
computedHeight: 0
}
},
methods: {
toggle: function() {
this.isActive = !this.isActive;
},
initHeight: function() {
this.$refs['myText'].style.height = 'auto';
this.$refs['myText'].style.position = 'absolute';
this.$refs['myText'].style.visibility = 'hidden';
this.$refs['myText'].style.display = 'block';
const height = getComputedStyle(this.$refs['myText']).height;
this.computedHeight = height;
this.$refs['myText'].style.position = null;
this.$refs['myText'].style.visibility = null;
this.$refs['myText'].style.display = null;
this.$refs['myText'].style.height = 0;
}
},
mounted: function() {
this.initHeight()
}
}).mount("#app");
p {
height: 0;
overflow: hidden;
transition: 1s;
}
<script src="https://unpkg.com/vue#3.2.31/dist/vue.global.prod.js"></script>
<div id="app">
<p ref="myText" :style="[isActive ? { height : computedHeight } : {}]">
Now it's smooth - getting closer to BS4 collapse, but now I need to know the exact height of each section at a particular screen size to set the max-height. Over-compensating max-width work ok, but is still a bit 'jerky'. Is there a way to calculate an
elements height before starting the show/hide? If the max-height value is too small, elements overlap.
</p>
<button #click="toggle">Open / Close</button>
</div>
however, I see you are using a watchEffect, so I surmise that you might be using (some🤷♂️) composition API functionality. In this case, the watch will execute before mount, so it will run initHeight which will fail.
If you are using composition api, there's more things there that might cause it to not work, so you may need to show more of the code. Alternatively, you can stick to the Class API, which works same as it did in vue2.
I am trying to build a toolbar that hides components from the right if there is not enough space to render them. My approach is to use refs and add up the width and render based on the condition if the total width has been overflowed. I want to get something working and go on improving it from there. It seems to work 'ok' when the screen size is decreased but not when trying to 're-render' the components when there is room. I suspect adding a display style of 'none' is causing some of the issues.
componentDidMount() {
this.littleFunction();
window.addEventListener('resize', this.littleFunction);
}
littleFunction = () => {
let sofar = 0;
for (const ref in this.refs) {
sofar += this.refs[ref].offsetWidth;
const index = ref.indexOf('test');
console.log(ref, sofar, this.input.offsetWidth);
if (sofar > this.input.offsetWidth && index === -1) {
this.refs[ref].style.display = 'none';
}
// // console.log(typeof this.refs[ref].style.display, this.refs[ref].style.display);
// if (this.refs[ref] !== this.input) {
// sofar = this.refs[ref].offsetWidth + sofar;
// }
// const index = ref.indexOf('test');
// // console.log(sofar, this.input.offsetWidth, index);
// if (sofar >= this.input.offsetWidth && index === -1) {
// this.refs[ref].style.display = 'none';
//
// this.forceUpdate();
// } else if (sofar < this.input.offsetWidth && index === -1) {
// // console.log('inhiaaa', sofar, this.input.offsetWidth);
// this.refs[ref].style.display = '';
//
// this.forceUpdate();
// }
}
}
After thinking about this for a while, i realized that if i set the style to display: 'none', the next time I try to run this logic to check how many components can fit, I am actually not getting the length back from the components that were previously set to display: 'none'. What I did was save the width of the components before applying calling the function.
componentDidMount() {
this.widths = new List();
for (const ref in this.refs) {
this.widths = this.widths.push(Map({
name: ref,
length: this.refs[ref].offsetWidth,
}));
}
this.littleFunction();
window.addEventListener('resize', this.littleFunction);
}
littleFunction = () => {
let sofar = 0;
this.widths.forEach(item => {
sofar += item.get('length');
const index = item.get('name').indexOf('test');
if (sofar > this.input.offsetWidth && index === -1) {
this.refs[item.get('name')].style.display = 'none';
// this.forceUpdate();
} else if (index === -1) {
this.refs[item.get('name')].style.display = 'inline';
// this.forceUpdate();
}
});
}
Have the toolbar to have style { width: '100%', overflow: 'hidden', whiteSpace: 'nowrap' } should give you the desired effect.
I'm a little confused if I can use React Virtualized's Collection component to solve my problem. I'll try to describe what I'm doing:
I'm using React Virtualized on a page to display two lists/collections of items. I've finished the first collection which has items that have the same width and height:
The first collection was pretty straight forward and easy to implement.
Now I'm working on the second collection which contains images of varying sizes. I want the cells to have the same height but different widths (depending on the image dimensions of course). The problem is that rows might not always have the same number of cells:
Is this possible to achieve with React Virtualized? If so, how can I determine the position in "cellSizeAndPositionGetter"?
I recently used react-virtualized List to display rows of fixed-height, variable-width image cards and it worked great.
My List rowRenderer uses an array of rows of image card elements. That is, an array of arrays of react components, as JSX.
See my final function, cardsRows, for how I build the rows based on element widths and screen width.
Here's how it looks:
Hope this helps!
Some snippets of my code:
import {AutoSizer, List} from 'react-virtualized';
...
updateDimensions() {
this.setState({
screenWidth: window.innerWidth,
});
}
componentDidMount() {
window.addEventListener("resize", this.updateDimensions);
}
componentDidUpdate(prevProps, prevState) {
const props = this.props;
const state = this.state;
if (JSON.stringify(props.imageDocs) !== JSON.stringify(prevProps.imageDocs) || state.screenWidth !== prevState.screenWidth)
this.setState({
cardsRows: cardsRows(props, state.screenWidth),
});
}
rowRenderer({key, index, style, isScrolling}) {
if (!this.state.cardsRows.length)
return '';
return (
<div id={index} title={this.state.cardsRows[index].length} key={key} style={style}>
{this.state.cardsRows[index]}
</div>
);
}
...
render() {
return (
<div style={styles.subMain}>
<AutoSizer>
{({height, width}) => (<List height={height}
rowCount={this.state.cardsRows.length}
rowHeight={164}
rowRenderer={this.rowRenderer}
width={width}
overscanRowCount={2}
/>
)}
</AutoSizer>
</div>
);
}
...
const cardsRows = (props, screenWidth) => {
const rows = [];
let rowCards = [];
let rowWidth = 0;
const distanceBetweenCards = 15;
for (const imageDoc of props.imageDocs) {
const imageWidth = getWidth(imageDoc);
if (rowWidth + distanceBetweenCards * 2 + imageWidth <= screenWidth) {
rowCards.push(cardElement(imageDoc));
rowWidth += distanceBetweenCards + imageWidth;
}
else {
rows.push(rowCards);
rowCards = [];
rowWidth = distanceBetweenCards;
}
}
if (rowCards.length) {
rows.push(rowCards);
}
return rows;
};
const styles = {
subMain: {
position: 'absolute',
display: 'block',
top: 0,
right: 0,
left: 0,
bottom: 0,
}
};
Sometimes it is desirable to persist scroll positions between page visits.
Turbolinks resets scroll position after loading the data.
How can I disable it for particular elements?
My solution in ES6:
const turbolinksPersistScroll = () => {
const persistScrollDataAttribute = 'turbolinks-persist-scroll'
let scrollPosition = null
let enabled = false
document.addEventListener('turbolinks:before-visit', (event) => {
if (enabled)
scrollPosition = window.scrollY
else
scrollPosition = null
enabled = false
})
document.addEventListener('turbolinks:load', (event) => {
const elements = document.querySelectorAll(`[data-${persistScrollDataAttribute}="true"]`)
for (let i = 0; i < elements.length; i++) {
elements[i].addEventListener('click', () => {
enabled = true
})
}
if (scrollPosition)
window.scrollTo(0, scrollPosition)
})
}
turbolinksPersistScroll()
And add on your links data-turbolinks-persist-scroll=true on links you want persist scrollbar position.
<a href="..." data-turbolinks-persist-scroll=true>Link</a>
This works for me, also with link_to remote: true.
Use the following javascript to persist scrolls. I have created a selector that matches all elements with class turbolinks-disable-scroll. Before loading,the script saves the scroll position and after loading it loads the saved positions.
// persist scrolls
// pirated from https://github.com/turbolinks/turbolinks-classic/issues/205
var elementsWithPersistentScrolls, persistentScrollsPositions;
elementsWithPersistentScrolls = ['.turbolinks-disable-scroll'];
persistentScrollsPositions = {};
$(document).on('turbolinks:before-visit', function() {
var i, len, results, selector;
persistentScrollsPositions = {};
results = [];
for (i = 0, len = elementsWithPersistentScrolls.length; i < len; i++) {
selector = elementsWithPersistentScrolls[i];
results.push(persistentScrollsPositions[selector] = $(selector).scrollTop());
}
return results;
});
$(document).on('turbolinks:load', function() {
var results, scrollTop, selector;
results = [];
for (selector in persistentScrollsPositions) {
scrollTop = persistentScrollsPositions[selector];
results.push($(selector).scrollTop(scrollTop));
}
return results;
});
It seems like there are two approaches to this problem.
Preserve flagged elements (#vedant1811's answer)
Preserve body scroll for flagged links
The second approach is the one that I've been looking for and couldn't find anywhere, so I'll provide my answer to that here.
The solution here is very similar to that of the first approach, but perhaps a little simpler. The idea is to grab the current scroll position of the body when an element is clicked, and then scroll to that position after the page is loaded:
Javascript
Turbolinks.scroll = {}
$(document).on('click', '[data-turbolinks-scroll=false]', function(e){
Turbolinks.scroll['top'] = $('body').scrollTop();
})
$(document).on('page:load', function() {
if (Turbolinks.scroll['top']) {
$('body').scrollTop(Turbolinks.scroll['top']);
}
Turbolinks.scroll = {};
});
Markup
<a href='/' data-turbolinks-scroll='false'>Scroll preserving link</a>
I use a scroll attribute on the Turbolinks object to store my scroll position when a [data-turbolinks-scroll=false] link is clicked, then after I scroll the page I clear this attribute.
It is important that you clear the attribute (Turbolinks.scroll = {}) otherwise, subsequent clicks on non-flagged anchor links will continue to scroll you to the same position.
Note: depending on the specific styling of html and body you may need to use the scroll offset from both. An example of how this might be accomplished is:
Turbolinks.scroll = {};
$(document).on('click', '[data-turbolinks-scroll=false]', function (e) {
Turbolinks.scroll['top'] = {
html: $("html").scrollTop(),
body: $("body").scrollTop()
}
});
$(document).on('turbolinks:load', function() {
if (Turbolinks.scroll['top']) {
$('html').scrollTop(Turbolinks.scroll['top']['html']);
$('body').scrollTop(Turbolinks.scroll['top']['body']);
}
Turbolinks.scroll = {};
});
I noticed that sometimes scroll is going up and then only down. This version prevents such behaviour:
const persistScrollDataAttribute = 'turbolinks-persist-scroll';
let scrollPosition = null;
const turbolinksPersistScroll = () => {
if (scrollPosition) {
window.scrollTo(0, scrollPosition);
scrollPosition = null;
}
const elements = document.querySelectorAll(`[data-${persistScrollDataAttribute}="true"]`)
for (let i = 0; i < elements.length; i++) {
elements[i].addEventListener('click', () => {
document.addEventListener("turbolinks:before-render", () => {
scrollPosition = window.scrollY;
}, {once: true})
})
}
}
document.addEventListener('turbolinks:load', turbolinksPersistScroll);
document.addEventListener('turbolinks:render', turbolinksPersistScroll);
We want to display a looping slideshow of pictures that looks like a gif. The current result is visible at this url: https://figuredevices.com.
Our current approach is using opacity to show or hide slides:
class SlideShow extends React.Component {
constructor(props, context) {
super(props);
this.state = {
currentSlide: 0
};
this.interval = null;
}
componentDidMount() {
this.interval = setInterval(this.transitionToNextSlide.bind(this), 200);
}
componentWillUnmount(){
if(this.interval){
clearInterval(this.interval);
}
}
transitionToNextSlide() {
let nextSlide = this.state.currentSlide + 1;
if (nextSlide == this.props.slides.length) {
nextSlide = 0;
}
this.setState({currentSlide: nextSlide});
}
render () {
let slides = this.props.pictures.map((picture, idx) => {
let slideContainerStyle = {
opacity: this.state.currentSlide == idx ? 1 : 0
};
return(
<div style={slideContainerStyle} key={idx}>
<Slide picture={picture}/>
</div>
);
})
let containerStyle = {
width:'100%'
};
return (
<div style={containerStyle}>
{slides}
</div>
);
}
};
Pictures are loaded 5 by five into this.props.picture. The number of pictures is not bounded and I am worried about performance as this number grows. There are two things that don't feel right to me:
The map operation in the render method is traversing a whole array every 200ms only to change two css properties.
The DOM is growing a lot in size but most of nodes are hidden
Would you suggest a better approach, maybe using animation or react-motion ?
You should maintain all of your pictures as an array, and every 200ms, increment the array index. Then, instead of displaying all of the pictures every single time, just have it display the picture at your current index. This is a lot better because you're only returning one photo ever instead of a bunch of invisible ones :)
Note: I wasn't able to test this, so I'm not sure if everything is exactly syntactically correct, but this is the general idea. Every 200ms, increment the slideIndex. Then, always return a div with only the one picture you want to see.
class SlideShow extends React.Component {
constructor(props, context) {
super(props);
this.state = {
slideIndex;
};
this.interval = null;
}
componentDidMount() {
this.interval = setInterval(this.transitionToNextSlide.bind(this), 200);
}
componentWillUnmount(){
if(this.interval){
clearInterval(this.interval);
}
}
transitionToNextSlide() {
this.setState({this.state.slideIndex: (this.state.slideIndex + 1) % this.props.slides.length})
render () {
let containerStyle = {
width:'100%'
};
return (
<div style={containerStyle}>
<Slide picture={this.props.pictures[this.state.currentSlide]}/>
</div>
);
}
};