部品思想でシンプルに！React不使用でモジュール式ウェブページを作成する方法

## 概要

本記事では、Reactや他のフレームワークを使用せずに、部品思想に基づいたウェブページを構築する手法を詳細に解説します。HTML、CSS、JavaScriptを活用して、再利用可能なUIコンポーネントを作成し、メンテナンスが容易で拡張性の高いウェブページの開発を目指します。初心者でも簡単にモジュール式の設計を実現できるように、実際のコード例とともに効果的なウェブ開発手法を紹介します。

## 作りたい成果物
Windowsのカレンダーと類似したUIコンポーネントを使用してウェブページを構築します。以下のウェブページから予定の追加と確認ができるようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2625874/b60852f5-bc21-f52c-0337-92dea25a0ed2.png)

## 作り方
以下の手順でウェブページを作成します：
- 部品用のモジュールを作成します。
- 部品間でデータを共有するためのモジュールを作成します。
- これらの部品を組み合わせてウェブページを構成します。

## 実装
### ComBase(部品用共有のモジュール)
`ComBase`クラスはJavaScriptでカスタムWebコンポーネントを作成するための基底クラスです。このクラスは`HTMLElement`クラスを継承し、標準のHTML要素に追加機能を提供するカスタム要素を作成する基礎を提供します。

#### ソースコード
**ファイル名**：com_base.js
```javascript
class ComBase extends HTMLElement {
    constructor() {
        super(); // HTMLElementのコンストラクタを呼び出し
        this.shadow = this.attachShadow({ mode: "open" });
        this.eventHandlerInfos = [] // イベント情報を格納します。
        this.dom_text = "" // layout info
        this.data = null;
        //const shadow = this.attachShadow({ mode: "open" });
        this.initLayout();
        
    }

    others() {

    }
    setDomText(value) {
      //this.dom_text = value;
      this.shadowRoot.innerHTML =  value;//this.dom_text;
      
    }

    initLayout() {

    }    
    setData(value) {
      this.data = value;
    }
   
    initEventHandler() {

    }

    
    async connectedCallback() {
      await this.render();
      await this.initEventHandler();
      await this.HandleEvent();
      await this.others();
      
  }

    render(){
        
    }
    
    addEventItem( event_item ) {
      this.eventHandlerInfos.push( event_item )
    }

    HandleEvent() {
      for ( let i = 0; i < this.eventHandlerInfos.length; i++ ) {
        var event_handler = this.eventHandlerInfos[i];
        this.shadowRoot.querySelector(event_handler.getElementId()).
        addEventListener(event_handler.getEvent(), event_handler.getEventHandler() );
      }
      
    } 
}
```
#### 説明
##### コンストラクタ
- `super();`は親クラスである`HTMLElement`のコンストラクタを呼び出し、カスタム要素の基本的な機能を初期化します。
- `this.shadow = this.attachShadow({ mode: "open" });`はShadow DOMを有効にし、カプセル化されたDOMツリーをこの要素に追加します。`mode: "open"`により、JavaScriptからShadow DOMにアクセス可能となります。
- `this.eventHandlerInfos = []`はイベントハンドラ情報を格納するための配列を初期化します。
- `this.dom_text = ""`はカスタム要素のDOM内のHTMLテキストを格納するためのプロパティです。
- `this.data = null;`はカスタム要素が扱うデータを保持するためのプロパティです。
- `this.initLayout();`はカスタム要素のレイアウトを初期化するためのメソッドを呼び出します。

