The hottest news on the front end these days is nothing more than the release of VUe3.0, although it is not the official version, it also makes people want to find out. Just have nothing to do in the afternoon, then skip over the document. But the best way to learn is to put it to use. Look around, just see the Calendar of Win10, whoops, good, it’s you.
The effect is shown below:
A simple online prototype
Full preview link
Set up vue3.0 project
Scaffolding is a must, but with the support of @vue/ CLI, building projects is very easy.
vue create win10-calendar
cd win10-calendar && vue add vue-next
Demand analysis
The requirements are clear. Copy. Study the subject before you copy. After careful manipulation, you can see that there are three operable areas in the head of the calendar. Click on the part of year/month to switch the view, which is calendar table, month table, year table, step by step. The upper and lower arrows have different functions in different views. In the calendar table, month is controlled, while in the month table, year is controlled. In the year table, N years are controlled at one time. When the mouse hovers over the table, it has a searchlight effect, which is quite interesting. Table switch, also have indentation, expansion of animation effects.
At this point in the analysis, the code organization is basically determined. Component entry, component header, calendar table, month table, year table 5 components. Searchlight effects are also extracted separately as a component for ease of abstraction. There are six components in total.
The three tables seem to show different things, but they are actually related. In fact, they all show different dimensions of the same date. The header is responsible for editing the date and switching between different views. The component portal, of course, is responsible for the integration of these sub-components, and the state of the sub-components needs to be raised to its level.
Looking at searchlights, CSS doesn’t have a searchlight property as handy as box-shadow, which highlights an area. But CSS magic is a brain teaser. Suppose we make a black layer, dig a hole in the middle, when the mouse moves, so that the center of the layer follows the mouse, is not a searchlight? As for not floating, the text should also be visible, floating, text and grid border visible at the same time, it is also very simple, the text positioning, z-index increase, let them [surface] can be.
Write a component
Vue3 is said to support most features of vue2. X. Vue single files are also still supported. Create a New Calendar/index.vue component and add child components along the way. The file structure is as follows
Calendar
index.vue
children
CalendarHead.vue
DatPanel.vue
MonhPanel.vue
YeaPanel.vue
Mask.js
Copy the code
Script section, most of the attributes can be removed. Add a setup function. In Vue3, the setup function is called between beforeCreate and created. It can return either a render function or an object. Object that can be used in the template.
The index in the early
Start by adding some global state to index.vue, such as a date object. These states need to be passed down to the child components, and the child components need to be able to change those states. Vue2. X generally uses props/event, which is a bit cumbersome to be honest. Vuex also feels too heavy to use.
Vue3 provides a set of provide/inject mechanisms. Although vue2. X also has it, it needs to be declared as props. Through the Provide/Inject API, arbitrary values can be passed directly to any level of child components through a convention key.
// index.vue
import {ref, provide} from 'vue';
import dayjs from "dayjs";
export default {
setup() {
const date = ref(dayjs().toDate());
const setDate = (value) = > {
date.value = dayjs(value).toDate();
};
const displayMode = ref("date");
const setDisplayMode = (mode) = > {
displayMode.value = mode;
};
provide("displayMode", [displayMode, setDisplayMode]);
provide("date", [date, setDate]);
return{}}}Copy the code
Subcomponents can directly obtain the value declared in index and set function through inject API.
Calendar component
Let’s start with the calendar
// DatePanel.vue
import { computed, h, inject } from "vue";
import dayjs from "dayjs";
export default {
setup() {
const [date, setDate] = inject("date");
const dateList = computed((a)= > getDateList(date.value));
return {
dateList,
weeks: ["Day"."一"."二"."Three"."Four"."Five"."Six"]}; }},// Create a calendar table
function getDateList(date) {
// First day of the month
const day0 = dayjs(date).startOf("month");
Sunday of the first week of this month
const firstDay = day0.subtract(day0.get("day"), "day");
const rows = 6; //Math.ceil((day0.get("day") + day0.daysInMonth()) / 7);
return Array(rows * 7)
.fill(0)
.map((n, i) = > firstDay.add(i, "day"));
}
Copy the code
Win10 calendar start from Monday, also js week 1 start, for easy calculation, we start from Sunday, also js week 0 start. To calculate the calendar, we just need to calculate the Sunday of the first week of the month, and then increase from that day to get the whole calendar. In order to ensure the height of the calendar stable, fixed 6 weeks, that is, 42 days. I’ll leave out the templates and CSS. Interested in accessing the source code directly
Searchlight mask
With the basic layout, you can start writing the mask layer. For this component, let’s try the function component. Functional components are assumed to be directly used as the setup part of the component, not read the source code, say wrong. The function can return JSX directly. If there is a problem with JSX parsing, non-native attributes cannot be passed through JSX, but can only be obtained through attrs attributes. Fortunately, the template part of this component is very simple, and the only dynamic part is the style. From the above analysis, the searchlight is actually a cut mask, and this effect can be achieved by radial gradient.
radial-gradient(transparent, rgba(0.0.0.1) 60px.# 000)
Copy the code
To make it work, we need to get the mouse’s coordinates on the element in real time. Such a function, the official has a very appropriate example, with a change can be used. We’ve abstracted this functionality into a hook as well.
// src/hooks/useMousePosition.js
import { onMounted, onUnmounted, toRefs, reactive } from "vue";
// Pass in a DOM reference and assign the mouse position over the element as the mouse moves over it
export default function (elRef) {
const state = reactive({
x: 0.y: 0.width: 0.height: 0.enter: false});let rect = {
top: 0.left: 0.width: 0.height: 0};function onEnter() {
if(! elRef || ! elRef.value)return;
state.enter = true;
rect = elRef.value.getBoundingClientRect();
state.height = rect.height;
state.width = rect.width;
}
function onMove(e) {
const { clientX, clientY } = e;
state.x = clientX - rect.left;
state.y = clientY - rect.top;
}
function onLeave() {
state.enter = false;
}
onMounted((a)= > {
if(! elRef || ! elRef.value)return;
elRef.value.addEventListener("mouseenter", onEnter);
elRef.value.addEventListener("mousemove", onMove);
elRef.value.addEventListener("mouseleave", onLeave);
});
onUnmounted((a)= > {
if(! elRef || ! elRef.value)return;
elRef.value.removeEventListener("mousemove", onMove);
elRef.value.removeEventListener("mouseenter", onEnter);
elRef.value.removeEventListener("mouseleave", onLeave);
});
return {
...toRefs(state),
};
}
Copy the code
Modified index. Vue.
import useMousePosition from "@/hooks/useMousePosition";
export default {
setup(){
const el = ref(null);
const position = useMousePosition(el);
return {
el
}
}
}
Copy the code
The template section
<div class="calendar" ref="el">
<! --other-->
<Mask :position="position"/>
</div>
Copy the code
Note that the ref reference in the template cannot be appended with :, where it is easy to think that the template needs to bind a reference to EL in setup to the element, when in fact it only needs to bind the string el to the element’s ref.
✅❌
With the parent component providing the coordinate dimension information, the Mask component is ready to move. Because the Mask moves through the parent component, it can be blown up to twice the size of the parent component as it moves to the edge. Also, in order to keep its center overlapping with the mouse, it needs to be offset to the left by 1/2 of its size.
// Mask.js import { computed, h } from "vue"; // export default function Mask(props) {const position = props. Position; const style = computed(() => { const size = Math.max(position.width, position.height) * 2; const isEnter = position.enter; return { transform: `translate(${position.x - size / 2}px, ${ position.y - size / 2 }px)`, backgroundImage: isEnter ? `radial-gradient(transparent, rgba(0, 0, 0, 1) 60px, #000)` : "", backgroundColor: isEnter ? "" : "#000", width: size ? size + "px" : "100%", height: size ? size + "px" : "100%", }; }); return <div class="mask" style={style.value} />; }Copy the code
The props function automatically passes its value to its subcomponents, so it does not need to append.value to any value obtained by props.
Operation area component
Now we have the basic effects, mouse around and cool effects. Let’s go ahead and refine it. Transfer fronts to CalendarHead components. There are two main operations here, one is to switch the view, one is to adjust the date up and down. Index already opens up reading and writing of these two values to child components through provide. To switch views, just check the current view and do a progression. To adjust the date, also check the view. When the current is a calendar table, the adjustment range is plus or minus one month; When the current is a monthly table, the adjustment range is plus or minus one year; When the current is the year table, the adjustment range is plus or minus 16 years;
import {inject, ref, computed} from "vue";
import dayjs from 'dayjs'
export default {
setup() {
const [displayMode, setDisplayMode] = inject("displayMode", [ref("date"), (v) => v]);
const [date, setDate] = inject("date", [ref(new Date()), (v) => v]);
const setPanelMode = (a)= > {
let mode = "date";
if (displayMode.value === "date") {
mode = "month";
}
if (displayMode.value === "month") {
mode = "year";
}
if (displayMode.value === "year") {
mode = "year";
}
setDisplayMode(mode);
};
const dateString = computed((a)= > (displayMode.value === "date" ? fmtDate : fmtYear)(date.value));
const handleDate = (isAdd) = >() = > {const setMap = {
date: {
value: 1.unit: "month",},month: {
value: 1.unit: "year",},year: {
value: 16.unit: "year",}};const setter = setMap[displayMode.value];
const value = isAdd
? dayjs(date.value).add(setter.value, setter.unit)
: dayjs(date.value).subtract(setter.value, setter.unit);
setDate(value.toDate());
};
const upward = handleDate(false);
const downward = handleDate(true);
return {
upward,
downward,
setPanelMode,
dateString,
}
}
}
Copy the code
Month/year component
Now you can switch months on the calendar, and then add the month/year table. First modify index.vue in response to the displayMode change and prepare for animation. Watch in the composite API is used here to actively monitor changes in reactive values. When displayMode changes, switch componentName and transitionName.
There are two kinds of animation, indenting is to switch to higher level, expanding is to switch to lower level, monitoring the old and new view mode can be obtained.
// index.vue
export default {
setup() {
// other code
const transitionName = ref("out");
const componentName = ref("date-panel");
const levels = ["date"."month"."year"];
watch(displayMode, (now, old) => {
const nowLevel = levels.indexOf(now);
const oldLevel = levels.indexOf(old);
transitionName.value = nowLevel < oldLevel ? "out" : "in";
// componentName must be set after or at the same time as transitionName to not lag the transitionName
componentName.value = {
date: "date-panel".month: "month-panel".year: "year-panel",
}[displayMode.value];
});
return {
componentName,
transitionName,
}
}
}
Copy the code
The template section
<div class="calendar" ref="el">
<CalendarHead/>
<div class="cell-wrap">
<transition :name="transitionName">
<component :is="componentName"/>
</transition>
</div>
<Mask :position="position"/>
</div>
Copy the code
Back to business. In fact, the month table and the year table also have an operation. Click the month table, switch the date to the selected month, and switch the view to the calendar table. Click the year table, toggle the date to the selected year, and toggle the view to the month table. This can be done easily with the displayMode and date extensions in index.vue.
// MonthPanel
import { computed, inject, ref } from "vue";
import dayjs from "dayjs";
export default {
setup() {
const [date, setDate] = inject("date", [ref(new Date()), (v) => v]);
const [displayMode, setDisplayMode] = inject("displayMode", [ef("date"),(v) => v]);
const monthList = computed((a)= > {
const month0 = dayjs(date.value).month(0);
return Array(16)
.fill(0)
.map((n, i) = > month0.add(i, "month"));
});
const getClass = (item) = > {
return [
"month-cell",
item.month() === dayjs(date.value).month() && " current-month ",
item.year() === dayjs(date.value).year() && " current-year ",
].join("");
};
const pickMonth = (item) = > {
setDate(item.toDate());
setDisplayMode("date");
};
return{ monthList, getClass, pickMonth, }; }};Copy the code
The year table is much the same, will not put, are interested in directly viewing the year table source and month table source
conclusion
Such a small toy, basically a common VUE3 API through. In the process of writing, I also encountered many difficulties, some are not familiar with the new API, some are ignorant, some are vue3 itself is not mature. Of course, it’s good to have trouble learning something new. There is something to be gained from overcoming it. In general, vue3 has a lot of potential, especially in the case of hooks. React already has a library like SWR, so you can imagine that some of the more complex logic can be packaged like this in the future. Some people think that libraries like React/Vue have already made it difficult for new developers to manipulate the DOM, and this situation will be exacerbated in the future. Developers will not know how to write a pull-up load, but only NPM install… Back to the vue. The composition API design eliminates the need to spell strings or use lightweight rendering libraries like Preact when developing small components. And it can be ported to other ends more easily, so a bunch of small application frameworks will be busy upgrading again. . Let’s talk about the downside of the experience.
- The ref.value design is a bit annoying, confusing and sometimes necessary
.value
Sometimes not. And isn’t there a better way to intercept base values than by wrapping them? Perhaps wrapping is not the only solution, such as wrapping the current module object or context directly? Don’t believe me (laugh - Ts support is not good enough. At the beginning, I went directly to TS. However, there were various difficulties and many unfamiliar places, in order to better eliminate problems, I went back to JS. It’s still not enough out of the box.
- The ref in the template is probably a string for compatibility with the old usage, but it would be nice to have support for binding ref values, otherwise it would be confusing.
The project address
The full project can be found on Github. Or go to my blog.