my new best friend
* I'm talking about the Vue Options/Class APIs compared to React's Options/Class APIs
<template>
<button @click="increment">
Count is: {{ state.count }}, double is: {{ state.double }}
</button>
</template>
<script>
import { reactive, computed } from 'vue'
export default {
setup() {
const state = reactive({
count: 0,
double: computed(() => state.count * 2)
})
function increment() {
state.count++
}
return {
state,
increment
}
}
}
</script>
Look Ma!
No 'this'!
Mixins
Less Confusing Nomenclature
Seperation of Concerns
This is actually not using Vue 3.0, but Vue 2.0. While I recommend that you do give 3.0 a try, it might be a bit much to convert legacy 2.0 projects.
That shouldn't stop you however, as you can just add the Vue Composition API as a plugin - like Vuex or Vue Router - and use it in Vue 2.0 codebases. (That's right: Try before you buy!)
# BASH
$ yarn add @vue/composition-api
/* ------ */
/* in main.ts/main.js */
import VueComposition from '@vue/composition-api';
Vue.use(VueComposition)
import { Component, Prop, Watch, Vue } from 'vue-property-decorator'
import { Action } from 'vuex-class'
import { IRoom, IEvent } from '../types'
@Component
export default class RoomClock extends Vue {
@Prop({ required: true }) roomEvents: Array<IEvent>
@Action('getEventsByRoom') getEventsByRoom: Function
ctx: CanvasRenderingContext2D | null
height: number = 0
width: number = 0
interval: number = 0
private CLOCK = {
PADDING: 0,
BORDER: 14,
DEFAULT: 'rgba(255,255,255, 0.1)',
HOUR_HAND: 'white',
BUSY: ['#FB4352', '#22CDAD', '#FEE44D', '#1EA1FC'],
RADIUS_DIVIDER: 2.2
}
$refs: {
container: Element
clockCanvas: HTMLCanvasElement
}
get x() {
return this.width / 2
}
get y() {
return this.height / 2
}
get radius() {
return (this.x * 94) / 100
}
randomColor() {
return this.CLOCK.BUSY[Math.floor(Math.random() * this.CLOCK.BUSY.length)]
}
@Watch('roomEvents')
draw() {
this.setCanvas()
this.drawOuterCircle()
this.drawOccupiedHours()
this.drawCurrentHourIndicator()
}
setCanvas() {
// check is container & canvas is avaliable
if (!this.$refs.container) return null
this.ctx = this.$refs.clockCanvas.getContext('2d')
// once both of them are avaliable
if (this.$refs.container.parentElement) {
const { width, height } = this.$refs.container.parentElement.getBoundingClientRect()
this.width = width + this.CLOCK.PADDING
this.height = height + this.CLOCK.PADDING
}
if (this.ctx) {
// clear everything
this.ctx.clearRect(0, 0, this.width, this.height)
return true
}
}
drawOuterCircle() {
if (this.ctx) {
this.ctx.beginPath()
this.ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false)
this.ctx.lineWidth = this.CLOCK.BORDER
this.ctx.strokeStyle = this.CLOCK.DEFAULT
this.ctx.stroke()
}
}
async drawOccupiedHours() {
if (this.ctx) {
// get the data
this.ctx.lineWidth = this.CLOCK.BORDER
// draw for all current and future occupied events
this.roomEvents.forEach(item => {
if (new Date() < new Date(item.End) && this.ctx) {
const startAngle = this.getAngle(item.Start)
const endAngle = this.getAngle(item.End)
// draw arc
this.ctx.beginPath()
this.ctx.strokeStyle = this.randomColor()
this.ctx.arc(this.x, this.y, this.radius, startAngle, endAngle)
this.ctx.stroke()
}
})
return true
}
}
drawCurrentHourIndicator() {
const now = new Date()
const nowPlusMinute = new Date()
nowPlusMinute.setMinutes(nowPlusMinute.getMinutes() + 2)
const startAngle = this.getAngle(now.toJSON())
const endAngle = this.getAngle(nowPlusMinute.toJSON())
if (this.ctx) {
// draw arc
this.ctx.beginPath()
this.ctx.strokeStyle = this.CLOCK.HOUR_HAND
this.ctx.lineWidth = this.CLOCK.BORDER
this.ctx.arc(this.x, this.y, this.radius, startAngle, endAngle)
this.ctx.stroke()
}
}
getAngle(timeStr: any) {
const time = new Date(timeStr)
const h = time.getHours()
const m = time.getMinutes()
const s = time.getSeconds()
// convert hours, minutes and seconds to angle
const angle = ((h % 12) * Math.PI) / 6 + (m * Math.PI) / (6 * 60) + (s * Math.PI) / (360 * 60)
// remove 90 deg from the angle to start at 12 O'clock
return angle - 0.5 * Math.PI
}
}
import {
defineComponent,
reactive,
ref,
watch,
toRefs,
onMounted,
} from '@vue/composition-api'
import { IEvent } from '../../types'
import { RoomClockProps, RoomClockState } from './types'
import useCanvas from './hooks/useCanvas'
const RoomClock = defineComponent({
name: 'room-clock',
setup(props: RoomClockProps, vm: unknown) {
const container = ref<null | HTMLElement>(null)
const clockCanvas = ref<null | HTMLCanvasElement>(null)
const roomEvents: IEvent[] = props.roomEvents
const state = reactive<RoomClockState>({
ctx: null,
height: 0,
width: 0,
interval: 0
})
const draw = useCanvas({ container, clockCanvas, state, roomEvents })
onMounted(() => {
draw()
})
watch(
() => roomEvents,
() => draw()
)
const { width, height } = toRefs(state)
return { width, height, container, clockCanvas }
}
})
old component
import { Component, Prop, Watch, Vue } from 'vue-property-decorator'
import { Action } from 'vuex-class'
import { IRoom, IEvent } from '../types'
@Component
export default class RoomClock extends Vue {
@Prop({ required: true }) roomEvents: Array<IEvent>
@Action('getEventsByRoom') getEventsByRoom: Function
ctx: CanvasRenderingContext2D | null
height: number = 0
width: number = 0
interval: number = 0
private CLOCK = {
PADDING: 0,
BORDER: 14,
DEFAULT: 'rgba(255,255,255, 0.1)',
HOUR_HAND: 'white',
BUSY: ['#FB4352', '#22CDAD', '#FEE44D', '#1EA1FC'],
RADIUS_DIVIDER: 2.2
}
$refs: {
container: Element
clockCanvas: HTMLCanvasElement
}
get x() {
return this.width / 2
}
get y() {
return this.height / 2
}
get radius() {
return (this.x * 94) / 100
}
randomColor() {
return this.CLOCK.BUSY[Math.floor(Math.random() * this.CLOCK.BUSY.length)]
}
@Watch('roomEvents')
draw() {
this.setCanvas()
this.drawOuterCircle()
this.drawOccupiedHours()
this.drawCurrentHourIndicator()
}
setCanvas() {
// check is container & canvas is avaliable
if (!this.$refs.container) return null
this.ctx = this.$refs.clockCanvas.getContext('2d')
// once both of them are avaliable
if (this.$refs.container.parentElement) {
const { width, height } = this.$refs.container.parentElement.getBoundingClientRect()
this.width = width + this.CLOCK.PADDING
this.height = height + this.CLOCK.PADDING
}
if (this.ctx) {
// clear everything
this.ctx.clearRect(0, 0, this.width, this.height)
return true
}
}
drawOuterCircle() {
if (this.ctx) {
this.ctx.beginPath()
this.ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false)
this.ctx.lineWidth = this.CLOCK.BORDER
this.ctx.strokeStyle = this.CLOCK.DEFAULT
this.ctx.stroke()
}
}
async drawOccupiedHours() {
if (this.ctx) {
// get the data
this.ctx.lineWidth = this.CLOCK.BORDER
// draw for all current and future occupied events
this.roomEvents.forEach(item => {
if (new Date() < new Date(item.End) && this.ctx) {
const startAngle = this.getAngle(item.Start)
const endAngle = this.getAngle(item.End)
// draw arc
this.ctx.beginPath()
this.ctx.strokeStyle = this.randomColor()
this.ctx.arc(this.x, this.y, this.radius, startAngle, endAngle)
this.ctx.stroke()
}
})
return true
}
}
drawCurrentHourIndicator() {
const now = new Date()
const nowPlusMinute = new Date()
nowPlusMinute.setMinutes(nowPlusMinute.getMinutes() + 2)
const startAngle = this.getAngle(now.toJSON())
const endAngle = this.getAngle(nowPlusMinute.toJSON())
if (this.ctx) {
// draw arc
this.ctx.beginPath()
this.ctx.strokeStyle = this.CLOCK.HOUR_HAND
this.ctx.lineWidth = this.CLOCK.BORDER
this.ctx.arc(this.x, this.y, this.radius, startAngle, endAngle)
this.ctx.stroke()
}
}
getAngle(timeStr: any) {
const time = new Date(timeStr)
const h = time.getHours()
const m = time.getMinutes()
const s = time.getSeconds()
// convert hours, minutes and seconds to angle
const angle = ((h % 12) * Math.PI) / 6 + (m * Math.PI) / (6 * 60) + (s * Math.PI) / (360 * 60)
// remove 90 deg from the angle to start at 12 O'clock
return angle - 0.5 * Math.PI
}
}
All of these concern static data and derived
data for the clock, so they were changed from observables to plain functions and seperated into ./utils/clock.ts
All of these concern drawing on the canvas, so they were moved to useCanvas()
...and changed from managed observable to plain function
which in fact, didn't need to be imported into the main component at all.