##### メソッド
- `setDomText(value)`は、`this.dom_text`の値を更新し、Shadow DOMの`innerHTML`に値を設定します。
- `initLayout()`はカスタム要素の初期レイアウトを設定する場所ですが、実装は派生クラスで行われるべきです。
- `setData(value)`は`this.data`プロパティに値を設定します。
- `initEventHandler()`はイベントハンドラを初期化しますが、具体的な実装は含まれていません。
- `connectedCallback()`はカスタム要素がDOMに接続された際に自動的に呼ばれるライフサイクルメソッドで、`render()`, `initEventHandler()`, `HandleEvent()`, `others()`を順に呼び出します。
- `render()`は、要素のレンダリングを行うための基本的なメソッドですが、具体的な内容は派生クラスで定義する必要があります。
- `addEventItem(event_item)`はイベントハンドラ情報を`eventHandlerInfos`配列に追加します。
- `HandleEvent()`は、`eventHandlerInfos`配列に格納されたイベントハンドラをDOM要素に適用します。各イベントハンドラは特定のDOM要素に対するイベントリスナを設定します。

### 日時部品

`ComDatetime`クラスは`ComBase`クラスを継承しており、基本的な機能を拡張して、現在の日時を表示するカスタムWebコンポーネントを提供します。以下にクラスの主要な部分を説明します。

#### ソースコード
**ファイル名**: `com_datetime.js`
```javascript
class ComDatetime extends ComBase {
    initLayout() {
                  
      var dom_text = `
      <style>
          /* スタイル定義 */
          #current_time {
              font-size: 33px;
              margin: 5px 0;
            }
            
           #today {
              font-size: 13px;
              color: #007bff;
            }

            
            
      </style>
          <span id="current_time"></span>
          <span id="today"></span>
      `;
      
      this.setDomText( dom_text );

    }

    render() {
      this.render_time();
      this.render_date();
    }
    
    render_time() {
      const timeElement = this.shadowRoot.querySelector("#current_time");
      if ( timeElement !== null && timeElement !== undefined ) {
        const now = new Date();
      const hour = now.getHours().toString().padStart(2, "0");
      const minute = now.getMinutes().toString().padStart(2, "0");
      const second = now.getSeconds().toString().padStart(2, "0");
      timeElement.textContent = `${hour}:${minute}:${second}`;

      }
      
    };

    others() {
      setInterval(this.render_time.bind(this), 1000);
    }

    render_date() {
      const todayElement = this.shadowRoot.querySelector("#today");
      if ( todayElement !== null && todayElement !== undefined ) { 
        const now = new Date();
        const year = now.getFullYear();
        const month = now.getMonth() + 1;
        const day = now.getDate();
        todayElement.textContent = `${year}年${month}月${day}日`;
      }
    };

    
}

// カスタム要素を定義 ('my-input-button'タグ名でMyInputButtonクラスを使用)
customElements.define('com-datetime', ComDatetime);
```

#### 説明
##### initLayout メソッド
このメソッドは、`ComBase`から継承された`initLayout`メソッドをオーバーライド（上書き）しています。具体的には、HTMLとCSSを含むテンプレート文字列を定義し、その内容を`setDomText`メソッドを用いてShadow DOMに注入します。このHTMLには現在の時間を表示する`current_time`と今日の日付を表示する`today`の2つの`span`タグが含まれています。

##### render メソッド
`render`メソッドは、時間と日付の表示を更新するために、`render_time`と`render_date`メソッドを呼び出します。

##### render_time メソッド
このメソッドは1秒ごとに現在の時間を更新し、`current_time`要素のテキストコンテンツとして表示します。`setInterval`を使用して1秒ごとにこのメソッドが呼び出されるように設定されています。

##### render_date メソッド
現在の日付を`today`要素に表示します。このメソッドは日付が変わるごとに更新する必要があるため、初期レンダリング時に一度だけ呼び出されれば十分です。

##### others メソッド
`ComBase`クラスで定義されている`others`メソッドをオーバーライドして、`render_time`メソッドを1秒ごとに呼び出すための`setInterval`を設定しています。これにより、時間がリアルタイムで更新され続けます。

##### カスタム要素の定義
`customElements.define('com-datetime', ComDatetime);`は、このクラスを`com-datetime`というカスタム要素として定義しています。これにより、HTMLファイル内で`<com-datetime></com-datetime>`というタグを使用することで、このカスタム要素を利用できるようになります。

### カレンダー部品
**ファイル名**: `com_calendar.js`

