// https://codesandbox.io/s/crimson-sky-7zozs?file=/src/main.js&ck_subscriber_id=1584215866&utm_source=convertkit&utm_medium=email&utm_campaign=%F0%9F%94%A5+%28%2366%29+Multiple+v-models+%28and+4+bonus+tips%21%29+-+8426354
<template>
<div class="drag-container">
<img
alt="Vue logo"
src="./assets/logo.png"
:style="{
left: `${x}px`,
top: `${y}px`,
cursor: dragging ? 'grabbing' : 'grab',
}"
draggable="false"
@mousedown="dragging = true"
/>
</div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const dragging = ref(false);
const mouseX = ref(0);
const mouseY = ref(0);
const x = ref(100);
const y = ref(100);
window.addEventListener("mousemove", (e) => {
if (dragging.value) {
const diffX = e.clientX - mouseX.value;
const diffY = e.clientY - mouseY.value;
x.value += diffX;
y.value += diffY;
}
mouseX.value = e.clientX;
mouseY.value = e.clientY;
});
window.addEventListener("mouseup", () => {
dragging.value = false;
});
return {
x,
y,
dragging,
};
},
};
</script>
<style scoped>
.drag-container {
width: 100vw;
height: 100vh;
}
img {
cursor: grab;
position: relative;
}
</style>
<div :class="disabled && 'disabled-component'"></div>
<div :class="disabled ? 'disabled-component' : 'not-yet-disabled'"></div>
<div
:class="{
primary: isPrimary,
secondary: isSecondary,
tertiary: isTertiary,
}"
/>
<div
:class="[
isPrimary && 'primary',
isSecondary && 'secondary',
isTertiary && 'tertiary',
]"
/>
<div :class="computedClasses" />
<div :class="[computedClasses, { 'active': isActive }]"></div>
const computedClasses = computed(() => ({
'text-red': isError.value,
'text-green': isSuccess.value,
'font-bold': shouldBeBold.value
}))
// or
const computedClasses = computed(() => [
isError.value && 'text-red',
isSuccess.value && 'text-green',
shouldBeBold.value && 'font-bold'
].filter(Boolean))
Source:
https://medium.com/arcana-network-blog/vue-3-composition-api-basics-and-patterns-44813f2c785d
/*
https://vueschool.io/articles/vuejs-tutorials/what-is-a-vue-js-composable/
*/
/*
Clarity of Data/Methods Source
Mixins = Data Source Obscured
Composables = Transparent Source of Data and Functions
*/
//MyComponent.vue
import ProductMixin from './ProductMixin'
import BrandMixin from './BrandMixin'
import UserMixin from './UserMixin'
export default{
mixins:[ProductMixin, BrandMixin, UserMixin],
created(){
// Where in the world did name come from?
// Let me look through each of the registered mixins to find out
// Oh man, it's not in any of them...
// It must be from a globally registered mixin
console.log(this.site)
// Oh, I got lucky here turns out the first mixin I inspected has the name
console.log(this.name)
}
}
//MyComponent.vue
import useProduct from './useProduct'
import useBrand from './useBrand'
import useUser from './useUser'
export default{
setup(){
const { name } = useProduct()
return { name }
}
created(){
// Where in the world did name come from?
// ah, it's not in setup anywhere... this doesn't exist and is an error
console.log(this.site)
// Oh, nice I can see directly in setup this came from useProduct
console.log(this.name)
}
}
/*
Naming Collisions
Mixins = Risk of Naming Collisions
Composables = NO risk of naming collisions
*/
//MyComponent.vue
import ProductMixin from './ProductMixin' // name = AirMax
import BrandMixin from './BrandMixin' // name = Nike
import UserMixin from './UserMixin' // name = John Doe
export default{
mixins:[ProductMixin, BrandMixin, UserMixin],
created(){
// Actually I'm not so lucky,
// yeah I found the name in ProductMixin
// but turns out UserMixin had a name too
console.log(this.name) // John Doe
}
}
//MyComponent.vue
import useProduct from './useProduct' // name = AirMax
import useBrand from './useBrand' // name = Nike
import useUser from './useUser' // name = John Doe
export default{
setup(){
const { name: productName } = useProduct()
const { name: brandName } = useBrand()
const { name: userName } = useUser()
return { productName, brandName, userName }
}
created(){
// Yay! Nothing is lost and I can get the name of each of the things
// together in my component but when writing the composables
// I don't have to think at all about what variable names might collide
// with names in other composables
console.log(this.productName)
console.log(this.brandName)
console.log(this.userName)
}
}
/*
Mutating Module's Reactive Data from the Component
Mixins = Can NOT Safeguard Own Reactive Data
Composables = Can Safeguard Own Reactive Data
*/
// RequestMixin.js
export default {
data(){
return {
loading: false,
payload: null
}
},
methods:{
async makeRequest(url){
this.loading = true
const res = await fetch(url)
this.payload = await res.json()
this.loading = false
}
}
}
// useRequest.js
import { readonly, ref } from "vue";
export default () => {
// data
const loading = ref(false);
const payload = ref(null);
// methods
const makeRequest = async (url) => {
loading.value = true;
const res = await fetch(url);
payload.value = await res.json();
};
// exposed
return {
payload: readonly(payload), //notice the readonly here
loading: readonly(loading), // and here
makeRequest
};
};
/*
Global State with Composables
*/
//CounterMixins.js
export default{
data(){
return { count: 0 }
},
methods:{
increment(){
this.count ++
}
}
}
//useCounter.js
import {ref, readonly} from 'vue'
export default () => {
const count = ref(0)
const increment = ()=> count.value++
return {
count: readonly(count),
increment
}
}
//useCounter.js
import {ref, readonly} from 'vue'
const count = ref(0)
export default () => {
const increment = ()=> count.value++
return {
count: readonly(count),
increment
}
}
<HelloWorld color="red" bg="green" />
<template>
<div :style="cssProps">KUK</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: ['color', 'bg'],
computed: {
cssProps() {
return {
'--color': this.color,
'--bg': this.bg,
};
},
},
};
</script>
<style scoped>
div {
color: var(--color);
background: var(--bg);
}
</style>
<script>
const initialState = () => {
return {
modalBodyDisplay: "getUserInput",
submitButtonText: "Lookup",
addressToConfirm: null,
bestViewedByTheseBounds: null,
location: {
name: null,
},
};
};
export default {
data() {
return initialState();
},
methods: {
reset() {
Object.assign(this.$data, initialState());
},
},
};
</script>
// pagination list, load more (or lazy load)
import { onMounted, onUnmounted } from 'vue';
export const useScrollToBottom = (callback = () => { }) => {
const handleScrolling = () => {
if ((window.innerHeight + window.scrollY) >= document.body.scrollHeight) {
callback();
}
}
onMounted(() => {
window.addEventListener('scroll', handleScrolling);
});
onUnmounted(() => {
window.removeEventListener('scroll', handleScrolling);
});
}
// usage
useScrollToBottom(() => { console.log('Scrolled to bottom') })
import { onMounted, onUnmounted } from 'vue';
export const useOnClickOutside = (ref = null, callback = () => {}) => {
function handleClickOutside(event) {
if (ref.value && !ref.value.contains(event.target)) {
callback()
}
}
onMounted(() => {
document.addEventListener('mousedown', handleClickOutside);
})
onUnmounted(() => {
document.removeEventListener('mousedown', handleClickOutside);
});
}
// usage
<template>
<div ref="container">View</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const container = ref(null);
useOnClickOutside(container, () => {
console.log('Clicked outside');
})
}
}
</script>