type PatchState = { readonly clickEventHandler: (path: string, mouseEvent: MouseEvent) => void; readonly changeEventHandler: (path: string) => void; readonly inputEventHandler: (path: string, inputEvent: InputEvent) => void; readonly setMessageDataMap: (newMap: NewMessageMap) => void; }; type ClickMessageData = { readonly stopPropagation: boolean; readonly message: Message; readonly url: string | null; }; type PathAndMessageData = { readonly path: string; readonly messageData: MessageData; }; type StateAndMessageList = { readonly state: State; readonly messageList: ReadonlyArray; }; type ClientStartOption = { readonly initStateAndMessageList: StateAndMessageList; readonly stateToView: (stateValue: State) => View; readonly renderView: ( viewValue: View, patchState: PatchState ) => void; readonly update: (messageValue: Message, stateValue: State) => State; readonly urlChangeMessageData: (pathAndSearchParams: { readonly path: string; readonly searchParams: string; }) => Message; }; type NewMessageMap = { readonly click: ReadonlyArray>>; readonly change: ReadonlyArray>; readonly input: ReadonlyArray< PathAndMessageData<(newText: string) => Message> >; }; export const start = ( option: ClientStartOption ): void => { /** * applyViewをする前に事前に実行する必要あり * * イベントのコールバックの関数の作成と, メッセージの登録をするオブジェクトを作成する. */ const createPatchState = (): PatchState => { let clickMessageDataMap: ReadonlyMap< string, ClickMessageData > = new Map(); let changeMessageDataMap: ReadonlyMap = new Map(); let inputMessageDataMap: ReadonlyMap Message> = new Map(); return { clickEventHandler: (path: string, mouseEvent: MouseEvent): void => { const messageData = clickMessageDataMap.get(path); console.log("クリックを検知した!", path, mouseEvent, messageData); if (messageData === undefined) { return; } if (messageData.stopPropagation) { mouseEvent.stopPropagation(); } if (typeof messageData.url === "string") { history.pushState(undefined, "", messageData.url); } pushMessageList(messageData.message); }, changeEventHandler: (path: string) => { const messageData = changeMessageDataMap.get(path); if (messageData === undefined) { return; } pushMessageList(messageData); }, inputEventHandler: (path: string, inputEvent: InputEvent) => { const messageData = inputMessageDataMap.get(path); if (messageData === undefined) { return; } pushMessageList( messageData((inputEvent.target as HTMLInputElement).value) ); }, setMessageDataMap: (newMessageMap) => { clickMessageDataMap = new Map( newMessageMap.click.map((e) => [e.path, e.messageData]) ); changeMessageDataMap = new Map( newMessageMap.change.map((e) => [e.path, e.messageData]) ); inputMessageDataMap = new Map( newMessageMap.input.map((e) => [e.path, e.messageData]) ); }, }; }; /** * メッセージキューにメッセージを追加 */ const pushMessageList = (message: Message): void => { messageList.push(message); }; /** * メインループ! */ const loop = (): void => { requestAnimationFrame(loop); if (messageList.length === 0) { return; } console.log("handle message!", [...messageList]); while (true) { const message = messageList.shift(); if (message === undefined) { break; } state = option.update(message, state); } const newView = option.stateToView(state); console.log({ state, newView }); oldView = newView; option.renderView(newView, patchState); }; const stateAndMessageList = option.initStateAndMessageList; let state: State = option.initStateAndMessageList.state; /** * メッセージのキュー */ const messageList: Array = [ ...option.initStateAndMessageList.messageList, ]; let oldView = option.stateToView(stateAndMessageList.state); const patchState = createPatchState(); option.renderView(oldView, patchState); loop(); // ブラウザで戻るボタンを押したときのイベントを登録 window.addEventListener("popstate", () => { pushMessageList( option.urlChangeMessageData({ path: window.location.pathname, searchParams: window.location.search, }) ); }); };