#### ソースコード
```javascript
class ComCarlendar extends ComBase {
    constructor() {
        super(); // HTMLElementのコンストラクタを呼び出し
        
        this.target_date = new Date();
        this.pre_selected_td = null;
        
    }

    initLayout() {
      var dom_text = `<style>
            /* スタイル定義 */
            #current_time {
                font-size: 33px;
                margin: 5px 0;
              }
            
              .highlight {
                border: 2px solid #0078EF; /* 赤い輪郭線を表示 */
              }
           
              
             .today {
                font-size: 13px;
                background-color: #007bff;

              }

              #calendar {
                margin: 0 auto;
                width: 300px;
              }
            
              #calendar td {
                padding: 8px;
                text-align: center;
                
              }
            
              #calendar td:hover {
                border: 1px solid #888888; /* 灰色輪郭線を表示 */
                cursor: pointer;
                
              }
            
              .button-group {
                text-align: right;
            }
            
            /* ボタンスタイル */
            .btn {
                display: inline-block;
                font-size: 14px;
                text-align: center;
                text-decoration: none;
                cursor: pointer;
                border: 1px solid #ccc;
                border-radius: 4px;
                background-color: #007bff;
                color: #fff;
            }
            .btn-secondary {
                background-color: #6c757d;
            }
            .year_month_row {
              display: flex;
              justify-content: space-between;
            }
              
        </style>
        
        <div class = "year_month_row">
            <!-- 左寄せの年月 -->
            <div style="text-align: left;"> 
              <span  id="year-month"></span>
            </div>
            <!-- ボタン２つを右寄せしたグループ -->
            <div class="button-group">
                <button type="button" class="btn" id="prev-month">前月</button>
                <button type="button" class="btn" id="next-month" class="btn-secondary">次月</button>
            </div>
        </div>
        <table id="calendar"></table>
        `;
        this.setDomText( dom_text );
    }
    
    initTDsEventHandler() {
      var this_=this;
      this.shadowRoot.querySelectorAll('#calendar td').forEach(e => {
        var day = e.getAttribute('data-item');
        if (day !== null && day !== undefined) {
          //var day_items = day.split('_');
          e.addEventListener("click", this_.clickDate.bind(this));
        }
      });
    }
    initEventHandler() {
      
      var eventItem = new ComEventModel("#prev-month","click", this.clickPreMonth.bind(this));
      this.addEventItem( eventItem );

      eventItem = new ComEventModel("#next-month","click", this.clickNextMonth.bind(this) );
      this.addEventItem( eventItem );
      this.initTDsEventHandler();
      
      
    }
    
    
    async clickPreMonth() {
      
      var target_date = this.target_date;
      
      var month = target_date.getMonth()
      var year = target_date.getFullYear()
      if ( month === 0  ) {
        month = 11;
        year = year -1;
      } else {
        month = month -1;
      }
      var target_date_n = new Date( year, month, 1 )
      this.target_date = target_date_n;
      await this.render(  );

      this.pre_selected_td = null;
      await this.initTDsEventHandler();
      
    }

    async clickNextMonth() {
      //const year_month = this.shadowRoot.querySelector('#year-month').value;
      
      var target_date = this.target_date;
      
      var month = target_date.getMonth()
      var year = target_date.getFullYear()
      if ( month === 11  ) {
        month = 0;
        year = year + 1;
      } else {
        month = month + 1;
      }
      var target_date_n = new Date( year, month, 1 )
      this.target_date = target_date_n;
      await this.render(  );

      this.pre_selected_td = null;
      await this.initTDsEventHandler();
    }

    render(){
        
        var calendarBody = this.renderCalendar(this.target_date );
        const calendarElement = this.shadow.querySelector("#calendar");
        calendarElement.innerHTML = calendarBody;
          
        var year_month_text = this.render_year_month(this.target_date);
        const yearmonthElement = this.shadow.querySelector("#year-month");
        yearmonthElement.textContent = year_month_text;
    }
    
    render_year_month(target_date) {
      if (target_date === undefined || target_date === null  ) {
          return '';    
      } else {
          //this.year_month_ele.textContent 
          return `${target_date.getFullYear()}年${target_date.getMonth() + 1}月`;    
      }
      
    }
    renderCalendar(target_date_in) {
      // �J�����_�[�̃w�b�_�[�𐶐�
      var header = `<tr>
      <th>日</th>
      <th>月</th>
      <th>火</th>
      <th>水</th>
      <th>木</th>
      <th>金</th>
      <th>土</th>
    </tr>`;
      //calendarElement.innerHTML = header;
      var target_date = get_target_day(target_date_in);
      // ���̏����̓��t���擾
      var year = target_date.getFullYear();
      var month = target_date.getMonth()
      //today = target_date.getDate()
      var firstDayOfMonth = new Date(year, month , 1);
      //var callback = this.clickDate;
    
      // ���̍ŏI���̓��t���擾
      var lastDayOfMonth = new Date(year, month + 1, 0);
    
      // �J�����_�[�̓��t�𐶐�
      var calendarBody = '';
      var currentRow = '<tr>';
      for (var i = 0; i < firstDayOfMonth.getDay(); i++) {
        currentRow += '<td></td>';
      }
    
      for (var day = 1; day <= lastDayOfMonth.getDate(); day++) {
        var currentDate = new Date(year, month , day);
        var current_day = new Date(current_date.getFullYear(), current_date.getMonth(), current_date.getDate() )
        var cellClass = (currentDate.toDateString() === current_day.toDateString() + '') ? 'today' : '';
    
        //currentRow += `<td class="${cellClass}" onclick="clickdate(${year}, ${month}, ${day})">${day}</td>`;
        currentRow += `<td class="${cellClass}" data-item = "${year}_${month}_${day}">${day}</td>`;
    
        if (currentDate.getDay() === 6) {
          currentRow += '</tr>';
          calendarBody += currentRow;
          currentRow = '<tr>';
        }
      }
    
      
      calendarBody += currentRow + '</tr>';
      return header + calendarBody;
    }

    
    clickDate(e){
      var obj = e.target;
      if ( obj === null || obj === undefined ) {
        return ;
      }
      var day = obj.getAttribute('data-item');
      var day_items = day.split('_');
      var year = day_items[0];
      var month = day_items[1];
      var day = day_items[2];
      var date = new Date(year, month , day);
      const event = new CustomEvent('shareData', { detail: date, bubbles: true });
      const input_event = document.querySelector('com-inputevent');
      // イベントをディスパッチ
      input_event.shadowRoot.dispatchEvent(event);
      if ( 'event_infos' in event_infos ) {
        event_infos = event_infos['event_infos'];
        
        if (date.toDateString() in event_infos ) {
          event_info_oneday = event_infos[date.toDateString()];
          input_event.set_eventinfo( event_info_oneday );
        }
        
      }

      if ( this.pre_selected_td !== null && this.pre_selected_td !== undefined ) {
        this.pre_selected_td.classList.remove('highlight');
      } 
      obj.classList.add('highlight');
      this.pre_selected_td = obj;


      
    }

}


// 今日
var current_date = new Date();
//カレンダーで対象日を選びました
//var selected_day = null; 
function get_target_day(target_date_in) {
    return (target_date_in === null || target_date_in === undefined) ? current_date  : target_date_in;
}

function set_target_day(in_date) {
    //var target_date = (selected_day === null || selected_day === undefined) ? current_date  : selected_day;
    selected_day = in_date;
}




// カスタム要素を定義 ('my-input-button'タグ名でMyInputButtonクラスを使用)
customElements.define('com-carlendar', ComCarlendar);
```
#### 説明
##### コンストラクタ
- `super();` により `HTMLElement` のコンストラクタが呼び出され、基本的なHTML要素の機能が初期化されます。
- `this.target_date = new Date();` は、現在の日付を保持し、カレンダー表示の基準となる日付を設定します。
- `this.pre_selected_td = null;` は、前回選択された日付を保持するプロパティです。

