基于Web?Components實(shí)現(xiàn)一個(gè)日歷原生組件
接到一個(gè)需求,需要在原生的頁(yè)面實(shí)現(xiàn)一個(gè)日歷功能,正好之前利用閑暇時(shí)間用js寫了一個(gè)萬(wàn)年歷在github吃灰, 再用web components封裝一下,完美的一個(gè)組件就誕生了。
Web Components 基礎(chǔ)
Web Components API相對(duì)于其他UI框架, 不用加載其他模塊, 原裝組件更簡(jiǎn)單,代碼量更小.API很多,這里只介紹本次封裝組件使用到的
template標(biāo)簽
template標(biāo)簽在頁(yè)面加載時(shí)該標(biāo)簽中的內(nèi)容不會(huì)顯示,需要使用js來(lái)使其展示在頁(yè)面上。標(biāo)簽里面可以包含css 和 組件html結(jié)構(gòu)。
日歷組件template 內(nèi)容: 包括日歷樣式css, 日歷組件html結(jié)構(gòu), 預(yù)留可擴(kuò)充功能的插槽
let template = document.createElement("template");
template.innerHTML = `<style>
* {
user-select: none;
list-style: none;
margin: 0;
padding: 0;
}
.calendar_wrap {
width: 1280px;
margin: 100px auto;
position: relative;
padding-bottom: 10px;
border-radius: 20px;
box-shadow: 10px 10px 50px 10px #ccc;
}
.pick_year_month {
width: 100%;
height: 80px;
line-height: 80px;
text-align: center;
background: #cdb092;
border-radius: 20px 20px 0 0;
}
.pick_year_item {
margin: 0 20px;
font-size: 24px;
font-weight: 600;
}
.date_action {
cursor: pointer;
display: inline-block;
width: 30px;
height: 30px;
border-radius: 50%;
text-align:center;
line-height: 30px;
background: #d6d4d4;
color: #b1afaa;
font-size: 24px;
margin: 0 4px;
}
.change_num_txt {
color: #000;
}
.weekday,
.month_days {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
}
.weekday_item,
.days_item {
width: 80px;
margin: 0 50px;
text-align: center;
font-size: 20px;
}
.weekday {
margin-bottom: 20px;
font-weight:600;
}
.days_item {
height: 80px;
margin-bottom: 10px;
padding-top: 5px;
text-align: center;
line-height: 80px;
font-size: 24px;
font-weight: bold;
}
.day_color {
color: #5678FE;
background: #bb7665;
color: #fff;
border-radius: 50%;
}
.gray_color {
color: #acacac;
}
.red_color {
color: #c26228;
}
.blue_color {
color: #c26228;
}
.real_day {
cursor: pointer;
}
.real_day:hover {
outline: 1px solid orange;
border-radius: 5px;
box-shadow: 0 0 2px 2px orange;
}
.select_day_active {
outline: 1px solid orange;
}
</style>
<div class="calendar_wrap" id="calendar_container">
<div class="pick_year_month">
<span class="pick_year_item" id="decrease_year">? </span>
<span class="pick_year_item" id="decrease_month">? </span>
<span class="pick_year_item"><span class="change_num_txt" id="year_num">2019</span>年<span class="change_num_txt" id="month_num">1</span>月
</span>
<span class="pick_year_item" id="increase_month"> ?</span>
<span class="pick_year_item" id="increase_year"> ?</span>
</div>
<ul class="weekday">
<li class="weekday_item blue_color">日</li>
<li class="weekday_item">一</li>
<li class="weekday_item">二</li>
<li class="weekday_item">三</li>
<li class="weekday_item">四</li>
<li class="weekday_item">五</li>
<li class="weekday_item blue_color">六</li>
</ul>
<ul class="month_days" id="getdays">
</ul>
<slot name="customBox"></slot>
</div>自定義元素類
自定義元素CustomCalendar, 繼承HTMLElement父類, 因此具有html元素特性
class CustomCalendar extends HTMLElement {
}接下來(lái)需要在自定元素類中加載剛才定義好的template內(nèi)容
class CustomCalendar extends HTMLElement {
#shadowDom
constructor() {
super();
this.#shadowDom = this.attachShadow({ mode: "open"});
this.#shadowDom.appendChild(template.content.cloneNode(true));
}
}
customElements.define('a-calendar', CustomCalendar);this.attachShadow() 控制影子DOM 是否可操作。返回一個(gè)影子DOM(this.#shadowDom 可以用來(lái)操作DOM)。影子DOM表示內(nèi)部元素與外部隔離,不受外部任何影響。
- mode: "closed" 內(nèi)部元素可見,外部無(wú)法操作DOM
- mode: "open" 內(nèi)部元素可見,外部可操作DOM
這里使用template.content.cloneNode(true) 克隆一份template內(nèi)容加載到自定義元素內(nèi)是因?yàn)榭赡苡?strong>多個(gè)自定義元素的實(shí)例,這個(gè)模板內(nèi)容還要留給其他實(shí)例使用,所以不能直接移動(dòng)它的子元素
瀏覽器原生方法 customElements.define('a-calendar', CustomCalendar) 表示了自定義元素a-calendar 與自定義類CustomCalendar的關(guān)聯(lián),然后就可以在HTML結(jié)構(gòu)中使用a-calendar標(biāo)簽了,在template中,我們還預(yù)留了插槽的功能,還可以使用插槽來(lái)自行擴(kuò)展組件
<a-calendar>
<div slot="customarea">111</div>
</a-calendar>父子組件傳值
在之前定義了自定義標(biāo)簽,在html中使用,還可以像Vue React組件那樣在標(biāo)簽上添加任意屬性
<a-calendar config="123"></a-calendar>
父組件向子組件傳遞值
和其他UI框架組件一樣, 父組件傳值給子組件也是直接在標(biāo)簽上添加要傳遞的屬性,只是在子組件接收傳值這塊,web components 處理有些出入。
在自定義類CustomCalendar中,可以通過(guò)attributeChangedCallback鉤子函數(shù)來(lái)監(jiān)聽標(biāo)簽上屬性值的變化,同時(shí)需要observedAttributes來(lái)注冊(cè)需要監(jiān)聽的屬性, observedAttributes返回一個(gè)數(shù)組,只有在數(shù)組中填入的屬性名字,屬性值變化后才會(huì)在attributeChangedCallback拿到對(duì)應(yīng)的值
class CustomCalendar extends HTMLElement {
#shadowDom
constructor() {
super();
this.#shadowDom = this.attachShadow({ mode: "open"});
this.#shadowDom.appendChild(template.content.cloneNode(true));
}
// 監(jiān)聽屬性attr 值變化
attributeChangedCallback(name, olddata, newData) {}
static get observedAttributes() {return ['config']; }
}子組件傳值給父組件
可以通過(guò)自定義事件來(lái)實(shí)現(xiàn),在子組件需要傳值的時(shí)刻,觸發(fā)自定義事件,父組件監(jiān)聽自定義事件,就能拿到傳值
在日歷組件中,我們初始化好了當(dāng)前的日期,需要向父組件傳遞這個(gè)日期,子組件觸發(fā)一個(gè)自定義yearmonthchange事件
class CustomCalendar extends HTMLElement {
#shadowDom
constructor() {
super();
this.#shadowDom = this.attachShadow({ mode: "open"})
this.#shadowDom.appendChild(template.content.cloneNode(true));
this.date = new Date(); //獲取當(dāng)前日期
this.year = this.date.getFullYear();
this.curYear = this.year;
this.month = this.date.getMonth() + 1;
this.curMonth = this.date.getMonth() + 1; // 當(dāng)前月份
this.day = this.date.getDate();
// 觸發(fā)自定義事件, 將年月傳遞給父組件
this.dispatchEvent(new CustomEvent('yearmonthchange', {detail: {year: this.year, month: this.month}}))
}
// 監(jiān)聽屬性attr 值變化
attributeChangedCallback(name, olddata, newData) {}
static get observedAttributes() {return ['config']; }
}在父組件監(jiān)聽yearmonthchange事件,獲取到傳值
<a-calendar id="cal" config="123"></a-calendar>
<script>
var cal = document.getElementById('cal');
cal.addEventListener('yearmonthchange', (e) => {
console.log(e.detail)
})
</script>日歷功能實(shí)現(xiàn)
實(shí)現(xiàn)好的日歷大致是這個(gè)樣子

以上介紹了基本的Web Components 使用, 搭好了組件的基本框架,接下來(lái)只需要把日歷的功能代碼填入到自定義類中就OK了。
初始化事件
一共五個(gè)點(diǎn)擊事件:增減年月, 點(diǎn)擊日期
changeCurrentYearMonth(op, type) {
this.month_days_wrap.innerHTML = '';
if (op === 'inc') {
this[type] = this[type] + 1;
if (type==='month' && this[type] > 12) {
this[type] = 1
this.year = this.year + 1;
this.year_change_txt.innerHTML = this.year;
}
} else {
this[type] = this[type] - 1;
if (type==='month' && this[type] === 0) {
this[type] = 12
this.year = this.year - 1;
this.year_change_txt.innerHTML = this.year;
}
}
var opEle = type + '_change_txt';
this[opEle].innerHTML = this[type];
this.dispatchEvent(new CustomEvent('change', {detail: {year: this.year, month: this.month}}))
this.getMonthDays(this.year, this.month);
}
clickDayItem(day) {
// 子組件向父組件傳遞值
this.dispatchEvent(new CustomEvent('dayClick', day))
}
addEvent () {
var container = this.#shadowDom.getElementById('container');
var self = this;
container.addEventListener('click', function(e){
switch (e.target.id) {
case 'increase_year':
self.changeCurrentYearMonth('inc', 'year');
break;
case 'decrease_year':
self.changeCurrentYearMonth('dec', 'year');
break;
case 'increase_month':
self.changeCurrentYearMonth('inc', 'month');
break;
case 'decrease_month':
self.changeCurrentYearMonth('dec', 'month');
break;
}
if(e.target.className.includes('real_day')) {
self.clickDayItem(e.target.innerHTML);
}
})
}初始化日歷
將當(dāng)前的日期填入到頂部的操作欄中
initDays () {
this.month_days_wrap = this.#shadowDom.getElementById('getdays');
this.year_change_txt = this.#shadowDom.getElementById('year_num');
this.month_change_txt = this.#shadowDom.getElementById('month_num');
this.year_change_txt.innerHTML = this.year;
this.month_change_txt.innerHTML = this.month;
this.getMonthDays(this.year, this.month);
}然后開始計(jì)算每個(gè)月的天數(shù)填入到每月的布局中
getMonthDays(yearNums, monthNums) {
// 當(dāng)月最后一天
var date1 = new Date(yearNums, monthNums, 0);
// 當(dāng)月第一天
var date2 = new Date(yearNums, monthNums - 1, 1);
// 上個(gè)月最后一天
var date3 = new Date(yearNums, monthNums - 1, 0);
var last_month_day = date3.getDate(); // 日期
var insert_before_nums = date2.getDay(); // 當(dāng)月第一天星期
var insert_after_nums = date1.getDay(); // 當(dāng)月最后一天星期
var days = date1.getDate();
for (var i = 1; i <= days; i++) {
let dates = new Date(yearNums, monthNums - 1, i);
var li = document.createElement('li');
li.className = 'days_item real_day';
li.innerHTML = i;
var isWeekend = dates.getDay();
if (isWeekend === 0 || isWeekend === 6) { // 周六日
li.className = 'days_item real_day blue_color';
}
this.month_days_wrap.appendChild(li);
}
if (monthNums === this.curMonth && yearNums === this.curYear) { //今日日期顯示色
var today = this.#shadowDom.querySelectorAll('.days_item')[this.day - 1];
today.className = "days_item real_day day_color"
}
for (let i = 0; i < insert_before_nums; i++) { // 上月后幾天
var days_one = this.#shadowDom.querySelectorAll('.days_item')[0];
var lis = document.createElement('li');
lis.className = 'days_item gray_color';
lis.innerHTML = last_month_day--;
this.#shadowDom.querySelector('.month_days').insertBefore(lis, days_one);
}
for (let i = 1; i <= 6 - insert_after_nums; i++) { // 下月前幾天
var lis = document.createElement('li');
lis.className = 'days_item gray_color';
lis.innerHTML = i;
this.#shadowDom.querySelector('.month_days').appendChild(lis);
}
}至此,日歷功能就完成了,我們可以把自定義類單獨(dú)放在一個(gè)js文件,這樣使用的時(shí)候,直接引入js文件就可以在頁(yè)面上使用組件啦~~
以上就是基于Web Components實(shí)現(xiàn)一個(gè)日歷原生組件的詳細(xì)內(nèi)容,更多關(guān)于Web Components日歷組件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript中的6種變體函數(shù)的區(qū)別和應(yīng)用
JavaScript?中函數(shù)調(diào)用有許多獨(dú)特的變體方式,例如?~function、-function?等,這些變體不僅展現(xiàn)了?JavaScript?語(yǔ)言的靈活性,也可以在某些場(chǎng)景下讓代碼更加簡(jiǎn)潔,本文將通過(guò)示例代碼和解析,來(lái)全面剖析這些特殊的函數(shù)調(diào)用方式及其返回值的區(qū)別2025-01-01
微信小程序提取公用函數(shù)到util.js及使用方法示例
這篇文章主要介紹了微信小程序提取公用函數(shù)到util.js及使用方法,結(jié)合實(shí)例形式分析了util.js公用函數(shù)相關(guān)調(diào)用操作技巧,需要的朋友可以參考下2019-01-01
js實(shí)現(xiàn)的定時(shí)關(guān)閉頁(yè)面或定時(shí)提醒效果代碼
比較有創(chuàng)意的定時(shí)關(guān)閉頁(yè)面或定時(shí)提醒效果代碼2008-02-02
解決JS組件bootstrap table分頁(yè)實(shí)現(xiàn)過(guò)程中遇到的問(wèn)題
這篇文章主要介紹了JS組件bootstrap table分頁(yè)實(shí)現(xiàn)過(guò)程中遇到的問(wèn)題,感興趣的小伙伴們可以參考一下2016-04-04
小程序?qū)崿F(xiàn)訂單倒計(jì)時(shí)功能
這篇文章主要為大家詳細(xì)介紹了小程序?qū)崿F(xiàn)訂單倒計(jì)時(shí)功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-04-04
JavaScript實(shí)現(xiàn)日期格式化的操作詳解
在我們做業(yè)務(wù)開發(fā)的漫長(zhǎng)歲月里,會(huì)多次跟時(shí)間打交道,相信大多數(shù)小伙伴對(duì)日期格式化也并不陌生,本文簡(jiǎn)單記錄了JavaScript實(shí)現(xiàn)日期格式化的過(guò)程,以及一些拓展,希望對(duì)大家有所幫助2023-05-05

