Now.js Framework Documentation

Now.js Framework Documentation

EventCalendar

TH 21 Apr 2026 14:56

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 view
  • data-first-day="1" ใช้วันจันทร์เป็นวันแรกของสัปดาห์
  • data-api ให้ component โหลดช่วงวันที่ที่มองเห็นจาก server
  • data-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 รายการในทุกวันของช่วงวันที่แบบ inclusive
  • onEventClickApi จะเรียก 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 ที่ซ้อนกัน เพื่อให้แสดงแบบเคียงกันแทนการทับกัน
  • ถ้าหน้าจอกว้างไม่เกิน 480px month 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.data
  • response.data.data
  • response.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 นี้ก็ยังจะถูกรันหลังจาก emit eventcalendar: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

Additional Resources