##### initLayout メソッド
カレンダーの初期レイアウトを設定します。スタイル定義とHTMLマークアップを含むテンプレート文字列を `setDomText` メソッドを使って Shadow DOM に注入します。

##### initTDsEventHandler メソッド
カレンダー内の日付（`td`要素）ごとにクリックイベントハンドラを設定します。これにより、ユーザーが特定の日付をクリックした際の動作を定義できます。

##### initEventHandler メソッド
「前月」と「次月」ボタンのクリックイベントを設定し、カレンダーの月を移動する機能を提供します。また、`initTDsEventHandler` を呼び出して、動的に生成された日付要素に対してイベントハンドラを再設定します。

##### clickPreMonth と clickNextMonth メソッド
これらのメソッドは、それぞれ「前月」ボタンと「次月」ボタンがクリックされたときに実行されます。内部でカレンダーの表示を更新し、新しい月の日付を表示します。

##### render メソッド
カレンダーのHTMLを生成し、`renderCalendar` メソッドによって生成されたカレンダーボディを Shadow DOM の `#calendar` に設定します。また、現在の年月を表示する部分も更新します。

##### render_year_month メソッド
現在の年月をフォーマットして返します。これはカレンダーのヘッダ部分で表示されます。

##### renderCalendar メソッド
指定された日付のカレンダーを生成します。これには各週の開始曜日から計算して、適切な日付を配置するロジックが含まれます。

