I have a page on my website in which I display some cards in a masonry layout.
It's implemented in Vue 3 and I want the layout do adapt to the container's child heights.
The layout flows from top to bottom, left to right. Just like the images shows:
To achieve this, I divided the cards in different columns using the order CSS property together with a flexbox.
However, for this to work, the parent container needs to have a fixed height.
I want this to be the minimum height possible to make sure all cards fit, that is, the exact height of the longest column of the layout.
I tried to set the container's height to 0 initially and then update it based on the card's height, however, this doesn't work very well and is really janky.
<template>
<section class="container" :style="{ height: containerHeight }">
<project-card v-for="i in projects.length" :key="i" :project="projects[i - 1]"
:style="{ order: (i - 1) % numberColumns + 1, width: (100 / numberColumns) - 1.5 + '%' }"
:ref="setProjectCardRef">
</project-card>
<span v-for="i in numberColumns - 1" :key="i" :style="{ order: i }" class="item break"></span>
</section>
</template>
<script>
import Projects from "#/api/Projects";
import ProjectCard from "#/components/ProjectCard";
export default {
name: "Projects",
components: {
"project-card": ProjectCard,
},
data() {
return {
projects: [],
projectCardsRefs: [],
windowWidth: window.innerWidth,
containerHeight: "100%"
}
},
mounted() {
this.getData();
window.addEventListener('resize', () => {
this.windowWidth = window.innerWidth
})
},
methods: {
getData() {
Projects.list().then((response) => {
for (let project of response.data)
project.image_url = process.env.VUE_APP_API_ENDPOINT + project.image_url;
this.projects = response.data;
});
},
setProjectCardRef(el) {
if (!this.projectCardsRefs.includes(el))
this.projectCardsRefs.push(el)
}
},
computed: {
numberColumns() {
return Math.round(this.windowWidth / 400)
},
},
async updated() {
await new Promise(r => setTimeout(r, 200));
let heights = Array(this.numberColumns).fill(0)
for (let i = 0; i < this.projectCardsRefs.length; i++) {
const style = this.projectCardsRefs[i].$el.currentStyle || window.getComputedStyle(this.projectCardsRefs[i].$el);
const marginTop = parseInt(style.marginTop.match(/\d+/g)[0]);
const height = parseFloat(style.height.match(/\d+(.\d+)?/g)[0]);
heights[i % this.numberColumns] += height + marginTop
}
this.containerHeight = 40 + Math.max(...heights) + "px";
}
}
</script>
<style scoped>
.container {
#apply flex flex-col flex-wrap space-y-6;
}
.break {
#apply mx-3 w-0;
flex-basis: 100%;
}
</style>
How can I set the container's height based on its children in a more responsive way?
Following Paulie_D's suggestion, I implemented it using Masonry.JS
<template>
<section id="container" v-masonry transition-duration="0.2s" item-selector=".item"
percent-position="true" ref="container" :gutter="spaceBetween">
<project-card class="item" v-masonry-tile v-for="i in projects.length" :key="i" :project="projects[i - 1]"
:style="`width: ${itemWidth}px; margin-bottom: ${spaceBetween}px`"/>
</section>
</template>
<script>
import Projects from "#/api/Projects";
import ProjectCard from "#/components/ProjectCard";
export default {
name: "Projects",
components: {
"project-card": ProjectCard,
},
data() {
return {
projects: [],
containerWidth: 0,
spaceBetween: 20
}
},
mounted() {
this.getData();
new ResizeObserver(this.onResize).observe(document.getElementById("container"))
this.onResize()
},
methods: {
getData() {
Projects.list().then((response) => {
for (let project of response.data)
project.image_url = process.env.VUE_APP_API_ENDPOINT + project.image_url;
this.projects = response.data;
});
},
onResize() {
this.containerWidth = document.querySelector("main").offsetWidth;
}
},
computed: {
numberColumns() {
return Math.round(this.containerWidth / 400);
},
itemWidth() {
return (this.containerWidth - this.numberColumns * this.spaceBetween) / this.numberColumns;
}
}
}
</script>
Related
I have an array of 510 elements (number[]). I need to create a chart with this data from an array. The problem is that when I try to draw a chart with 510 points, the chart's width is stretched to a width that is larger than the width of the div. How to set canvas width equal to parent width?
<template>
<canvas class="canvas"
ref="canvasRef"
>
</canvas>
</template>
<script lang="ts">
import { Options, Prop, Watch } from 'vue-property-decorator'
import BaseComponent from '#/views/components/BaseComponent'
import { IPipe } from '#/views/apps/wave/historian/components/ChartContainer.vue'
#Options({
name: 'TestGraph',
})
export default class TestGraph extends BaseComponent {
#Prop() pipeList!: IPipe[]
mounted () {
this.$nextTick(() => {
this.drawGraph()
})
}
#Watch('pipeList', { deep: true })
drawGraph () {
const canvasRef = this.$refs.canvasRef as HTMLCanvasElement
canvasRef.width = canvasRef.offsetParent!.clientWidth
const ctx: CanvasRenderingContext2D = canvasRef.getContext('2d')!
ctx.scale(1.3, 1.4)
this.pipeList.forEach(it => {
let prevCoords = 0
if (it.selected) {
it.data.forEach((item, idx) => {
ctx.moveTo(prevCoords, item)
ctx.lineTo(prevCoords + 5, it.data[idx + 1])
prevCoords += 5
})
}
})
ctx.strokeStyle = 'rgb(255,255,255)'
ctx.stroke()
}
}
</script>
<style lang="scss" scoped>
.canvas {
display: flex;
width: 100%;
height: 100%;
position: absolute;
}
</style>
I have a Sidebar on left of my App, and it is set on 300px wide. When I click on a button, the Sidebar goes from 300px to 50px.
I change this width with a state :
const [isMinimized, setIsMinimized] = useState(false);
[...]
<div className={`${isMinimized ? 'w-16' : '2xl:w-80 w-56'} relative bg-white`}>
I'd like to set an animation when the sidebar minimize. I tried to add an animation on my div, but does not work.
Is it possible ?
You can specify a className when isMinimized is true ( for Example isMinimzed ? 'animatedScrollbar w-16' : '2xl:w-80 w-56') and add your transition as css using the className of the minimized state .
You need to add transition-width so tailwind adds transition-property:width to the element's style properties.
You can do this all within JSX with tailwind classes. No need for other code.
Check below
function MyApp() {
const [isMinimized, setIsMinimized] = React.useState(false);
return (
<div >
<div
className = {
`${isMinimized ? 'w-16' : '2xl:w-80 w-56'} relative bg-black duration-1000 transition-width ease-in-out`
} >
MY DIV
</div>
<button
onClick = {() => setIsMinimized(!isMinimized)}
>
CLICK
</button>
</div>
)
}
ReactDOM.createRoot(
document.getElementById("root")
).render( <
MyApp / >
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
Or see jsFiddle
If you want to add custom transition durations or behaviors you would need to change tailwind config. Read more here tailwind transition
Use Web Animation API
First create a ref object of the element you change its width.
const ref = useRef(null);
And use it:
<div ref={ref} className={relative bg-white`}>
Call this function on click.
const onClick = () => {
const { current } = ref;
if (current) {
const keyframe = [{ width: "300px" }, { width: "50px" }];
const options = {
duration: 1000,
fill: "forwards",
delay: 0,
easing: "ease-in",
};
const newKeyframeEffect = new KeyframeEffect(current, keyframe, options);
const animation = new Animation(newKeyframeEffect, document.timeline);
animation.play();
animation.onfinish = () => {
}
}
};
Do whatever you need in onfinish functoin.
If you need to change the width of the element back to 300px, do this:
Track if the element is expanded.
const [isExpended, setIsExpended] = useState(false)
If expanded, change its width from 300px to 50px. Else vise versa.
const keyframe = isExpended ? [{ width: "50px" }, { width: "300px" }]: [{ width: "300px" }, { width: "50px" }];
When the animation finishes, change the isExpended state.
setIsExpended(!isExpended)
Example code
const App = () => {
const ref = useRef(null)
const [isExpended, setIsExpended] = useState(false)
const onClick = () => {
const { current } = ref;
if (current) {
const keyframe = isExpended ? [{ width: "50px" }, { width: "300px" }]: [{ width: "300px" }, { width: "50px" }];
const options = {
duration: 1000,
fill: "forwards",
delay: 0,
easing: "ease-in",
};
const newKeyframeEffect = new KeyframeEffect(current, keyframe, options);
const animation = new Animation(newKeyframeEffect, document.timeline);
animation.play();
animation.onfinish = () => setIsExpended(!isExpended);
}
};
return <div ref={ref} style={{width: "300px", background: "red"}}>
<button onClick={onClick} type="button">Click</button>
</div>
}
Try this
https://stackblitz.com/edit/react-17rkkx?file=src%2FApp.js
Reference
useRef - https://reactjs.org/docs/hooks-reference.html#useref
Web Animation API - https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API
I am trying to add a font-awesome arrow icon via my css code like this:
<style>
.negative {
color: red;
}
.positive {
color: green;
}
.negative::after {
content: "\f106";
}
</style>
I have font-awesome included in my html via CDN. For some reason my icon does not display properly, it just shows a square. Any ideas why and how I can fix it?
Here is the rest of my code, showing the logic behind the displaying of percentages:
<template>
<div>
<v-data-table
:headers="headers"
:items="rowsToDisplay"
:hide-default-footer="true"
class="primary"
>
<template #item.thirtyDaysDiff="{ item }">
<span :class="item.thirtyDaysDiffClass">{{ item.thirtyDaysDiff }}%</span>
</template>
<template #item.sevenDaysDifference="{ item }">
<span :class="item.sevenDaysDiffClass">{{ item.sevenDaysDiff }}%</span>
</template>
</v-data-table>
</div>
</template>
<script>
import axios from 'axios';
export default {
data () {
return {
bitcoinInfo: [],
isPositive: false,
isNegative: false,
headers: [
{
text: 'Currency',
align: 'start',
value: 'currency',
},
{ text: '30 Days Ago', value: '30d' },
{ text: '30 Day Diff %', value: 'thirtyDaysDiff'},
{ text: '7 Days Ago', value: '7d' },
{ text: '7 Day Diff %', value: 'sevenDaysDifference' },
{ text: '24 Hours Ago', value: '24h' },
],
}
},
methods: {
getBitcoinData() {
axios
.get('data.json')
.then((response => {
var convertedCollection = Object.keys(response.data).map(key => {
return {currency: key, thirtyDaysDiff: 0, sevenDaysDifference: 0, ...response.data[key]}
})
this.bitcoinInfo = convertedCollection
}))
.catch(err => console.log(err))
},
calculateDifference(a, b) {
let calculatedPercent = 100 * Math.abs((a - b) / ((a + b) / 2));
return Math.max(Math.round(calculatedPercent * 10) / 10, 2.8).toFixed(2);
},
getDiffClass(a, b) {
return a > b ? 'positive': a < b ? 'negative' : ''
},
calculateSevenDayDifference(item) {
let calculatedPercent = 100 * Math.abs((item['24h'] - item['7d']) / ((item['24h'] + item['7d']) / 2));
return Math.max(Math.round(calculatedPercent * 10) / 10, 2.8).toFixed(2);
}
},
computed: {
rowsToDisplay() {
return Object.keys(this.bitcoinInfo)
.map(key => {
return {
currency: key,
...this.bitcoinInfo[key]
}
}).map((item) => ({
...item,
thirtyDaysDiff: this.calculateDifference(item['7d'], item['30d']),
thirtyDaysDiffClass: this.getDiffClass(item['7d'], item['30d']),
sevenDaysDiff: this.calculateDifference(item['24h'], item['7d']),
sevenDaysDiffClass: this.getDiffClass(item['24h'], item['7d']),
}))
}
},
mounted() {
this.getBitcoinData()
}
}
</script>
have you tried to import your icons inside the template area with <i ...></i>?
here is an working example. Check out the cdn.fontawesome/help-page to get more information.
Vue.createApp({
data () {
return {
isPositive: false,
isNegative: true
}
}
}).mount('#demo')
.negative {
color: red;
}
.positive {
color: green;
}
.neutral {
color: #666;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" rel="stylesheet"/>
<script src="https://unpkg.com/vue#next"></script>
<div id="demo">
<i class="fas" :class="isPositive ? 'fa-angle-up positive' : isNegative ? 'fa-angle-down negative' : 'fa-minus neutral' "></i>
<br>
<br>
<button #click="isPositive = !isPositive; isNegative = !isNegative" v-text="'change pos and neg'" />
</div>
so basically you'll bind the icon classes to your own conditions. You could write the conditions for example with the tenary operator into your template area. Hope you get the idea.
Host Fontawesome yourself by following the steps in this Fontawesome documentation.
https://fontawesome.com/docs/web/setup/host-yourself/webfonts
i hope this help.
The component has the following binding:
<div
class="columns dropdowns"
:style="{
height: `${dropdownsgridheight}px`,
}"
>
That height is getting calculated based on count of dropdowns. On mobile screens, it shows poorly. So I need to remove this style binding when screen height is less than 812px. How can it be done correctly in Vue.js?
Try this:
<div
class="columns dropdowns"
:style="finalHeight"
></div>
and in your component:
data() {
return {
finalHeight: ''
}
},
created() {
window.addEventListener("resize", this.myEventHandler);
},
destroyed() {
window.removeEventListener("resize", this.myEventHandler);
},
methods: {
myEventHandler() {
if(window.innerHeight > 812) {
this.finalHeight = {
"height": this.dropdownsgridheight + 'px'
}
}
else {
this.finalHeight = '15px'
}
},
},
computed: {
dropdownsgridheight () {
return '50'
}
},
mounted() {
this.myEventHandler()
}
here is an example: https://jsfiddle.net/Nanif/yvw2h5pe/1/
Position: sticky doesn't support by the most mobile browsers. But position: fixed is not that thing I need (because of fixed block overlaps content in the bottom of document).
I guess for jquery it will be easy to set static position for fixed block if we get bottom of document onscroll.
But for Vue2 I haven't any idea how to do the same. Give some advice please. Or maybe better solution exists.
As I mentioned in the comments, I'd recommend using a polyfill if at all possible. They will have put a lot of effort into getting it right. However, here is a simple take on how you might do it in Vue.
I have the application handle scroll events by putting the scrollY value into a data item. My sticky-top component calculates what its fixed top position would be, and if it's > 0, it uses it. The widget is position: relative.
new Vue({
el: '#app',
data: {
scrollY: null
},
mounted() {
window.addEventListener('scroll', (event) => {
this.scrollY = Math.round(window.scrollY);
});
},
components: {
stickyTop: {
template: '<div class="a-box" :style="myStyle"></div>',
props: ['top', 'scrollY'],
data() {
return {
myStyle: {},
originalTop: 0
}
},
mounted() {
this.originalTop = this.$el.getBoundingClientRect().top;
},
watch: {
scrollY(newValue) {
const rect = this.$el.getBoundingClientRect();
const newTop = this.scrollY + +this.top - this.originalTop;
if (newTop > 0) {
this.$set(this.myStyle, 'top', `${newTop}px`);
} else {
this.$delete(this.myStyle, 'top');
}
}
}
}
}
});
#app {
height: 1200px;
}
.spacer {
height: 80px;
}
.a-box {
display: inline-block;
height: 5rem;
width: 5rem;
border: 2px solid blue;
position: relative;
}
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<div class="spacer"></div>
<div class="a-box"></div>
<sticky-top top="20" :scroll-y="scrollY"></sticky-top>
<div class="a-box"></div>
</div>
This seem to work for me
...
<header
ref="header"
class="header-container"
:class="{ 'header-container--sticky': isHeaderSticky }"
>
...
...
data() {
return{
scrollY: null,
headerTop: 0,
isHeaderSticky: false,
}
},
mounted() {
window.addEventListener('load', () => {
window.addEventListener('scroll', () => {
this.scrollY = Math.round(window.scrollY);
});
this.headerTop = this.$refs.header.getBoundingClientRect().top;
});
},
watch: {
scrollY(newValue) {
if (newValue > this.headerTop) {
this.isHeaderSticky = true;
} else {
this.isHeaderSticky = false;
}
}
}
...
...
.header-container {
&--sticky {
position: sticky;
top: 0;
z-index: 9999;
}
}
...