Vue/Vuetify - breakpoints based on component size - vue.js

As far as I can see, Vuetify allows you to define breakpoints based on viewport size. Is it also possible to define breakpoints based on the size of the component? E.g.
when several components are shown on an overview page, it would use a more "compact" layout to be able to show the components side by side on large screens.
when only one component is shown it could take up more space (on large screens).
the "compact" layout could also be used when only one component is shown on a small screen.
on small screens, the overview page could show several components vertically rather than side by side.
Or can you recommend a better approach to this?

As far as I know, there is no such feature in Vuetify. However, you can always hide v-col containing the component and other columns will take up the freed-up space.
For example:
https://codepen.io/olegnaumov/pen/rNpzvEX

Since you cannot rely on global breakpoints (at least for changes based on the number of components), you can pass your component a layout prop that dictates the internal layout of the component.
Widget component
<template>
<div>
<div>Widget</div>
<v-row :class="{ 'flex-column': layout === 'vertical' }">
<v-col> Column 1 </v-col>
<v-col> Column 2 </v-col>
<v-col v-if="layout !== 'compact'"> Column 3 </v-col>
<v-col v-if="layout !== 'compact'"> Column 4 </v-col>
</v-row>
</div>
</template>
<script>
export default {
name: "Widget",
props: {
layout: {
type: String,
default: "normal", // Options: ['normal', 'vertical', 'compact']
},
},
};
</script>
Then, you can make use of two directives provided by Vuetify to compute the layout of the child components and parent container: v-mutate to watch for changes in the number of children (using the child modifier) and v-resize to watch for resize of the app.
Parent component
<template>
<v-app v-resize="onResize">
<v-container fluid>
<v-row
ref="widget_container"
v-mutate.child="onMutate"
:class="{ 'flex-column': layout_container === 'vertical' }"
>
<template v-for="n in show">
<v-col :key="n">
<widget :layout="layout_widget"></widget>
</v-col>
</template>
</v-row>
</v-container>
</v-app>
</template>
<script>
import Widget from "/src/components/widget.vue";
export default {
name: "App",
components: {
Widget,
},
data() {
return {
show: 2, // Any other method to change the number of widgets shown
// See the codesanbox for one of them
container_width: null,
widget_width: null,
// Tweak below values accordingly
container_breackpoints: {
400: "vertical",
900: "normal",
},
widget_breackpoints: {
200: "compact",
500: "normal",
},
};
},
computed: {
layout_container() {
let breackpoint = Object.keys(this.container_breackpoints)
.sort((a, b) => b - a)
.find((k) => this.container_width > k);
return this.container_breackpoints[breackpoint] || "normal";
},
layout_widget() {
if (this.layout_container === "vertical") return "vertical";
let breackpoint = Object.keys(this.widget_breackpoints)
.sort((a, b) => b - a)
.find((k) => this.widget_width > k);
return this.widget_breackpoints[breackpoint] || "normal";
},
},
methods: {
setLayout() {
this.container_width = this.$refs.widget_container.clientWidth;
this.widget_width =
this.container_width / this.$refs.widget_container.children.length;
},
onMutate() {
this.setLayout();
},
onResize() {
this.setLayout();
},
},
};
</script>
Note, this is not a production-ready solution, but rather a proof of concept showing the use of v-mutate and v-resize for this purpose.
You can see this Codesanbox for demonstration.