##### clickDate メソッド
ユーザーが特定の日付をクリックしたときに実行されるイベントハンドラです。選択された日付に対して特定のスタイルを適用し、前回選択された日付のスタイルを解除します。また、カスタムイベントを発火させて、他のコンポーネントとのデータ共有を可能にします。

##### カスタム要素の定義
最後に、`customElements.define('com-carlendar', ComCarlendar);` により、このクラスを `com-carlendar` という名前のカスタム要素として登録します。これにより、HTML内で `<com-carlendar></com-carlendar>` タグを使用してカレンダーコンポーネントを利用できるように

### イベント部品
`ComInputEvent` クラスは `ComBase` クラスを継承しており、イベントやリマインダーを入力し管理するためのカスタムウェブコンポーネントを提供します。このクラスは特にイベント管理のUIと機能を充実させています。以下はクラスの主要な特徴と機能についての説明です。
`ファイル名`：com_input_event.js
#### ソース
```js
class ComInputEvent extends ComBase {
    initLayout() {
      var dom_text = `<style>
            /* スタイル定義 */
            .time-location {
              display: flex;
              align-items: center;
              margin-bottom: 20px;
            }
            .input-field {
              padding: 5px 10px;
              width: 100%;
              margin-right: 10px;
              border: none;
              border-radius: 5px;
              background: #444;
              color: white;
            }
            .button-group {
              
              position: relative;
              right: 20px;
              display: flex;
              float: right;
            }
            .button {
              padding: 10px 20px;
              border: none;
              border-radius: 5px;
              margin-left: 10px;
              cursor: pointer;
            }
            .save-button {
              background: #4CAF50;
              color: white;
            }
            .hidden_input_event, .hidden_event_list, .hidden_button-group {
              display : none;  
            }
            
            .window {
              border-radius: 10px;
              width: 300px;
              margin-top: 20px;
              display: flex;
              flex-direction: column;
            }
              
        </style>
        
      <div class="window">
        <span id = "today_or_weekday" class="value" bind-text="current_day">今日</span>
        <input id = 'input_event' class="input-field title" placeholder="インベントまたはリマインドを追加します">
        <span id = "no_event">イベントなし</span>
        <div class = "hidden_input_event">
          <div class="time-location">
            <input type="time" class="input-field time-start" value="16:00">
            <span>から</span>
            <input type="time" class="input-field time-end" value="17:00">
          </div>
          <div class="time-location">
            <input type="text" class="input-field location-input" placeholder="場所を追加">
          </div>
        </div>  
        <div class = "hidden_event_list">
          <table id="event_list"></table>
        </div>
      
        <div class="hidden_button-group">
          <button id="save_event" class="button save-button">保存</button>
          <!-- <button id= "event_detail" class="button detail-button">詳細</button>
          --!>
        </div>
        
      </div>
        `;
        this.setDomText( dom_text );
    }
    
    initEventHandler() {
      
      var eventItem = new ComEventModel("#input_event","change", this.input_event.bind(this));
      this.addEventItem( eventItem );

      var save_btn = new ComEventModel("#save_event","click", this.save_event.bind(this));
      this.addEventItem( save_btn );

      this.shadowRoot.addEventListener('shareData', this.getDate.bind(this));
    

    }
    
    input_event() {
      const div_input_event = this.shadowRoot.querySelector('.hidden_input_event');
      div_input_event.style.display = 'block'; 

      const div_eventlist = this.shadowRoot.querySelector('.hidden_event_list');
      div_eventlist.style.display = 'none'; 

      const no_event_ele = this.shadowRoot.querySelector('#no_event');
      no_event_ele.style.display = 'none'; 

      const save_btn = this.shadowRoot.querySelector('.hidden_button-group');
        save_btn.style.display = 'block';

      
    }

    save_event() {
      let event_info = {};

      
      const event_title = this.shadowRoot.querySelector('#input_event').value;
      event_info['title'] = event_title;
      
      // イベントの開始時間を取得
      const start_time = this.shadowRoot.querySelector('.time-start').value;
      event_info['start_time'] = start_time;
      // イベントの終了時間を取得
      const end_time = this.shadowRoot.querySelector('.time-end').value;
      event_info['end_time'] = end_time;
      // イベントの場所を取得
      const location = this.shadowRoot.querySelector('.location-input').value;
      event_info['location'] = location;

      if ( event_infos && this.current_day.toDateString() in event_infos ) {
        event_infos[this.current_day.toDateString()].push( event_info );
      } else {
        event_infos[this.current_day.toDateString()] = [event_info];
      }
            
    }

    // set_eventinfo ( value ) {
    //   this.event_info =value;
    // }

    others() {
      let date = new Date();
      this.current_day = new Date( date.getFullYear(), date.getMonth(), date.getDate() );

    }
    render_today( target_date ) {
      
      if ( target_date !== undefined && target_date !== null ) {
        this.current_day = target_date;
        var weekday  = getDayOfWeek( target_date );
        var day = target_date.getDate();

        const span_today_weekday = this.shadowRoot.querySelector('#today_or_weekday');
            if ( span_today_weekday !== null && span_today_weekday !== undefined ) {
              let current_day = new Date();
              if ( target_date.toDateString() === current_day.toDateString() )
                return "今日";    
              return `${weekday}曜日 ${day}`;  
        }

      }
      
    }
    getDate( e ) {
      
      //console.log('Received data:', e.detail)
      const app = this.shadowRoot.querySelector('.window');
      let data = { 'current_day': e.detail }; // countをdataオブジェクトのプロパティとして宣言
      const binder = new DataBinder(app, data, this.render_today.bind(this) );
      
      const table_body = this.display_eventinfo();
      
      
      if ( table_body ) {
        const div_input_event = this.shadowRoot.querySelector('.hidden_input_event');
        div_input_event.style.display = 'none'; 

        const save_btn = this.shadowRoot.querySelector('.hidden_button-group');
        save_btn.style.display = 'none'; 

        const event_list = this.shadowRoot.querySelector('.hidden_event_list');
        event_list.style.display = 'block'; 

        const event_list_table = this.shadowRoot.querySelector('#event_list');
        event_list_table.innerHTML = table_body;
      
        const no_event = this.shadowRoot.querySelector('#no_event');
        no_event.style.display = 'none'; 

      } else {
        const no_event_ele = this.shadowRoot.querySelector('#no_event');
        no_event_ele.style.display = 'block'; 

        const div_input_event = this.shadowRoot.querySelector('.hidden_input_event');
        div_input_event.style.display = 'none'; 

        const event_list = this.shadowRoot.querySelector('.hidden_event_list');
        event_list.style.display = 'none'; 

        const save_btn = this.shadowRoot.querySelector('.hidden_button-group');
        save_btn.style.display = 'none';
      }
      
      
      binder.emit('change', { property: 'current_day' }); // 

    }

    display_eventinfo() {
      var header = `<tr>
          <th>タイトル</th>
          <th>開始時間</th>
          <th>終了時間</th>
          <th>場所</th>
          
        </tr>`;
      var table_body = '';  
      if ( event_infos ) {
          const events = event_infos[this.current_day.toDateString()];
          if ( events  ) {
            events.forEach(event => {
              var currentRow = '<tr>'
              var event_tile = event['title'];
              var start_time = event['start_time'];
              var end_time = event['end_time'];
              var location = event['location'];
              currentRow += `<td>${event_tile}</td>`;
              currentRow += `<td>${start_time}</td>`;
              currentRow += `<td>${end_time}</td>`;
              currentRow += `<td>${location}</td>`;
              
              currentRow += '</tr>';
              table_body += currentRow;
              
              
          
        });
          
      } 
      if ( table_body !== '' ) {
        return header+ table_body;
      }
      return '';
    }
  }
    
}


function getDayOfWeek(target_day) {
    
  // 曜日の番号を取得 (0: 日曜日, 1: 月曜日, ..., 6: 土曜日)
  var dayOfWeek = target_day.getDay();

  // 曜日を表す文字列を返す
  var days = ['日', '月', '火', '水', '木', '金', '土'];
  return days[dayOfWeek];
}


// カスタム要素を定義 ('my-input-button'タグ名でMyInputButtonクラスを使用)
customElements.define('com-inputevent', ComInputEvent);
```
#### 説明

