List View UI Components Setup

December 12, 2024

Files

js/
  components/
    activity.component.js
    activity.list-view.js
  libs/
    list-container-builder.js
  main-component.js
  utils.js
index.html
index.js

Source Code

index.html

<div class="_listActivities"></div>

<!-- # templates -->
<template id="tmp-item-activity">
	<div data-slot="root" data-kind="item">
		<div data-slot="name"></div>
		<button data-action="edit">Edit</button>
		<button data-action="remove">Remove</button>
	</div>
</template>

utils.js

let utils = (function () {
	// # self
	let SELF = {
		DOMSlots,
	};

	// # function

	function DOMSlots(parentNode) {
		let slots = {};
		[...parentNode.querySelectorAll("[data-slot]"), parentNode].forEach(
			(node) => {
				let key = node.dataset?.slot;

				if (!key || slots[key]) return;

				slots[key] = node;
			}
		);
		return slots;
	}

	return SELF;
})();

libs/list-container-builder.js

function ListContainerBuilder(opt) {
  
  let $ = document.querySelector.bind(document);
  let containerEl = opt.container ? $(opt.container) : document.createDocumentFragment();
  let templateEl = $(opt.template);
  
  if (templateEl === null) throw new Error(`template not found with selector: ${opt.template}`);
   
  let SELF = {
    Refresh,
    RefreshSingle,
    AppendItems,
    SetContainer,
    GetContainer,
  };
  
  function SetContainer(el) {
    containerEl = el;
  }
  
  function GetContainer() {
    return containerEl;
  }
  
  function Refresh(items) {
    refreshListContainer(items, containerEl, templateEl, (clonedNode, item) => opt.builder(clonedNode, item));
  }
  
  function AppendItems(items) {
    let docFrag = document.createDocumentFragment();
    
    for (let item of items) {
      let clonedNode = templateEl?.content.cloneNode(true);
      opt.builder(clonedNode, item);
      docFrag.append(clonedNode);
    }
    
    containerEl?.append(docFrag);
  }
  
  function RefreshSingle(item) {
    let itemEl = opt.lookup?.(containerEl, item);
    if (!itemEl) return;
    
    opt.builder(itemEl, item);
  }
  
  function refreshListContainer(items, containerEl, templateEl, onItemClone) {
    let docFrag = document.createDocumentFragment();
    
    containerEl?.replaceChildren();
    
    if (items?.length > 0) {
      for (let item of items) {
        let clonedNode = templateEl?.content.cloneNode(true);
        let node = onItemClone(clonedNode, item);
        if (!node) continue;
        docFrag.append(node);
      }
    }
    
    containerEl?.append(docFrag);
  }
  
  return SELF;
}

index.js

{
	urls: [
    "js/libs/list-container-builder.js",
  ],
},
{
	urls: [
    "js/components/activity.list-view.js",
    "js/utils.js",
  ],
},

dom-events.js


let DOMEvents = (function() {
  
  const commonEventTypes = {
    onclick: 'click',
    ondblclick: 'dblclick',
    onmousedown: 'mousedown',
    onmouseup: 'mouseup',
    onmousemove: 'mousemove',
    onmouseover: 'mouseover',
    onmouseout: 'mouseout',
    ontouchstart: 'touchstart',
  
    onkeypress: 'keypress',
    onkeydown: 'keydown',
    onkeyup: 'keyup',
    onfocus: 'focus',
    onblur: 'blur',
    
    oninput: 'input',
    onchange: 'change',
    onsubmit: 'submit',
    onreset: 'reset'
  };

  function notImplemented() {
    console.error('Not implemented')
  }

  let attachListeners = function(attr, eventType, callbacks, containerEl) {
    let elements = containerEl.querySelectorAll(`[${attr}]`);
    for (let el of elements) {
      let callbackFunc = callbacks?.[el.getAttribute(attr)] ?? notImplemented;
      el.addEventListener(eventType, callbackFunc);
    }
  };
  
  function Listen(eventsMap, containerEl=document) {
    let {groupKey} = eventsMap;
    let infix = groupKey ? `-${groupKey}` : '';

    for (let key in eventsMap) {
      if (key == 'groupKey') continue;
      
      let callbackMap = eventsMap[key];
      let eventType = callbackMap.eventType ?? getCommonEventType(key);

      if (!eventType) {
        console.error('Event type not defined:', key);
        continue;
      }
      attachListeners(`data${infix}-${key}`, eventType, callbackMap, containerEl);
    }
  }
  
  function getCommonEventType(key) {
    return commonEventTypes[key];
  }
  
  return {
    Listen,
  };

})();


