What is the best way to create a grid dynamically with Vue?
I was thinking of using the v-for loop to generate cards on a page for X number of 'modules', but would like to arrange them in rows of 3.
e.g
Card | Card | Card
Card | Card | Card
You can use CSS grid layout, make sure to check browser support.
We'll need to make the container a grid container using display: grid and use grid-template-columns.
You can create a component that accepts a number prop and then use it in the repeat() notation.
repeat({numberOfColumns}, minmax({minColumnSize}, {maxColumnSize}))
new Vue({
el: "#app",
data() {
return {
cards: [1, 2, 3, 4],
numberOfColumns: 3,
}
},
computed: {
gridStyle() {
return {
gridTemplateColumns: `repeat(${this.numberOfColumns}, minmax(100px, 1fr))`
}
},
},
methods: {
addCard() {
this.cards.push('new-card')
},
},
})
Vue.config.productionTip = false;
Vue.config.devtools = false;
.card-list {
display: grid;
grid-gap: 1em;
}
.card-item {
background-color: dodgerblue;
padding: 2em;
}
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
ul {
list-style-type: none;
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.js"></script>
<div id="app">
Columns: <input v-model.number="numberOfColumns">
<ul :style="gridStyle" class="card-list">
<li v-for="(card, index) in cards" class="card-item">
{{ index + 1 }}
</li>
</ul>
<button #click="addCard">
Add card
</button>
</div>
Related
How can I change css width from 50% to 100 % when click the button see more detail here >>> Sample sandbox
<template>
<div id="theSpecial">Hello World Special</div>
<button #click="changeWidth">Change width</button>
</template>
<script>
export default {
data() {
return {
testBoolean: false,
};
},
methods: {
changeWidth() {
this.testBoolean = true;
//change width to 100%
},
},
};
</script>
CSS
#theSpecial {
background-color: purple;
color: white;
width: 50%;
}
You have to make some change on your code
First of all add this to your css
.theSpecial{width:50%}
.fullWidth{width:100%}
To toggle the full width modify the method
changeWidth() {
this.testBoolean = !this.testBoolean;
//this will toggle the width on every click
},
and then use this in your component template
<div class="theSpecial" v-bind:class="{fullWidth:testBoolean}">
N.B. change the id into class, beacuse id has more css specifity.
This will toggle the class full width accordly to the value of testBoolean.
This is your Sandbox
Here you can find documentation about class binding
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<div id="theSpecial" :class="{ 'full-width': testBoolean }">
Hello World Special
</div>
<button #click="changeWidth">Change width</button>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
},
data() {
return {
testBoolean: false,
};
},
methods: {
changeWidth() {
this.testBoolean = true;
},
},
};
</script>
#theSpecial {
background-color: purple;
color: white;
width: 50%;
}
#theSpecial.full-width {
width: 100%;
}
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
data() {
return {
testBoolean: false,
};
},
methods: {
changeWidth() {
this.testBoolean = !this.testBoolean;
//change width to 100%
},
},
.theSpecial {
background-color: purple;
color: white;
width: 50%;
}
.fullwidth {
background-color: purple;
color: white;
width: 100%;
}
<div :class="(this.testBoolean === true)? 'fullwidth':'theSpecial'">Hello World Special</div>
<button #click="changeWidth">Change width</button>
So I'm trying to make a simple dice roller and want to add a spin animation to make it look cool. For some reason, the animation only works on the initial roll, and not on any after that (unless I refresh). Here is what I have:
App.tsx
export default function App() {
const [dice, setDice] = useState<string[]>([]);
return (
<div className="container">
<h1 className="title-text">Dice Roller</h1>
<div className="roll-dice-button">
<RollBtn setDice={setDice} />
</div>
<Dice dice={dice} />
</div>
)
}
RollBtn.tsx
type Props = {
setDice: (s: string[]) => void
}
export default function RollBtn({setDice}: Props) {
const roll = () => {
let dice: string[] = []'
for(let i = 0; i < 2; i++) {
dice.push(Math.round(Math.random() * 5 + 1));
}
setDice(dice);
}
return <button onClick={() => roll()}>Roll Dice</button>;
}
Dice.tsx
type Props = {
dice: string[];
}
export default function Dice({ dice }: Props) {
return (
<div className="dice-container">
{dice.map((d, i) => (
<div className="die" key={i}>{d}</div>
))}
</div>
)
}
styles.scss
.dice-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 1em;
justify-items: center;
align-items: center;
.die {
width: 2em;
height: 2em;
background-color: white;
border-radius: 8px;
color: black;
font-weight: 900;
font-size: 2em;
line-height: 2em;
text-align: center;
animation: spin_dice .25s;
}
}
As I previously stated, on the initial roll it will do the animation, but after that, it will just change the numbers within the dice.
So, if the animation is not a loop, it will be executed just once when the component is rendered. So you need to change the class of the div .die for "" and then change it back to .die when you click on the roll button.
For that you should put the RollBtn and Dice in just one component.
I need to make the vertical slider animation ( dots and line ) as in this pic
i managed to do the Accordion and the dots but i don't know how i will going to implement it ( i'm using pseudo )
**my accordion component Where i define the logic of my nested accordions as in images based on array of data **
function MultiLevelAccordion({
data,
bodyClass,
headerClass,
wrapperClass,
renderHeader,
renderContent,
}) {
const RootAccordionId = 'parent-0';
const [accordionsStates, setActiveCardsIndex] = useMergeState({});
const onAccordionToggled = (id, activeEventKey) => {
console.log(activeEventKey);
setActiveCardsIndex({
[id]: activeEventKey ? Number(activeEventKey) : activeEventKey
});
};
console.log('data', data);
const accordionGenerator = (data, parentId) => {
return map(data, (item, index) => {
const active = accordionsStates[parentId] === index;
const hasChildren = item.hasOwnProperty('children') && isArray(item.children) && !isEmpty(item.children);
const isRootAccordion = RootAccordionId === parentId;
const isLastNestedAccordion = !isRootAccordion && !hasChildren;
const accordion = (
<Card className={classNames(wrapperClass, {
'nested-root-accordion': !isRootAccordion,
'last-nested-root-accordion': isLastNestedAccordion,
'multi-level-accordion': !isLastNestedAccordion
})}
>
<Accordion.Toggle
{...{ ...item.id && { id: item.id } }}
onClick={() => this}
as={Card.Header}
eventKey={`${index}`}
className={'cursor-pointer d-flex flex-column justify-content-center'}
>
<div className="d-flex justify-content-between align-items-center">
{renderHeader(item, hasChildren)}
<img
style={{
transition: 'all .5s ease-in-out',
transform: `rotate(${active ? 180 : 0}deg)`
}}
src={setIcon('arrow-down')}
className="ml-2"
alt="collapse"
/>
</div>
</Accordion.Toggle>
<Accordion.Collapse eventKey={`${index}`}>
<Card.Body
className={`accordion-content-wrapper ${!hasChildren ? 'accordion-children-body' : ''} ${bodyClass}`}
>
{!hasChildren ? renderContent(item, hasChildren) : (
<Accordion onSelect={activeEventKey => onAccordionToggled(`${parentId}-${index}`, activeEventKey)}>
<Fade cascade top when={active}>
{accordionGenerator(item.children, `${parentId}-${index}`)}
</Fade>
</Accordion>
)}
</Card.Body>
</Accordion.Collapse>
</Card>
);
return isRootAccordion ? accordion : (
<div className={'d-flex align-items-center'}>
{accordion}
<div className="accordion-indicator-wrapper">
<div className="accordion-indicator" id={`indicator-${parentId}-${index}`} />
</div>
</div>
);
});
};
if (!isArray(data)) {
return;
}
return (
<Accordion onSelect={activeEventKey => onAccordionToggled(RootAccordionId, activeEventKey)}>
{accordionGenerator(data, RootAccordionId)}
</Accordion>
);
}
export default MultiLevelAccordion;
the styles used in scss
.faqs-questions-wrapper {
padding: 20px 10px
}
.faqs-q-count {
color: $black-color;
font-size: calc(1rem - 1rem/8)
}
.faqs-q-a-wrapper {
flex-basis: 95%;
}
.faq-child-title {
color: $black-color
}
.nested-root-accordion {
flex-basis: 90%;
}
.accordion-indicator-wrapper {
flex-basis: 10%;
width: 100%;
display: flex;
justify-content: center;
.accordion-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: $theme-color;
position: relative;
}
}
Any clue?
Thanks in Advance.
React JS is gonna make this easy
The lines expansion will need to be coded based on the height of the box window
For the dropdown container keep the vertical button lines in a separate div than the Accordian
Check out this pen for creating lines between buttons
https://codepen.io/cataldie/pen/ExVGjya
css part:
.status-container{
padding:10px;
margin:10px;
position:relative;
display: inline-block;
}
.bullet{
padding:0px;
margin:0px;
display: inline-block;
z-index: 10;
}
.bullet:before {
content: ' \25CF';
font-size: 5em;
}
.bullet-before{
/*position:relative;
right:-12px;*/
}
.bullet-after{
/*position:relative;
left:-30px;*/
}
.line{
stroke:blue;
stroke-width:0.3em;
padding:0px;
margin:0px;
display: inline-block;
}
.line-on{
stroke:red;
}
.line-off{
stroke:gray;
}
.color-on{
color: red;
}
.color-off{
color: gray;
}
https://codepen.io/emwing/pen/LgzJOx
I think you can use some inspiration here
This is the HTML code of my mat-card list in my Angular application:
<div [ngClass]="verticalResultsBarStyle">
<div class="innerDiv">
<mat-card [ngClass]="barStyle" *ngFor="let card of obs | async | paginate: { id: 'paginator', itemsPerPage: 10, currentPage: currentPage, totalItems: totalItems }" (click)="highlightCard()">
<mat-card-header>
<mat-card-title>{{card[0].title}}</mat-card-title>
<mat-card-subtitle>{{card[0].subtitle}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p>{{card[0].info1}}</p>
<p>{{card[0].info2}}</p>
<p>{{card[0].info3}}</p>
<p>{{card[0].info4}}</p>
<p>{{card[0].info5}}</p>
</mat-card-content>
</mat-card>
<pagination-controls (pageChange)="OnPaginateChange($event)" id="paginator"></pagination-controls>
</div>
</div>
this is the CSS:
.verticalResultsBarContainerEnabled {
background-color: #eeeeee;
width: 16%;
float: right;
overflow: visible;
}
.verticalResultsBarContainerDisabled {
display: none;
}
.card {
margin-left: 10px;
margin-right: 10px;
margin-bottom: 10px;
}
.cardHighlight {
margin-left: 10px;
margin-right: 10px;
margin-bottom: 10px;
background-color: #c12400;
color: white;
}
.innerDiv {
height: 755px;
overflow: auto;
}
.paginator {
margin: auto;
width: 92%;
}
and finally this is a snippet of TS :
highlightCard() {
if (this.barStyle === 'card') {
this.barStyle = 'cardHighlight';
} else {
this.barStyle = 'card';
}
}
I would to click on a single mat card and highlight only the selected mat card, but when I click on a single card, the entire list changes color. How can I fix that?
You need to use index for each of your cards. Your issue is normal.
I see barStyle is one generall for all your cards. You need to update barStyle for each card, when your cards come from backend.
First, in your .ts :
service.subscribe(cards => {
let i = 0
cards.forEach(card => {
this.barStyle[i] = // your default barStyle;
i++;
});
If you have an id for each car, better, you can avoid to use i.
then, in template:
<mat-card [ngClass]="barStyle[i]" *ngFor="let card of obs; let i = index | async | paginate: { id: 'paginator', itemsPerPage: 10, currentPage: currentPage, totalItems: totalItems }" (click)="highlightCard(i)">
I see you use async function in template. So, in ts, you expect and Observable. To update style of each card, you need to subscribe at your service and update objects. So, need to do some changes:
cards: Observable will become cards: Card[];
loadingData = true;
call the service as:
getData() {
this.loadingData = true;
service().subscribe(cards => {
cards.forEach(card => {
this.barStyle[card.id] = // your default barStyle;
this.cards = cards;
this.loadingData = false; // this is set false only after data is loaded
});
}
highlightCard(id: number) {
if (this.barStyle[id] === 'card') {
this.barStyle[id] = 'cardHighlight';
} else {
this.barStyle[id] = 'card';
}
}
in template:
<mat-card ***ngIf="!loadingData"**
[ngClass]="barStyle[card.id]"
*ngFor="let card of obs | async |
paginate: { id: 'paginator',
itemsPerPage: 10,
currentPage: currentPage,
totalItems: totalItems }"
(click)="highlightCard(card.id)">
Somethimes, your template will render before to load data, so you need to use a boolean to wait for it.
**To keep clean code, for pagination you can use a variable initialized in .ts*
I prepared example of image slider what I need.
I encounter with styling issue when using images with various dimensions. When element leaving the array, its location is set as absolute value, what is necessary for smooth transition, tho, but the image is also moved up.
I would like to have nice vertical align into middle even leave or enter the array, but could not get on any way.
Another issue, what I would like to solve is when left the window and then went back after a while. The animation running all cycles at once to reach current state instead just stop animation and run after. Maybe it is my responsibility, but browsers doesn't offer nice event to catch blur window or am I wrong?
According to this discussion
Thank you for any ideas.
let numbers = [{key:1},{key:2},{key:3},{key:4},{key:5},{key:6},{key:7}]
let images = [
{ key:1,
src:"http://lorempixel.com/50/100/sports/"},
{ key:2,
src:"http://lorempixel.com/50/50/sports/"},
{ key:3,
src:"http://lorempixel.com/100/50/sports/"},
{ key:4,
src:"http://lorempixel.com/20/30/sports/"},
{ key:5,
src:"http://lorempixel.com/80/20/sports/"},
{ key:6,
src:"http://lorempixel.com/20/80/sports/"},
{ key:7,
src:"http://lorempixel.com/100/100/sports/"}
]
new Vue({
el: '#rotator',
data: {
items: images,
lastKey: 7,
direction: false
},
mounted () {
setInterval(() => {
if (this.direction) { this.prevr() } else { this.nextr() }
}, 1000)
},
methods: {
nextr () {
let it = this.items.shift()
it.key = ++this.lastKey
this.items.push(it)
},
prevr () {
let it = this.items.pop()
it.key = ++this.lastKey
this.items.unshift(it)
}
}
})
.litem {
transition: all 1s;
display: inline-block;
margin-right: 10px;
border: 1px solid green;
background-color: lightgreen;
padding: 10px 10px 10px 10px;
height: 100px;
}
.innerDiv {
border: 1px solid red;
}
.container {
overflow: hidden;
white-space: nowrap;
height: 250px;
border: 1px solid blue;
background-color: lightblue;
}
.list-enter {
opacity: 0;
transform: translateX(40px);
}
.list-leave-to {
opacity: 0;
transform: translateX(-40px);
}
.list-leave-active {
position: absolute;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.11/vue.js"></script>
<div id="rotator">
<button #click="direction = !direction">
change direction
</button>
<transition-group
name="list"
tag="div"
class="container">
<div
v-for="item in items"
:key="item.key" class="litem">
<!--
<div
class='innerDiv'>
{{ item.key }}
</div>
-->
<div class='innerDiv'>
<img :src='item.src'>
</div>
</div>
</transition-group>
</div>
It tooks a while but on the end I think that I have got better result for sliding animation with changing direction feature.
One annoying think is when I swithing the sliding direction so animation is for a 'microsecond' changed to next state and than return to correct one, after it the animation continue as expected. It is happening only in one direction and I don't know how to fix it. Also last box behave differently too only once. No clue at all.
So just 98% solution :-)
let images = [
{key:1, domKey:1, src:"http://lorempixel.com/50/100/sports/" },
{key:2, domKey:2, src:"http://lorempixel.com/50/50/sports/" },
{key:3, domKey:3, src:"http://lorempixel.com/100/50/sports/" },
{key:4, domKey:4, src:"http://lorempixel.com/20/30/sports/" },
{key:5, domKey:5, src:"http://lorempixel.com/80/20/sports/" },
{key:6, domKey:6, src:"http://lorempixel.com/20/80/sports/" },
{key:7, domKey:7, src:"http://lorempixel.com/100/100/sports/" }
]
let setPositionRelative = el => el.style.position = "relative"
new Vue({
el: '#rotator',
data: {
items: images,
lastKey: 7,
direction: true,
changeDirectionRequest: false
},
mounted () {
Array.from(this.$el.querySelectorAll("div[data-key]")).map(setPositionRelative)
setInterval(() => {
if(this.changeDirectionRequest) {
this.changeDirectionRequest = false
this.direction = !this.direction
if (this.direction)
Array.from(this.$el.querySelectorAll("div[data-key]")).map(setPositionRelative)
else
Array.from(this.$el.querySelectorAll("div[data-key]")).map(el => el.style.position = "")
}
if (this.direction) this.prevr()
else this.nextr()
}, 1000)
},
methods: {
nextr () {
let it = this.items.shift()
it.key = ++this.lastKey
this.items.push(it)
},
prevr () {
let it = this.items.pop()
it.key = ++this.lastKey
this.items.unshift(it)
setPositionRelative(this.$el.querySelector("div[data-domkey='"+it.domKey+"']"))
}
}
})
.container {
overflow: hidden;
white-space: nowrap;
height: 200px;
border: 1px solid blue;
background-color: lightblue;
display: flex;
align-items: center;
}
.innerDiv {
border: 1px solid red;
width: auto;
height: auto;
display:-moz-box;
-moz-box-pack:center;
-moz-box-align:center;
display:-webkit-box;
-webkit-box-pack:center;
-webkit-box-align:center;
display:box;
box-pack:center;
box-align:center;
}
.litem {
transition: all 1s;
margin-right: 10px;
border: 1px solid green;
background-color: lightgreen;
padding: 10px 10px 10px 10px;
}
.list2-enter, .list-enter {
opacity: 0;
transform: translateX(40px);
}
.list2-leave-to, .list-leave-to {
opacity: 0;
transform: translateX(-40px);
}
.list-leave-active {
position: absolute;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.11/vue.js"></script>
<div id="rotator">
<button #click="changeDirectionRequest = true">change direction</button>
<transition-group name="list" tag="div" class="container">
<div v-for="item in items"
:key="item.key"
:data-domkey="item.domKey"
class="litem">
<div class='innerDiv'>
<img :src='item.src'>
</div>
</div>
</transition-group>
</div>