##### initLayout メソッド
このメソッドは、カスタムエレメントの初期HTMLとスタイルを定義します。スタイルでは、入力フィールドやボタンのデザインを定義し、HTMLでは日付、イベント入力欄、場所指定欄などのインターフェースを設定します。特に、一部の要素はデフォルトで非表示（`display: none;`）に設定され、特定のアクションに応じて表示されるようになっています。

##### initEventHandler メソッド
このメソッドは、各種イベントハンドラを設定します。主にイベント入力フィールドの変更や保存ボタンのクリック、およびカスタムイベント `shareData` のリスニングを含みます。これにより、ユーザーの操作に応じて適切な処理が行われます。

##### input_event メソッド
イベント入力フィールドが変更されたときに呼び出されるメソッドです。このメソッドでは、イベント詳細入力エリアを表示し、保存ボタンをアクティブにするなどのUI変更を行います。

##### save_event メソッド
イベントの情報を収集し、内部データ構造に保存します。このメソッドでは、イベントのタイトル、開始時間、終了時間、場所などの情報を収集し、それらを日付をキーとするデータストアに格納します。

##### getDate メソッド
`shareData` イベントを受け取ると呼び出され、選択された日付に応じて表示を更新します。特に、選択された日付に関連するイベントがある場合は、それらをリストアップし表示します。イベントがない場合は「イベントなし」と表示します。

