Now.js Framework Documentation
EventCalendar
EventCalendar
EventCalendar คือคอมโพเนนต์ปฏิทินของ Now.js สำหรับแสดงมุมมองแบบเดือน สัปดาห์ และวัน โดยเปิดใช้ผ่าน window.EventCalendar และถ้ามี Now.js core runtime อยู่แล้ว bundle นี้จะ register manager เพิ่ม ทำให้เรียกผ่าน Now.getManager('eventCalendar') ได้ด้วย
ไฟล์ที่เกี่ยวข้องและการ build
- Runtime bundle:
Now/dist/eventcalendar.min.css,Now/dist/eventcalendar.min.js - Source entry:
Now/entry-eventcalendar.js - Source implementation:
js/components/EventCalendar.js - Source styles:
Now/css/event-calendar.css - คำสั่ง build:
npm run build:eventcalendar
คุณสมบัติหลัก
- รองรับมุมมองเดือน สัปดาห์ และวัน
- เลือก locale อัตโนมัติ โดยใช้ framework i18n ก่อน แล้วค่อย fallback ไปยังข้อความไทยและอังกฤษของ component
- รองรับ scheduling semantics 2 แบบ:
continuousและrecurring-slot - แสดง bar ต่อเนื่องข้ามวันใน month และ week view
- จัดคอลัมน์ให้ timed events ที่ชนกันใน week/day view
- โหลดข้อมูลจาก API และดึง array ได้จาก response หลายรูปแบบผ่าน
eventDataPath - รองรับ event click API ผ่าน
ResponseHandler - รองรับ month period picker พร้อม min/max bounds
- รองรับ keyboard navigation และการ emit event ผ่าน framework
- ค้นหา calendar ให้เองทั้งตอน DOM ready และใน SPA lifecycle event ที่รองรับ
การโหลด bundle
EventCalendar ถูกออกแบบมาให้ทำงานร่วมกับ Now.js core runtime ดังนั้นควรโหลด core CSS และ JS ก่อน bundle ของ EventCalendar
Requirements
- จำเป็นสำหรับ runtime ปกติ:
Now/dist/now.core.min.css,Now/dist/now.core.min.js - จำเป็นสำหรับการ emit framework events:
EventManagerจาก core bundle - แนะนำให้มี:
ModalหรือDialogManagerสำหรับ popup รายละเอียด - integration ที่ใช้เพิ่มได้:
ApiService,ResponseHandlerและI18nManager
ลำดับการโหลด
<link rel="stylesheet" href="Now/dist/now.core.min.css">
<link rel="stylesheet" href="Now/dist/eventcalendar.min.css">
<script src="Now/dist/now.core.min.js"></script>
<script src="Now/dist/eventcalendar.min.js"></script>หลังโหลด script แล้ว EventCalendar.init() จะรันอัตโนมัติ โดย component จะค้นหา calendar ตอน DOM ready และหลัง route:changed, modal:shown, page:loaded ส่วนข้อความ locale จะ refresh ใหม่อีกครั้งหลัง locale:changed และ i18n:updated
ถ้าแอปของคุณ inject markup ของ calendar นอก lifecycle events เหล่านี้ ให้เรียก EventCalendar.discoverCalendars(container) เองหลังใส่ DOM เสร็จ
การใช้งานพื้นฐาน
แบบ Declarative HTML
<div
id="calendar"
data-event-calendar
data-view="month"
data-views="month,week,day"
data-locale="th"
data-first-day="1"
data-max-events="3"
data-show-period-picker="true"
data-api="api/calendar"
data-event-data-path="data"
></div>ตัวอย่างนี้ทำอะไร:
data-view="month"กำหนดให้เริ่มที่ month viewdata-first-day="1"ใช้วันจันทร์เป็นวันแรกของสัปดาห์data-apiให้ component โหลดช่วงวันที่ที่มองเห็นจาก serverdata-event-data-path="data"ใช้ดึง event array จาก response รูปแบบ{ data: [...] }
ผลลัพธ์ที่คาดหวัง: ปฏิทินจะแสดง month view และยิง request ไปที่ api/calendar พร้อม query ของช่วงวันที่ที่มองเห็นอยู่ทันทีที่ render ครั้งแรก
แบบ JavaScript API
// สร้าง instance ใหม่ หรือใช้ตัวเดิมถ้า #calendar ถูก initialize แล้ว
const calendar = EventCalendar.create('#calendar', {
defaultView: 'month',
locale: 'th',
showPeriodPicker: true,
minDate: '2026-01',
maxDate: '2026-12',
onDateClick(date, instance) {
console.log('Date clicked', date, instance);
}
});
// ถ้าไม่ได้ใช้ config.api ให้ inject ข้อมูล local ด้วย setEvents()
EventCalendar.setEvents(calendar, [
{
id: 'meeting-01',
title: 'ประชุมทีม',
start: '2026-04-21T09:00:00',
end: '2026-04-21T10:30:00',
allDay: false,
scheduleType: 'continuous',
color: '#4285F4'
},
{
id: 'booking-01',
title: 'จองรถต่อเนื่อง',
start: '2026-04-21T08:00:00',
end: '2026-04-23T17:00:00',
allDay: false,
scheduleType: 'continuous',
color: '#EA4335'
},
{
id: 'class-slot',
title: 'จองห้องเรียน 08:00 - 09:00',
start: '2026-04-21T08:00:00',
end: '2026-04-27T09:00:00',
startDate: '2026-04-21',
endDate: '2026-04-27',
slotStartTime: '08:00',
slotEndTime: '09:00',
scheduleType: 'recurring-slot',
color: '#34A853'
}
]);ตัวอย่างนี้ทำอะไร:
create()จะคืน instance เดิม ถ้า element นี้ถูก initialize ไปแล้วsetEvents()เป็นวิธีที่รองรับจริงสำหรับใส่ local event array- ตัวอย่างนี้ผสม event แบบ
continuousและrecurring-slotอยู่ใน calendar เดียวกัน
ผลลัพธ์ที่คาดหวัง: ปฏิทิน month view จะแสดงทั้ง timed meeting วันเดียว รายการจองที่พาดหลายวัน และ recurring daily slot
ตัวอย่างขั้นสูง
Calendar ที่โหลดจาก API และดึงข้อมูลจาก response แบบซ้อนชั้น
<script>
window.App = {
calendar: {
handleEventClick(eventData, instance) {
console.log('Clicked event', eventData, instance);
}
}
};
</script>
<div
id="api-calendar"
data-event-calendar
data-view="month"
data-api="/api/calendar"
data-event-data-path="data.items"
data-on-event-click="App.calendar.handleEventClick"
></div>ตัวอย่างนี้ทำอะไร:
- ส่ง request ไปที่
/api/calendar?start=YYYY-MM-DD&end=YYYY-MM-DD - ดึง event array จาก
response.data.items - resolve callback จาก global function path string
Recurring Daily Slot ที่ใช้ click-to-API สำหรับรายละเอียด
const bookingCalendar = EventCalendar.create('#booking-calendar', {
defaultView: 'week',
scheduleMode: 'continuous',
onEventClickApi: '/api/bookings/{id}'
});
EventCalendar.setEvents(bookingCalendar, [
{
id: 'slot-01',
title: 'Training Room 08:00 - 09:00',
start: '2026-04-21T08:00:00',
end: '2026-04-27T09:00:00',
startDate: '2026-04-21',
endDate: '2026-04-27',
slotStartTime: '08:00',
slotEndTime: '09:00',
scheduleType: 'recurring-slot',
color: '#34A853'
}
]);ตัวอย่างนี้ทำอะไร:
recurring-slotจะ render timed occurrence 1 รายการในทุกวันของช่วงวันที่แบบ inclusiveonEventClickApiจะเรียก API ขอรายละเอียดเมื่อผู้ใช้คลิก event- ถ้า response มี actions ระบบจะให้
ResponseHandlerจัดการก่อน fallback ไปยัง modal รายละเอียดในตัว component
ตัวเลือกการตั้งค่า
| Option | Data attribute | Default | คำอธิบาย |
|---|---|---|---|
defaultView |
data-view |
'month' |
มุมมองเริ่มต้น รองรับ 'month', 'week', 'day' |
views |
data-views |
['month', 'week', 'day'] |
มุมมองที่อนุญาต แบบ declarative ใช้ string คั่นด้วย comma |
locale |
data-locale |
'auto' |
ภาษาที่ต้องการ ถ้าเป็น 'auto' จะอิง i18n manager, language ของเอกสาร หรือ browser locale |
timezone |
- | 'local' |
โหมด parse วันที่จาก payload ถ้าใช้ 'UTC' จะตีความ string ที่ไม่มี timezone ให้เป็น UTC |
scheduleMode |
data-schedule-mode |
'continuous' |
semantic เริ่มต้นของ event ที่ไม่ได้ส่ง scheduleType มา |
firstDayOfWeek |
data-first-day |
0 |
วันแรกของสัปดาห์ โดย 0 คือวันอาทิตย์ |
maxEventsPerDay |
data-max-events |
3 |
จำนวน inline month items สูงสุด และจำนวน spanning rows สูงสุดใน layout ปกติ |
showNavigation |
data-show-navigation |
true |
แสดงปุ่ม previous, next และ Today |
showToday |
data-show-today |
true |
แสดงปุ่ม Today |
showViewSwitcher |
data-show-view-switcher |
true |
แสดงปุ่มสลับ month/week/day |
showPeriodPicker |
data-show-period-picker |
false |
แสดง dropdown เดือนและปีเฉพาะใน month view |
api |
data-api |
null |
endpoint สำหรับโหลด event เมื่อกำหนดไว้ จะโหลดข้อมูลครั้งแรกและโหลดใหม่ตอน navigation |
apiMethod |
data-api-method |
'GET' |
HTTP method ที่ใช้ใน fetch fallback ถ้ามี ApiService อยู่แล้ว component จะใช้ ApiService.get(url) |
eventDataPath |
data-event-data-path |
'data' |
path ที่ใช้ดึง event array จาก API response |
minDate |
data-min-date |
null |
ขอบล่างของ navigation และ picker รองรับ Date, YYYY-MM, YYYY-MM-DD |
maxDate |
data-max-date |
null |
ขอบบนของ navigation และ picker รองรับ Date, YYYY-MM, YYYY-MM-DD |
yearRangeBefore |
data-year-range-before |
10 |
จำนวนปีก่อนหน้าที่แสดงใน picker เมื่อไม่มี minDate |
yearRangeAfter |
data-year-range-after |
10 |
จำนวนปีถัดไปที่แสดงใน picker เมื่อไม่มี maxDate |
eventColors |
- | palette ภายใน | ชุดสี fallback ที่ใช้เมื่อ event payload ไม่ได้ส่ง color มา |
onDateClick |
data-on-date-click |
null |
callback function หรือ global function path string หลังคลิกวันที่ |
onEventClick |
data-on-event-click |
null |
callback function หรือ global function path string หลังคลิก event |
onEventClickApi |
data-on-event-click-api |
null |
URL template สำหรับ flow แบบ click-to-API เช่น api/events/{id} |
รูปแบบ Event Payload
คอมโพเนนต์รับ plain object แล้ว normalize ก่อน render
{
"id": "event-001",
"title": "คอร์สฝึกอบรม",
"start": "2026-04-21T08:00:00",
"end": "2026-04-27T09:00:00",
"allDay": false,
"scheduleType": "recurring-slot",
"startDate": "2026-04-21",
"endDate": "2026-04-27",
"slotStartTime": "08:00",
"slotEndTime": "09:00",
"color": "#34A853",
"description": "คลาสประจำวัน",
"location": "Room A"
}| Field | Type | หมายเหตุ |
|---|---|---|
id |
string | ไม่ส่งมาก็ได้ ระบบจะสร้างให้ |
title |
string | ไม่ส่งมาจะ fallback เป็น 'Untitled' |
start |
string|Date | ควรส่งเสมอ ใช้สำหรับ parse, sort และคำนวณช่วงเวลา |
end |
string|Date | ไม่ส่งได้ ระบบจะใช้ค่าเดียวกับ start |
allDay |
boolean | สำหรับ continuous ค่า default คือ true ถ้าไม่ส่งมา ดังนั้น timed event ต้องส่ง allDay: false ให้ชัดเจน ส่วน recurring-slot ระบบจะบังคับเป็น false |
scheduleType / scheduleMode |
string | รองรับ continuous และ recurring-slot โดย scheduleType ของ event จะสำคัญกว่าค่า scheduleMode ระดับ calendar |
startDate / rangeStart |
string|Date | ช่วงเริ่มต้นแบบ inclusive ใช้บ่อยกับ recurring-slot |
endDate / rangeEnd |
string|Date | ช่วงสิ้นสุดแบบ inclusive ใช้บ่อยกับ recurring-slot |
slotStartTime |
string | เวลา HH:mm สำหรับ recurring daily slot |
slotEndTime |
string | เวลา HH:mm สำหรับ recurring daily slot |
color |
string | สีของ event ถ้าไม่ส่งมาจะใช้จาก palette ของ component |
description, location, category |
string | metadata เพิ่มเติมสำหรับ modal และ detail flow |
หลัง normalize แล้ว object เดิมจะถูกเก็บไว้ภายในเป็น event.data ดังนั้น URL template อย่าง data-on-event-click-api="/api/bookings/{id}" สามารถแทน placeholder จาก normalized event และถ้ามี helper สำหรับ nested lookup ก็สามารถอ่านค่าจาก payload เดิมได้ด้วย
Scheduling Semantics
continuous
- เป็นโหมด default
- timed event วันเดียวจะ render เป็น timed block เมื่อส่ง
allDay: false - event ต่อเนื่องหลายวันจะ render เป็น spanning bar ใน month view และ week all-day lane
- ใน week/day view event แบบ multi-day timed จะแสดง timed slice เฉพาะวันเริ่มและวันจบ ไม่ duplicate ครบทุกวัน
recurring-slot
- ใช้แทน slot เวลาเดิมที่เกิดซ้ำทุกวันภายในช่วงวันที่แบบ inclusive
- จะไม่ render ใน all-day lane ของ month/week
- ใน month view จะแสดงเป็น inline item ในแต่ละ cell ที่อยู่ในช่วงวันที่
- ใน week/day view จะ render timed occurrence ทุกวันโดยใช้
slotStartTimeและslotEndTime
หมายเหตุด้านการ render
- Month view วัด metrics จาก DOM จริงเพื่อจัด spacing ระหว่าง spanning bars กับ inline items ไม่ได้ใช้สูตร pixel แบบ hard-coded
- Week/day view จัดคอลัมน์ให้ timed events ที่ซ้อนกัน เพื่อให้แสดงแบบเคียงกันแทนการทับกัน
- ถ้าหน้าจอกว้างไม่เกิน
480pxmonth view จะเก็บ spanning lane ไว้ 1 แถวแบบ compact และนับรายการที่ซ่อนไว้เพิ่มใน+N moreแทนการซ่อนข้อมูลทิ้ง - รายการแบบ
recurring-slotจะได้ class.ec-schedule-recurring-slotซึ่ง CSS เริ่มต้นใช้เส้น accent แบบ dashed
การอ่าน API Response
loadEvents() จะส่ง query start และ end ของช่วงวันที่ที่มองเห็นอยู่ในรูปแบบ YYYY-MM-DD
component รองรับการดึง event array จาก response ได้หลายรูปแบบ เช่น
- array ตรง ๆ
response.dataresponse.data.dataresponse.data.data.data- path ที่กำหนดไว้ใน
eventDataPath
ตัวอย่าง response:
{
"success": true,
"data": [
{
"id": "booking-01",
"title": "จองรถต่อเนื่อง",
"start": "2026-04-21 08:00:00",
"end": "2026-04-23 17:00:00",
"allDay": false,
"scheduleType": "continuous",
"color": "#EA4335"
}
]
}Public JavaScript API
| Method | Parameters | Returns | คำอธิบาย |
|---|---|---|---|
init(options) |
object ของ global defaults | EventCalendar |
merge ค่า default ระดับ global, bind lifecycle handlers ครั้งเดียว และ scan DOM ปัจจุบัน |
discoverCalendars(container) |
Element หรือ document |
void |
scan container หา [data-event-calendar] แล้วสร้าง instance ให้ |
create(element, options) |
selector หรือ Element, optional config |
instance หรือ null |
ถ้า element ถูก initialize แล้ว จะคืน instance เดิม |
addEvent(calendar, event) |
instance หรือ selector, event object | void |
normalize event ใหม่, render ใหม่ และ emit eventcalendar:eventAdd |
removeEvent(calendar, eventId) |
instance หรือ selector, string id | void |
ลบ event ที่ตรง id, render ใหม่ และ emit eventcalendar:eventRemove |
updateEvent(calendar, eventId, partialData) |
instance หรือ selector, string id, partial event object | void |
merge แล้ว normalize ใหม่ ก่อน emit eventcalendar:eventUpdate |
getEvents(calendar, start, end) |
instance หรือ selector, optional Date range |
array | ถ้าส่ง start และ end มา จะคืน event ที่มีช่วงเวลาทับกับ window นั้น |
setEvents(calendar, events) |
instance หรือ selector, event array | void |
แทนที่ local event collection และ render ใหม่ทันที |
refreshEvents(calendar) |
instance หรือ selector | void |
โหลดข้อมูลใหม่จาก config.api เมื่อมีการกำหนด API endpoint |
getInstance(element) |
selector, Element หรือ instance |
instance หรือ null |
คืน instance ที่ผูกกับ element หรือ null ถ้ายังไม่ได้ initialize |
navigate(calendar, direction) |
instance หรือ selector, -1 หรือ 1 |
void |
เลื่อนช่วงเวลาปัจจุบันไปข้างหน้า/ย้อนหลังตาม view ที่ใช้งานอยู่ |
goToToday(calendar) |
instance หรือ selector | void |
กระโดดกลับมาที่วันปัจจุบันและ emit eventcalendar:today |
changeView(calendar, view) |
instance หรือ selector, 'month', 'week', 'day' |
void |
สลับ view และ render ข้อมูลที่มีอยู่ในขณะนั้น ถ้า API ของคุณคืนข้อมูลเฉพาะช่วงที่มองเห็น ควรเรียก refreshEvents() เพิ่มหลังสลับไปยัง view ที่กว้างกว่า |
destroy(calendar) |
instance หรือ selector | void |
ลบ listeners, ล้าง DOM, unregister instance และ emit eventcalendar:destroy |
Event ที่ component ส่งออก
component จะส่ง event ผ่าน EventManager และ dispatch DOM CustomEvent ชื่อเดียวกันบน document ด้วย
| Event | detail payload |
|---|---|
eventcalendar:dateClick |
{ date, instance, element } |
eventcalendar:eventClick |
{ event, instance, element } |
eventcalendar:navigate |
{ date, direction, view, instance } |
eventcalendar:today |
{ date, view, instance } |
eventcalendar:viewChange |
{ view, date, instance } |
eventcalendar:periodChange |
{ date, year, month, view, instance } |
eventcalendar:eventAdd |
{ event, instance } |
eventcalendar:eventRemove |
{ eventId, instance } |
eventcalendar:eventUpdate |
{ event, instance } |
eventcalendar:destroy |
{ element } |
ตัวอย่างการฟัง event:
document.addEventListener('eventcalendar:eventClick', (event) => {
console.log('Clicked event payload', event.detail.event);
});Accessibility และ interaction
- root element ของ calendar ใช้
role="application"และถ้ายังไม่มี focus target จะถูกตั้งtabindex="0"ให้ใช้งานด้วย keyboard ได้ - label ของช่วงเวลาปัจจุบันใช้
role="status"และaria-live="polite"เพื่อให้ screen reader ประกาศการเปลี่ยนช่วงวันที่ - ปุ่ม navigation และปุ่ม Today จะได้
aria-labelตาม locale ปัจจุบัน - keyboard shortcuts ที่มีให้ในตัวคือ
ArrowLeftและArrowRightสำหรับเลื่อนช่วงเวลา,HomeหรือTสำหรับกลับมาวันนี้, และM,W,Dสำหรับสลับไปยัง month, week, day view ที่เปิดใช้ไว้
การเชื่อมกับ ResponseHandler
เมื่อกำหนด onEventClickApi ระบบจะ build URL โดยแทน placeholder ด้วยข้อมูลของ event
<div
data-event-calendar
data-api="api/calendar"
data-on-event-click-api="api/calendar/{id}"
></div>พฤติกรรมปัจจุบัน:
- ถ้า API response มี actions ระบบจะให้
ResponseHandlerจัดการ - ถ้าไม่มี actions component จะ merge
payload.dataที่ตอบกลับเข้ากับ event ที่ถูกคลิก แล้ว fallback ไปยัง event detail modal ในตัว - ถ้ามี
onEventClickด้วย callback นี้ก็ยังจะถูกรันหลังจาก emiteventcalendar:eventClick
ข้อควรระวังและแนวทางที่แนะนำ
- timed event แบบ
continuousต้องส่งallDay: falseด้วยเสมอ ไม่เช่นนั้นระบบจะตีความเป็น all-day item - ใช้
scheduleType: 'recurring-slot'เฉพาะกรณี slot เวลาเดิมที่เกิดซ้ำทุกวันในช่วงวันที่ ใช้continuousสำหรับการจองที่พาดช่วงเวลาจริง - ถ้าต้องการ popup รายละเอียดแบบ built-in ให้โหลด
ModalหรือDialogManagerด้วย ไม่เช่นนั้นควรเขียนonEventClickของตัวเอง - ถ้าเปิด
showPeriodPickerและระบบมีช่วงวันที่ที่อนุญาตชัดเจน ควรกำหนดminDateและmaxDateไปพร้อมกัน - ถ้าแอป inject HTML นอก SPA lifecycle events ที่รองรับ ให้เรียก
EventCalendar.discoverCalendars(container)เอง - ถ้าสลับจาก view ที่แคบและโหลดข้อมูลจาก API ไปยัง view ที่กว้างกว่า ให้เรียก
EventCalendar.refreshEvents(calendar)เพื่อดึงช่วงข้อมูลใหม่ - หลังแก้ source code หรือ styles ต้อง rebuild ไฟล์ที่ใช้งานจริงด้วย
npm run build:eventcalendar
จุด hook สำหรับ CSS
ตัวอย่าง CSS variables ที่ใช้บ่อย:
:root {
--ec-primary: #4285F4;
--ec-day-height: 120px;
--ec-event-height: 22px;
--ec-event-gap: 2px;
--ec-allday-label-width: 60px;
--ec-time-label-width: 60px;
}class สำคัญ:
.ec-schedule-continuous.ec-schedule-recurring-slot.ec-continuation-slice.ec-event-start.ec-event-end.ec-today.ec-other-month