You can write your own, using useResizeObserver and applying size classes on the element, which can in turn be used to modify the CSS rules to itself and its descendants:
Example:
const { createApp, ref } = Vue;
const { createVuetify } = Vuetify;
const vuetify = createVuetify();
const { useResizeObserver } = VueUse;
const app = createApp({
setup() {
const el = ref(null)
const size = ref('')
useResizeObserver(el, (entries) => {
const { width } = entries[0].contentRect;
size.value =
width < 600
? "xs"
: width < 960
? "sm"
: width < 1264
? "md"
: width < 1904
? "lg"
: "xl";
});
return { el, size };
},
});
app.use(vuetify).mount("#app");
.xs span {
color: orange;
}
.sm span {
color: red;
}
.md span {
color: blue;
}
.lg span {
color: green;
}
<script src="https://cdn.jsdelivr.net/npm/vue#3.2.45/dist/vue.global.prod.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify#3.1.1/dist/vuetify.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/vuetify#3.1.1/dist/vuetify.min.css" rel="stylesheet"/>
<script src="https://unpkg.com/#vueuse/shared"></script>
<script src="https://unpkg.com/#vueuse/core"></script>
<div id="app">
<v-app>
<v-main class="h-100">
<v-row no-gutters class="h-100" style="background: #f5f5f5">
<v-col
class="h-100"
cols="12"
sm="6"
lg="9"
>
<v-card class="h-100 pa-2" :class="size" ref="el">
v-card: {{ size }} <br>
window:
<span class="d-inline-block d-sm-none">xs</span>
<span class="d-none d-sm-inline-block d-md-none">sm</span>
<span class="d-none d-md-inline-block d-lg-none">md</span>
<span class="d-none d-lg-inline-block d-xl-none">lg</span>
<span class="d-none d-xl-inline-block">xl</span>
</v-card>
</v-col>
</v-row>
</v-main>
</v-app>
</div>

Related

Vuetify v-intersect in Nuxt app fires before enters view