##### display_eventinfo メソッド
指定された日付に関連するイベント情報を表示するためのテーブルを生成します。各イベントについて、タイトル、開始・終了時間、場所を列挙します。

##### render_today メソッド
選択された日付を「今日」または曜日と日付の形式で表示します。この表示はユーザーが現在どの日付を見ているかを明確にするために使用されます。

##### getDayOfWeek 関数
日付から曜日を求めるヘルパー関数です。この関数は日付を受け取り、対応する曜日の文字列（例: "月", "火"）を返します。

##### カスタム要素の定義
`customElements.define('com-inputevent', ComInputEvent);` により、このクラスを `com-inputevent` というタグ名でカスタムエレメントとして登録します。これにより、HTML内で `<com-inputevent></com-inputevent>` タグを使用してイベント入力コンポーネントをページに組み込むことができます。

### Webページ
**ファイル名**：`カレンダー.html`
#### ソース
```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Windows風カレンダー</title>
  <link rel="stylesheet" href="calendar.css">
  <script src="./component/com_base.js"></script>
  <script src="./component/com_model.js"></script>
  <script src="./component/com_carlendar.js"></script>
  <script src="./component/com_datetime.js"></script>
  <script src="./databind/event_emitter.js"></script>
  <script src="./databind/data_bind.js"></script>
  <script src="./component/com_input_event.js"></script>
  <script src="./app.js"></script>

  
  <script>
    function toggleVisibility() {
      var container = document.querySelector('#event_info');
      container.style.display = container.style.display === 'none' ? 'block' : 'none';
    }
  </script>

</head>
<body>
  <div class = "container">
    <div class = "calendar-container">
      <com-datetime class="date-container"></com-datetime>
      <hr class="hr">
      <com-carlendar ></com-carlendar>
      
    </div>  
    <div id = "event_info" class = "event_container">
      <hr class="hr">
      <com-inputevent ></com-inputevent>

    </div>
    <div class="toggle-text" onclick="toggleVisibility()">クリックで表示・非表示</div>
</body>
</html>

```
### その他のソース