index.js

import { loadScripts } from "./script-loader.js";

(function () {
	loadScripts([
		{
			urls: [
                "js/utils/dom-events.js", 
                "js/events-map.js"
            ],
			callback: function () {
				DOMEvents.Listen(eventsMap);
			},
		},
/* ... */
	]);
})();

main-component.js

function Init() {
	listViewActivity.RefreshList();
}

components/activity.list-view.js

let listViewActivity = (function() {
    
    let $ = document.querySelector.bind(document);

    let SELF = {
      RefreshList,
      RefreshSingle,
      GetOptions,
      SetOptions,
    };
    
    // # local
    let local = {
        states: {
            isEventRegistered: false,
        },
    };

    // # list
    let listContainer = new ListContainerBuilder({
        container: '._listActivities',
        template: '#tmp-item-activity',
        builder: (node, item) => buildListItem(node, item),
        lookup: (containerEl, item) => containerEl.querySelector(`[data-id="${item.id}"]`),
    });

    // # function

    function GetOptions() {
        return local.options;
    }

    function SetOptions(options) {
        for (let key in options) {
            if (typeof(local.options[key]) != 'undefined') {
                local.options[key] = options[key];
            }
        }
    }

    function registerEventListeners() {
        if (local.states.isEventRegistered) return;

        local.states.isEventRegistered = true;
        let container = listContainer.GetContainer();

        container.addEventListener('click', HandleClickEvt);
    }

    function HandleClickEvt(evt) {
      let targetEl = evt.target;
      let itemEl = targetEl?.closest('[data-kind="item"]');
      let action = targetEl?.closest('[data-action]')?.dataset.action;
      
      if (!itemEl) return;
      
      handleClickAction(itemEl, action);
    }

    // # dom events, # events
    function handleClickAction(itemEl, action) {
      let data = {
        id: itemEl.dataset.id,
      };
      let {id} = data;

      switch (action) {
        case 'edit': compoActivity.Edit(id); break;
        case 'remove': compoActivity.Remove(id); break;
      }
    }

    // # refresh
    function RefreshList() {
        registerEventListeners();

        let items = servActivities.GetAll();
        listContainer.Refresh(items);
    }

    // # build
    function buildListItem(node, item) {
        let slots = utils.DOMSlots(node);
        let itemEl = slots.root;

        let {id, name} = item;

        slots.root.dataset.id = id;
        slots.name?.replaceChildren(name);

        return itemEl;
    }
    
    function RefreshSingle(id) {
        let item = servActivities.GetById(id);
        listContainer.RefreshSingle(item);
    }

    return SELF;
    
})();

components/activity.component.js

let SELF = {
  Edit,
  Remove,
};

// # function

function Edit(id) {
  let item = servActivities.GetById(id);
  let {name} = item;

  let userVal = window.prompt('Activity name', name);
  if (userVal === null) return;

  item.name = userVal;
  servActivities.Save_();

  listViewActivity.RefreshSingle(id);
}

function Remove(id) {
  let item = servActivities.GetById(id);
  let {name} = item;

  let isConfirm = window.confirm(`Delete activity: ${name}?`);
  if (!isConfirm) return;

  servActivities.Remove(id)
  servActivities.Save_();

  listViewActivity.RefreshList();
}

Comments

Thank You

for your visit