I have a page with the following layout:
<template>
<v-container fluid class='pa-0 ma-0 assignment-container'>
<v-row class='pa-0 ma-0 gallery-bg'>
// ...v-img with height 60vh
</v-row>
<v-row class='pa-3'>
// ...row content
</v-row>
<v-row class='pa-3'>
// ... row content
</v-row>
<v-row
v-if='!works.length'
v-intersect='onIntersect'
class='pa-3 mt-4 flex-column'>
<v-row class='pa-3 ma-0'>
<h3 class='mb-4 acumin-semibold section-title section-title-h3'>
Works:
</h3>
</v-row>
<v-row class='pa-0 ma-0'>
<v-col
v-for='(skeleton, i) in skeletonCards'
:key='i'
xs='12'
sm='6'
md='4'
lg='3'>
<v-skeleton-loader
class='mx-auto'
max-width='374'
height='250'
type='card' />
</v-col>
</v-row>
</v-row>
<v-container v-else fluid>
<v-row class='pa-3 ma-0'>
<h3 class='mb-4 acumin-semibold section-title section-title-h3'>
Works:
</h3>
</v-row>
<v-row class='pa-0 ma-0'>
<v-col
v-for='work in works'
:key='work._id'
xs='12'
sm='6'
md='4'
lg='3'>
<WorkCard :assignment-id='$route.params.id' :work='work' />
</v-col>
</v-row>
</v-container>
</v-container>
</template>
<script>
// ...all the imports
export default {
components: {WorkCard, UIButton},
async asyncData(context) {
// ... fetch and return assignment
},
data() {
return {
works: []
}
},
computed: {
skeletonCards() {
return this.$vuetify.breakpoint.lg ? 4 : 3
}
},
methods: {
async fetchWorks() {
this.works = await this.$nuxt.context.app.apolloProvider.defaultClient.query({
query: worksByAssignmentIdQuery,
variables: {
id: this.$nuxt.context.route.params.id,
}
})
.then(({data}) => data.assignmentWorks)
.catch(err => console.error(err))
},
onIntersect() {
console.log('intersect fired')
this.fetchWorks()
}
},
}
</script>
The problem is that v-intersect directive fires even when it's not in the view yet.
I tried to define threshold:
v-intersect='{
handler: onIntersect,
options: {
threshold: [1.0]
}
}'
And it keeps firing.
Then I thought maybe it's because it's rendered on the server, so I tried to wrap this part of markup in <client-side> element. Still firing. I tried to wrap the entire page in that element, tried to put an empty <p> element after all the rest and apply that directive on it - and it still fired.
I had the fetching part inside fetch() method with fetchOnServer set to false and I called this.$fetch() in my onIntersect method. And it kept firing every time. As if this row is always in the view, even though it is not.
I ran out of ideas... Any help, please?
OK, looks like I finally solved it. First of, apparently you can't place the v-intersect directive on a v-row element.
So I created an invisible div element with 0px width and height, on which I applied the v-intersect directive:
...
<div
v-intersect='{
handler: onIntersect,
options: {
threshold: [1.0]
}
}'
class='invisible' />
...
<style scoped>
.invisible {
width: 0;
height: 0;
position: absolute;
bottom: 0;
}
<style>
Thein in my onIntersect method I'm passing isIntersecting parameter, and invoking the fetchWorks method if it's true:
onIntersect(entries, observer, isIntersecting) {
if (isIntersecting) {
this.fetchWorks()
}
}
Now it intersects correctly and fetching data when the view (div) is in viewport (even though it's invisible), whether when scrolled there or the page was refreshed on that pixel.
Since I'll need to use v-intersect in other parts of my project, I'm considering turning this div into a component, to which I'll pass the intersect callback function.

Render Component in loop, use Index in method of child component (VueJS)

I have two components where some exchange of props takes place. Props is the whole todo array, which is updated by a click on the button with the "addTodo" method. Passing the array down to the child works fine. I can display the props dynamically in my p-tags, but it seems to be not possible to use it my the methods of my child component.
<template>
<v-app>
<v-content>
<h2>Add a Todo</h2>
<v-col cols="12" sm="6" md="3">
<v-text-field label="Regular" v-model="text"></v-text-field>
</v-col>
<div class="my-3">
<v-btn medium #click="addTodo">Add Todo</v-btn>
</div>
<div v-for="(todo, index) in todos" v-bind:key="index">
<HelloWorld
v-bind:todos="todos"
v-bind:index="index"
v-bind:class="(todos[index].done)?'green':'red'"
/>
</div>
</v-content>
</v-app>
</template>
<script>
import HelloWorld from "./components/ToDo.vue";
export default {
components: {
HelloWorld
},
data: function() {
return {
text: "",
todos: []
};
},
methods: {
addTodo() {
this.todos.push({
text: this.text,
done: false
});
}
}
};
</script>
This is my child component
<template>
<v-card max-width="250">
<v-card-text>
<h2 class="text-center">{{todos[index].text}}</h2>
<p class="display-1 text--primary"></p>
<p>{{index}}</p>
</v-card-text>
<v-card-actions>
<v-btn text color="deep-purple accent-4" #click="done"></v-btn>
<v-btn text color="orange accent-4">Delete Task</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
export default {
props: ["todos", "index"],
methods: {
done() {
this.todos[1].text = "bla";
}
}
};
</script>
<style scoped>
.seperator {
display: flex;
justify-content: space-between;
}
</style>
I pass a whole array with objects as props, and using the index inside the p-tag works fine, but I also want to use it like this:
methods: {
done() {
this.todos[index].text = "bla";
}
}
'index' is not defined
Everything works fine, but I am not able use the index value inside the method. What am I doing wrong here?
The way you write it out, there is nothing in scope defining index. Where is that value coming from?
Index is a prop and so it must be referenced with this.
done () {
this.todos[this.index].text = 'bla'
}

vuetify tabs component doesnt work correctly with flex animation

I'm trying to get the v-tabs to work with my expand menu.
Basically when I click the toggle open, the right side menu will slide out, and inside this menu I want to use the tabs component from vuetify.
It doesn't seem to work, when clicking on the tabs, it's jumping all over the places.
It starts to work correctly when I resize the window manually. Any help please?
Here's the codepen
codepen.io/anon/pen/WmKQLp
You should be able to use a Navigation Drawer without any custom styling needed... (Vuetify has built in components for what you're trying to accomplish)..
Here is a 'quick and dirty' pseudo example showing how you can accomplish this:
Codepen Example can be found here. updated with resizing ability
EDIT:
If you did want to use your custom CSS, you will need to add an additional custom CSS class - this is happening because of the translate, among other Vuetify styles conflicting with your custom CSS...
As outlined here, add this class: (I highly advise against doing this)
.v-tabs__container {
transform: translateX(0px)!important;
}
HTML
<div id="app">
<v-app>
<v-navigation-drawer app right width="550" v-model="navigation.shown">
<v-toolbar color="primary">
<v-toolbar-title class="headline text-uppercase">
<span>t a</span><span class="font-weight-light"> B S </span>
</v-toolbar-title>
</v-toolbar>
<v-tabs>
<v-tab v-for="n in 3" :key="n">
Item {{ n }}
</v-tab>
<v-tab-item v-for="n in 3" :key="n">
<v-card flat>
<v-card-text>Content for tab {{ n }} would go here</v-card-text>
</v-card>
</v-tab-item>
</v-tabs>
</v-navigation-drawer>
<v-layout justify-center>
<v-btn #click="navigation.shown = !navigation.shown">Toggle {{ direction }}</v-btn>
</v-layout>
</v-app>
</div>
JS/Vue
new Vue({
el: "#app",
data: () => {
return {
navigation: {
shown: false,
}
};
},
computed: {
direction() {
return this.navigation.shown === false ? "Open" : "Closed"
}
},
});
EDIT: (with resizing ability)
HTML:
<div id="app">
<v-app>
<v-navigation-drawer
ref="drawer"
app
right
:width="navigation.width"
v-model="navigation.shown"
>
<v-toolbar color="primary">
<v-toolbar-title class="headline text-uppercase">
<span>t a</span><span class="font-weight-light"> b s </span>
</v-toolbar-title>
</v-toolbar>
<v-tabs>
<v-tab v-for="n in 3" :key="n">
Item {{ n }}
</v-tab>
<v-tab-item v-for="n in 3" :key="n">
<v-card flat>
<v-card-text>Content for tab {{ n }} would go here</v-card-text>
</v-card>
</v-tab-item>
</v-tabs>
</v-navigation-drawer>
<v-layout justify-center>
<v-btn #click="navigation.shown = !navigation.shown">Toggle {{ direction }}</v-btn>
</v-layout>
</v-app>
</div>
JS/Vue:
new Vue({
el: "#app",
data: () => {
return {
navigation: {
shown: false,
width: 550,
borderSize: 3
}
};
},
computed: {
direction() {
return this.navigation.shown === false ? "Open" : "Closed";
}
},
methods: {
setBorderWidth() {
let i = this.$refs.drawer.$el.querySelector(
".v-navigation-drawer__border"
);
i.style.width = this.navigation.borderSize + "px";
i.style.cursor = "ew-resize";
},
setEvents() {
const minSize = this.navigation.borderSize;
const el = this.$refs.drawer.$el;
const drawerBorder = el.querySelector(".v-navigation-drawer__border");
const vm = this;
const direction = el.classList.contains("v-navigation-drawer--right")
? "right"
: "left";
function resize(e) {
document.body.style.cursor = "ew-resize";
let f = direction === "right"
? document.body.scrollWidth - e.clientX
: e.clientX;
el.style.width = parseInt(f) + "px";
}
drawerBorder.addEventListener(
"mousedown",
function(e) {
if (e.offsetX < minSize) {
el.style.transition = "initial";
document.addEventListener("mousemove", resize, false);
}
},
false
);
document.addEventListener(
"mouseup",
function() {
el.style.transition = "";
vm.navigation.width = el.style.width;
document.body.style.cursor = "";
document.removeEventListener("mousemove", resize, false);
},
false
);
}
},
mounted() {
this.setBorderWidth();
this.setEvents();
}
});

Vue + Vuetify expansion panel enter/leave animation

I'm currently developing a webapp in Vue.js with the help of Vuetify.
I found the expansion panel element very usefull and I wanted to use it in my website.
So I easily get the data i want to display from Firebase and i proceed to load the items with this template:
<v-expansion-panel popout>
<v-expansion-panel-content
v-for="doc in filteredclienti"
:key="doc.id">
<div slot="header">{{doc.data().Nome}} {{doc.data().Cognome}}</div>
<v-card>
Card content
</v-card>
</v-expansion-panel-content>
</v-expansion-panel
Everything works fine, the panel is ok and the popup animation works fine too.
Now I'd like to display a simple enter/leave animation to each item.
I tried to add a <transition-group> tag after the <v-expansion-panel popuot> but the console tells me want only a <v-expansion-panel-content> element.
So i tried to add <transition-group> inside <v-expansion-panel-content> but in this case the layout is no more correct and the popup animation is not working anymore.
How can i do it? Thanks!
That should do the trick, I've added a delete button, that will slide the "deleted" doc out.
enjoy.
<template>
<v-app>
<v-expansion-panel popout>
<transition-group name="list" tag="v-expansion-panel">
<v-expansion-panel-content v-for="doc in filteredclienti" :key="doc.id">
<div slot="header">
<span>{{doc.data}}</span>
<v-btn class="error" #click="deleteItem(doc.id)">
<v-icon>delete</v-icon>
</v-btn>
</div>
<v-card>Card content</v-card>
</v-expansion-panel-content>
</transition-group>
</v-expansion-panel>
</v-app>
</template>
<script>
export default {
data: () => ({
filteredclienti: [
{ id: 1, data: "data1" },
{ id: 2, data: "data1" },
{ id: 3, data: "data1" },
{ id: 4, data: "data1" }
]
}),
methods: {
deleteItem(id) {
this.filteredclienti = this.filteredclienti.filter(d => d.id !== id);
}
}
};
</script>
<style>
.list-enter-active,
.list-leave-active {
transition: all 1s;
}
.list-enter,
.list-leave-to {
opacity: 0;
transform: translateX(100vw);
}
</style>

How can I bind directives to custom components in VueJS?

So I am using vuetify with vue-cli and this is my current component code:
<template>
<div>
<v-row>
<v-col xl3 md3 xs12>
<strong>{{field}}</strong>
</v-col>
<v-col xl9 md9 xs12>
{{value}}
</v-col>
</v-row>
</div>
</template>
<script>
export default {
data() {
return {
}
},
props: ['field', 'value']
}
</script>
And I am using it in my templates like this
<template>
<two-column field="Some Field" value="Some Value"></two-column>
</template>
<script>
import TwoColumnRow from './vuetify_modifications/TwoColumnRow'
...
</script>
Now everything works perfectly but what if I want to make the grid sizes dynamic? Like for example I do with something like
<two-column field="Some Field" value="Some Value" sizes="xl3 md3 xs12"></two-column>
Is that possible? Thank you in advance.
How about this:
<foo :sizes="{ xl3: '', md3: '', xs12: '' }"></foo>
And:
<template>
<div>
<v-row>
<v-col v-bind="sizes">
<strong>{{field}}</strong>
</v-col>
</v-row>
</div>
</template>
<script>
export default {
props: {
sizes: { type: Object, default: () => {} }
// ...
}
}
</script>
One way I've been able to accomplish this is through the use of computed properties.
For simplicity of creating the example I've used colors to represent what is happening. Since it seems as through all you're really asking is how could you dynamically apply classes or value based conditions inside a component, this should work with some tweaks.
const TwoColumnRow = Vue.component('two-column', {
template: '#two-column-row-template',
data: function() {
return {}
},
props: ['field', 'value', 'colors'],
computed: {
colorList: function() {
// Split the string of colors by space and return an array of values
return this.colors.split(' ');
}
}
});
const vm = new Vue({
el: '#app-container',
data: {}
});
.red {
color: red;
}
.blue {
color: blue;
}
<script src="https://unpkg.com/vue#2.1.10/dist/vue.js"></script>
<div id="app-container">
<table>
<two-column field="toast" value="cheese" colors="blue red"></two-column>
</table>
</div>
<script type="x-template" id="two-column-row-template">
<tr>
<td v-bind:class="colorList[0]">{{field}}</td>
<td v-bind:class="colorList[1]">{{value}}</td>
</tr>
</script>
This runs, so you could insert some statements {{colorList}} inside the component to see what is being rendered.