Related
Created a small project in vuejs to understand how binding works with classes and style and so on.
The behavior I wanted is that when clicking on different dynamic buttons they should have different backgrounds and it works partially. The issue is when I clicked on one button the other buttons also change their background-color at the same time and I don't want this behavior.
I have active props and I think it's causing this issue.
DynamicButton.vue
<template>
<div>
<div class="btn1">
<button
v-on="$listeners"
:class="[dark ? 'dark' : 'light', 'baseButton']"
class="btn"
:style="{ backgroundColor: color }"
>
{{ buttonText }}
</button>
</div>
</div>
</template>
<script>
export default {
name: "DynamicButton",
props: {
buttonText: {
type: String,
default: "label",
},
dark: {
type: Boolean,
default: false,
},
light: {
type: Boolean,
default: true,
},
active: {
type: Boolean,
default: false,
},
color: {
type: String,
default: "gray",
},
},
};
</script>
<style scoped>
.baseButton {
border-radius: 5px;
border: none;
padding: 10px;
width: 200px;
height: 30px;
}
.light {
background: white;
color: black;
border: 1px solid lightgray;
}
.dark {
background: black;
color: white;
}
.btn {
margin: 10px;
}
</style>
app.vue
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png" />
<DynamicButton
buttonText="Dark Mode"
:dark="true"
#click="handleDarkMode"
:color="active ? 'red' : 'blue'"
/>
<DynamicButton
buttonText="Light Mode"
#click="handleLightMode"
:color="active ? this.color : '#16a085'"
/>
</div>
</template>
<script>
import DynamicButton from "./components/DynamicButton.vue";
export default {
name: "App",
components: {
HelloWorld,
DynamicButton,
},
props: {
// darkColorChange: {
// type: String,
// default: "",
// },
// lightColorChange: {
// type: String,
// default: "",
// },
},
data() {
return {
active: true,
color: "#3aa1b6",
};
},
methods: {
handleDarkMode() {
console.log("Dark-mode clicked");
// eslint-disable-next-line
// this.darkColorChange.style.backgroundColor = "pink";
this.active = !this.active;
},
handleLightMode() {
console.log("Light-mode clicked");
this.active = !this.active;
},
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
my buttons Components
The problem is that you are using the same variable active for two different components, so it's only normal that both components would react the same way.
I would go for two different variables or an array of objects, containing the data for all the buttons because you may have more buttons in the future.
How to align the width of the cells in the header and in the main part?
I marked the correct option in the picture with green checkmarks.
https://i.stack.imgur.com/PP1w2.png
My example and solution now: https://codepen.io/horus123/pen/YzVOGLQ
<div id="test">
<div class="table">
<div class="table__header">
<div v-for="(item,index) in headers" :key="index" class="table__head-el">
{{ item.title }}
</div>
</div>
<div class="table__body">
<div v-for="(el, indexx) in tableItems" :key="indexx" class="table__row">
<span v-for="(elem, indexxx) in el" :key="indexxx" class="table__field">
{{elem}}
</span>
</div>
</div>
</div>
One option would be to use the same grid for both the header and the main part. In other word the display: grid would be apply to the div.table element. In order to make div.table__head-el and div.table__field cells of this grid div.table__header, div.table__body and table__row must have display: contents. The rest of the CSS must be adapt as well but HTML and JS stay the same (I've added a blank property at the end of tableItems objects so the length of those items match the length of the header)
new Vue({
el: "#test",
data: {
headers: [
{
title: '#'
},
{
title: 'ID', icon: 'height'
},
{
title: 'Номер', icon: 'height'
},
{
title: 'Тип', icon: 'height'
},
{
title: 'Марка', icon: 'height'
},
{
title: 'Логист', icon: 'height'
},
{
title: 'Колонна', icon: 'height'
},
{
title: 'Трекер', icon: 'height'
},
{
title: 'Дата привязки трекера', icon: 'height'
},
{
title: 'Дата последних координат', icon: 'height'
},
{
title: 'Удалена'
},
{
title: 'Дата удаления'
}
],
tableItems: [
{
number: 1,
id: '42537370',
numberCar: 'В855АТ147',
type: 'Тягач',
brand: 'Mercedes-Benz',
logistician: 'Томсон Артём Александрович',
column: 'Андреев Евгений',
tracker: '86793',
dateStart: '29.03.2021 16:42:01',
dateEnd: '07.06.2021 13:49:39',
isDeleted: false,
blank: ''
},
{
number: 1,
id: '42537370',
numberCar: 'В855АТ147',
type: 'Тягач',
brand: 'Mercedes-Benz',
logistician: 'Имя Фамилия',
column: 'Андреев',
tracker: '48671111111193',
dateStart: '29.03.2021 16:42:01',
dateEnd: '07.06.2021 13:49:39',
isDeleted: false,
blank: ''
}
],
},
computed: {
},
methods: {
}
});
html {
--border: 1px solid black;
--border-radius: 8px;
}
.table {
max-width: 100%;
padding: 0 75px;
display: grid;
grid-template-columns: minmax(0, 60px) repeat(11, minmax(0, auto));
}
.table__header, .table__body, .table__row {
display: contents;
}
.table__head-el, .table__field {
padding: 12px 20px;
}
.table__head-el {
border-top: var(--border);
border-bottom: var(--border);
margin-bottom: 20px;
}
.table__head-el, .table__field {
display: grid;
place-items: center;
overflow: hidden;
}
.table__head-el:first-child {
border-left: var(--border);
border-radius: var(--border-radius) 0 0 var(--border-radius);
}
.table__head-el:last-child {
border-right: var(--border);
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
.table__row:first-child > .table__field {
border-top: var(--border);
}
.table__row:last-child > .table__field {
border-bottom: var(--border);
}
.table__field:first-child {
border-left: var(--border);
}
.table__field:last-child {
border-right: var(--border);
}
.table__row:first-child > .table__field:first-child {
border-top-left-radius: var(--border-radius);
}
.table__row:first-child > .table__field:last-child {
border-top-right-radius: var(--border-radius);
}
.table__row:last-child > .table__field:first-child {
border-bottom-left-radius: var(--border-radius);
}
.table__row:last-child > .table__field:last-child {
border-bottom-right-radius: var(--border-radius);
}
.table__row:hover > .table__field {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="test">
<div class="table">
<div class="table__header">
<div v-for="(item,index) in headers" :key="index" class="table__head-el">
{{ item.title }}
</div>
</div>
<div class="table__body">
<div v-for="(el, indexx) in tableItems" :key="indexx" class="table__row">
<span v-for="(elem, indexxx) in el" :key="indexxx" class="table__field">
{{elem}}
</span>
</div>
</div>
</div>
</div>
I'm converting elements over from jQuery into Vue 3 JS.
I'm creating a component called "CardID" that will have person's initials.
In some cases it will be selectable, in others not. So, I'm passing that class when I'm using the component to determine if it is selectable or not.
If a user clicks on it and it has the class "is-selectable", a blue border will go around it. Clicks again, and it goes away.
In Vue3, I believe the only way I can get the className on the element is through using the event.target.
This works great if I click the grey area of the button. But if I click on the white circle or the letter, it won't work.
Any help will be appreciated. I know the same thing happens in vanilla JavaScript.
Using the component...
<CardID class="is-selectable" symbolText="K" />
The code:
<template>
<button class="card" :class="{active: isActive}" #click="checkClass">
<div class="card__circle">
<div class="card__symbol">{{ symbolText }}</div>
</div>
</button>
</template>
<script>
export default {
name: "CardID",
data: function () {
return {
isActive: false
}
},
props: {
symbolText: String,
},
methods: {
checkClass(evt) {
if(evt.target.className.includes("is-selectable")) {
this.isActive = !this.isActive;
}
}
}
}
</script>
<style scoped>
.active {
border: 2px solid #1971D4;
box-shadow: inset 0px 0px 0px 4px #fff;
}
.card {
position: relative;
width: 160px;
height: 115px;
border-radius: 8px;
border: 2px solid transparent;
background-color: #E7E7E8;
}
.card__circle {
position: absolute;
top: 50%;
left: 50%;
z-index: 10;
width: 60px;
height: 60px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.7);
transform: translate(-50%, -50%);
}
.card__symbol {
position: absolute;
top: 50%;
left: 50%;
z-index: 20;
font-size: 2.25rem;
color: #1d242b;
transform: translate(-50%, -50%);
}
Custom components can have custom props, so you should create a selectable prop in you CardID component and use that to check behaviour.
Something like:
<template>
<button class="card" :class="{active: isActive}" #click="checkClass">
<div class="card__circle">
<div class="card__symbol">{{ symbolText }}</div>
</div>
</button>
</template>
<script>
export default {
name: "CardID",
data: function () {
return {
isActive: false
}
},
props: {
selectable: Boolean,
symbolText: String,
},
methods: {
checkClass(evt) {
if(this.selectable) {
this.isActive = !this.isActive;
}
}
}
}
</script>
And use your component like:
// non selectable card
<CardID :selectable="false" symbolText="K" />
// selectable card
<CardID :selectable="true" symbolText="K" />
PS: also note that you can set a default value for your props, which is usually useful for boolean properties
Try to use this.$el.className instead of evt.target.className :
<template>
<button class="card" :class="{active: isActive}" #click="checkClass">
<div class="card__circle">
<div class="card__symbol">{{ symbolText }}</div>
</div>
</button>
</template>
<script>
export default {
name: "CardID",
data: function () {
return {
isActive: false
}
},
props: {
symbolText: String,
},
methods: {
checkClass(evt) {
if(this.$el.className.includes("is-selectable")) {
this.isActive = !this.isActive;
}
}
}
}
</script>
I'm trying to use a property of my data in a computed method like this:
data() {
return {
ToDoItems: [
{ id: uniqueId("todo-"), label: "Learn Vue", done: false },
{
id: uniqueId("todo-"),
label: "Create a Vue project with the CLI",
done: true,
},
{ id: uniqueId("todo-"), label: "Have fun", done: true },
{ id: uniqueId("todo-"), label: "Create a to-do list", done: false },
],
};
},
computed: {
listSummary() {
const numberFinishedItems = this.ToDoItems.filter((item) => item.done)
.length;
return `${numberFinishedItems} out of ${this.ToDoItems.length} items completed`;
},
},
But the IDE (Visual Studio Code) and the compiler throw an error:
Property 'ToDoItems' does not exist on type 'ComponentPublicInstance<{}, {}, {}, {}, {}, EmitsOptions, {}, {}, false, ComponentOptionsBase<{}, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, EmitsOptions, string, {}>>'.
I'm following the vue.js tutorial of mozilla (https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Vue_computed_properties#adding_a_summary_counter) but using v3.
Has anything changed that this isn't possible anymore / differently?
Thanks in advance
complete code:
<template>
<div id="app">
<h1>To-Do List</h1>
<to-do-form #todo-added="addToDo"></to-do-form>
<h2 id="list-summary">{{ listSummary }}</h2>
<ul aria-labelledby="list-summary" class="stack-large">
<li v-for="item in ToDoItems" :key="item.id">
<to-do-item :label="item.label" :done="true" :id="item.id"></to-do-item>
</li>
</ul>
</div>
</template>
<script lang="ts">
import uniqueId from "lodash.uniqueid";
import { defineComponent } from "vue";
import ToDoItem from "./components/ToDoItem.vue";
import ToDoForm from "./components/ToDoForm.vue";
export default defineComponent({
name: "App",
components: {
ToDoItem,
ToDoForm,
},
data() {
return {
ToDoItems: [
{ id: uniqueId("todo-"), label: "Learn Vue", done: false },
{
id: uniqueId("todo-"),
label: "Create a Vue project with the CLI",
done: true,
},
{ id: uniqueId("todo-"), label: "Have fun", done: true },
{ id: uniqueId("todo-"), label: "Create a to-do list", done: false },
],
};
},
methods: {
addToDo(toDoLabel: string) {
this.ToDoItems.push({
id: uniqueId("todo-"),
label: toDoLabel,
done: false,
});
},
},
computed: {
listSummary() {
const numberFinishedItems = this.ToDoItems.filter((item) => item.done)
.length;
return `${numberFinishedItems} out of ${this.ToDoItems.length} items completed`;
},
},
});
</script>
<style>
/* Global styles */
.btn {
padding: 0.8rem 1rem 0.7rem;
border: 0.2rem solid #4d4d4d;
cursor: pointer;
text-transform: capitalize;
}
.btn__danger {
color: #fff;
background-color: #ca3c3c;
border-color: #bd2130;
}
.btn__filter {
border-color: lightgrey;
}
.btn__danger:focus {
outline-color: #c82333;
}
.btn__primary {
color: #fff;
background-color: #000;
}
.btn-group {
display: flex;
justify-content: space-between;
}
.btn-group > * {
flex: 1 1 auto;
}
.btn-group > * + * {
margin-left: 0.8rem;
}
.label-wrapper {
margin: 0;
flex: 0 0 100%;
text-align: center;
}
[class*="__lg"] {
display: inline-block;
width: 100%;
font-size: 1.9rem;
}
[class*="__lg"]:not(:last-child) {
margin-bottom: 1rem;
}
#media screen and (min-width: 620px) {
[class*="__lg"] {
font-size: 2.4rem;
}
}
.visually-hidden {
position: absolute;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px);
clip: rect(1px, 1px, 1px, 1px);
clip-path: rect(1px, 1px, 1px, 1px);
white-space: nowrap;
}
[class*="stack"] > * {
margin-top: 0;
margin-bottom: 0;
}
.stack-small > * + * {
margin-top: 1.25rem;
}
.stack-large > * + * {
margin-top: 2.5rem;
}
#media screen and (min-width: 550px) {
.stack-small > * + * {
margin-top: 1.4rem;
}
.stack-large > * + * {
margin-top: 2.8rem;
}
}
/* End global styles */
#app {
background: #fff;
margin: 2rem 0 4rem 0;
padding: 1rem;
padding-top: 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1);
}
#media screen and (min-width: 550px) {
#app {
padding: 4rem;
}
}
#app > * {
max-width: 50rem;
margin-left: auto;
margin-right: auto;
}
#app > form {
max-width: 100%;
}
#app h1 {
display: block;
min-width: 100%;
width: 100%;
text-align: center;
margin: 0;
margin-bottom: 1rem;
}
</style>
Boussadjra Brahim's answer is "alright" but it doesn't actually address the issue. user16362509 is correct, but doesn't answer the question.. What you need to do is yes, annotate the return types of both your data properties and computed properties so TS knows what's going on. Not only that, but it provides stricter type checking if you annotate all types. I don't believe this issue actually occurs when properly using Vue3's composition API, but the problem does, as you can see, exist in options Api. Try: (Or consider using Composition Api). Weirdly enough, when you annotate the return type of a computed method, it understands the context, just quirks of Vue but it's good practice to have the types explicit.
<script lang="ts">
import uniqueId from "lodash.uniqueid";
import { defineComponent } from "vue";
import ToDoItem from "./components/ToDoItem.vue";
import ToDoForm from "./components/ToDoForm.vue";
export default defineComponent({
name: "App",
components: {
ToDoItem,
ToDoForm,
},
data(): { ToDoItems: Array<{id: *string*, label: string, done: boolean}> } {
return {
ToDoItems: [
{ id: uniqueId("todo-"), label: "Learn Vue", done: false },
{
id: uniqueId("todo-"),
label: "Create a Vue project with the CLI",
done: true,
},
{ id: uniqueId("todo-"), label: "Have fun", done: true },
{ id: uniqueId("todo-"), label: "Create a to-do list", done: false },
],
};
},
methods: {
addToDo(toDoLabel: string): void {
this.ToDoItems.push({
id: uniqueId("todo-"),
label: toDoLabel,
done: false,
});
},
},
computed: {
listSummary(): string {
const numberFinishedItems = this.ToDoItems.filter((item) => item.done)
.length;
return `${numberFinishedItems} out of ${this.ToDoItems.length} items completed`;
},
},
});
</script>
You are already using Vue 3. Why not use composition API with script setup for even better typescript support?
Live demo
<template>
<div id="app">
<h1>To-Do List</h1>
<form #submit.prevent="addToDo">
<input type="text" ref="label" />
<button type="submit">Add</button>
</form>
<h2 id="list-summary">{{ listSummary }}</h2>
<ul aria-labelledby="list-summary" class="stack-large">
<li v-for="item in ToDoItems" :key="item.id">
<input type="checkbox" v-model="item.done" />
{{ item.id }} {{ item.label }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
interface ToDoItem {
id: string;
label: string;
done: boolean;
}
const label = ref(null)
const ToDoItems = ref<ToDoItem[]>([
{ id: 1, label: 'Learn Vue', done: false },
{ id: 2, label: 'Create a Vue project with the CLI', done: true },
{ id: 3, label: 'Have fun', done: true },
{ id: 4, label: 'Create a to-do list', done: false },
]);
const addToDo = () => {
ToDoItems.value.push({
id: ToDoItems.value.length + 1,
label: label.value.value,
done: false,
});
label.value.value = '';
};
const listSummary = computed(() => {
return `${ToDoItems.value.filter((item) => item.done).length} out of ${ToDoItems.value.length} items completed`;
});
</script>
I am trying to change the cursor of the draggable item in chrome. Everything i tried it is not working. There are solution on Stackoverflow but they are all outdated and not working with the actual chrome version.
On drag the item is copied to a container which is the dragimage for the draggable.
What i want is to have a grabbing cursor while dragging. How would that be possible? Any Ideas?
See my code snippet for an example.
new Vue({
el: '#app',
data: {
text_drop: 'Droppable Area',
text_drag: 'Drag Area',
drag_elements: [
{text: 'one', selected: true},
{text: 'two', selected: false},
{text: 'three', selected: false},
{text: 'four', selected: false},
]
},
computed: {
selected_elements(){
let selected = [];
this.drag_elements.map((drag) => {
if(drag.selected){
selected.push(drag);
}
})
return selected;
}
},
methods: {
drag_it(event){
let html = document.getElementById("dragElement");
let drop_docs = this.selected_elements;
if(drop_docs.length > 1){
let multiple = document.createElement('div');
multiple.classList.add('dragMultiple');
multiple.innerHTML = drop_docs.length + ' items';
html.innerHTML = '';
html.appendChild(multiple)
}else{
html.innerHTML = event.target.outerHTML;
}
event.dataTransfer.setData('text/plain', '' );
event.dataTransfer.setDragImage(html, 0, 0);
event.dataTransfer.effectAllowed = "move";
},
drag_over(event){
document.documentElement.style.cursor="-webkit-grabbing";
},
drag_end(event){
document.documentElement.style.cursor="default";
},
select(event, drag_element){
if(event.metaKey || event.shiftKey){
drag_element.selected = !drag_element.selected;
} else {
this.drag_elements.map((drag) => {
if(drag === drag_element){
drag.selected = true;
}else{
drag.selected = false;
}
})
}
}
}
})
#Dragme{
width: 200px;
height: 50px;
margin-left: 20px;
text-align: center;
border:1px solid black;
float:left;
}
#Dragme:hover {
cursor: -webkit-grab;
}
#Dragme:active {
cursor: -webkit-grabbing;
}
#Dropzone{
float: left;
width: 500px;
height: 100px;
border: 1px solid;
margin-bottom: 50px;
}
.selected{
border: 2px solid yellow !important;
}
.dragMultiple{
border: 1px solid black;
padding: 10px;
background-color: white;
}
#dragElement{
position: absolute;
top: 400px;
}
<script src="https://vuejs.org/js/vue.min.js"></script>
<div id="app">
<div id="Dropzone">{{text_drop}}</div>
<div id="drag_elements">
<div v-for="drag in drag_elements"
#dragstart="drag_it"
#dragover="drag_over"
#dragend="drag_end"
#mouseup="select($event, drag)"
draggable="true"
:class="{selected: drag.selected}"
id="Dragme">{{drag.text}}</div>
</div>
</div>
<div id="dragElement">
</div>
Update
Actually it can be solved with the following answer
CSS for grabbing cursors (drag & drop)
It is important to add the dndclass
thx
Blockquote
#Carr for the hint
Update
After Dragend or drop the cursor is not set to default. Only when moved it changes back. Any Ideas?
Update
With they command key on mac or the shift key multiple items can be selected and dragged. A new dragitem is created for that purpose but the cursor does not allways fall back after dragend or drop.
Update
Integrate method to from answer -by Carr
In fact, setDragImage api is to set the image for replacing that plain document icon which be aside with default cursor, not cursor itself. So your code about '.dragElement' is not working as you expected, it's unstable and causes weird effect when I am testing, I have removed them in my answer.
What I've done below is a little bit tricky, but I think it's at least in correct logic. However, maybe there is a more elegant solution.
new Vue({
el: '#app',
data: {
text_drop: 'Droppable Area',
text_drag: 'Drag Area'
},
methods: {
drag_it(event){
event.dataTransfer.setData('text/plain', '' );
event.dataTransfer.effectAllowed = "move";
},
drag_over(event){
document.documentElement.style.cursor="-webkit-grabbing";
},
drag_end(event){
document.documentElement.style.cursor="default";
}
}
})
#Dragme{
width: 200px;
height: 50px;
text-align: center;
border:1px solid black;
float:left;
}
#Dragme:hover {
cursor: -webkit-grab;
}
#Dragme:active {
cursor: -webkit-grabbing;
}
#Dropzone{
float: left;
width: 300px;
height: 100px;
border: 1px solid;
margin-bottom: 50px;
}
<script src="https://vuejs.org/js/vue.min.js"></script>
<div id="app">
<div id="Dropzone">{{text_drop}}</div>
<div #dragstart="drag_it"
#dragover="drag_over"
#dragend="drag_end"
draggable="true"
id="Dragme">{{text_drag}}</div>
</div>
Update - derivative problems about original question
"dragImage" sticks at bottom, all elements are disappeared, or flashing sometimes.
And here is still a weird part, id attribute should be unique:
And add quote from MDN document about setDragImage, I wrongly recalled svg in comment, it should be canvas :
... The image will typically be an <image> element but it can also be a
<canvas> or any other image element. ...
We could draw text in canvas, it's another question.