#### CSS
**ファイル名**：`calendar.css`
```html
body, html {
  max-height: 737px; /* Full height */
  max-width: 300px; /* Full height */
    margin: 0; /* Reset default margin */
  background-color: #333; /* Dark background */
  color: white; /* White text */
  font-family: Arial, sans-serif; /* Sans-serif font */
}

.container {
  position: absolute; /* Positioning relative to the nearest positioned ancestor */
  right: 0; /* Align to the right */
  bottom: 0; /* Align to the bottom */
  width: 300px; /* Fixed width */
  padding: 20px; /* Padding inside the container */
}

  .calendar-container {
    color: white;
    width : 300px;
    
  }

  .year_month_row {
    display: flex;
    justify-content: space-between;
  }

  .date-container {
    display: flex;
    flex-direction: column;
    align-items: left;
  }

  .change_month {
    flex-direction: row;
    display: flex;
  }

  #current-time {
    font-size: 18px;
    margin: 10px 0;
  }

  #today {
    font-size: 13px;
    color:#007bff;
  }

  .today {
    font-size: 15px;
    background-color:#007bff;
  }

  

  #calendar {
    border-collapse: collapse;
    margin: 0 auto;
    width: 300px;
  }

  #calendar td {
    padding: 8px;
    text-align: center;
  }

  #calendar td:hover {
    background-color: blue;
    cursor: pointer;
  }

  .button-group {
    text-align: right;
}

/* ボタンスタイル */
.btn {
    display: inline-block;
    font-size: 14px;
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    border: 1px solid #ccc;
    border-radius: 4px;
    background-color: #007bff;
    color: #fff;
}
.btn-secondary {
    background-color: #6c757d;
}

.event_container {
  height: 247px; /* Fixed width */

  margin: 0 auto; /* Center the container */

}

.toggle-text {
  text-align: right;
  right: 20px;
  cursor: pointer;
}
```
#### 部品間データ共有
詳細を以下の投稿を参照ください
[純粋なJavaScriptで実現するデータバインディング](https://qiita.com/SimonLee/items/a87bd22d097be8c05962)

#### ComEventModel
**ファイル名**:`com_model.js`
```javascript
class ComEventModel {
    constructor(element_id, event, event_handler) {
        //event op name, 例えば：click
        this.event = event;
        this.element_id = element_id;
        this.event_handler = event_handler;
    }

    // 配列またはオブジェクトで開始時間と終了時間を返す
    getEvent() {
        return this.event
    }

    setEvent(value) {
        this.event = value;
    }

    getElementId() {
      return this.element_id;
    }

    setElementId(value) {
      this.element_id = value;
    }

    getEventHandler() {
      return this.event_handler;
    }

    setEventHandler(value) {
      this.event_handler = value;
    }

    
}
```
#### app.js
```java
let event_infos = {};
```
### ソースフォルダー構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2625874/defb4cb2-68c0-307d-a3a0-f0a9456da082.png)