Search code examples
javascriptmodelcontrollerdom-eventsevent-delegation

How to bind each array item to its element node representation and make the nodes watch the array?


I have an array, and I want to print them in div tags, and also when I make changes on the array, I want the change also to occur on divs (for example when I delete an item, div print of item also be deleted; or change the value of an item, expect thing happen to the div of the item). I made a little research and I found something I didn't know before that called Proxy object. I wrote the following code:

let fruits = [
  "Apple", "Pear", "Oak", "Orange", "Tangerine", "Melon",
  "Ananas", "Strawberry", "Cherry", "Sour Cherry","Banana",
  "Plum", "Pomegranate", "Apricot", "Peach", "Nectarine",
  "Kiwi", "Mulberry", "Fig", "Grapefruit", "Grape",
];
let trees = document.getElementById("trees");
let tree = new Array();

let aproxy = new Proxy(fruits, {
  get: (target, key, receiver) => {
    let treeDiv = document.createElement("div");
    treeDiv.classList.add("tree");

    let span = document.createElement("span");
    span.textContent = target[key] + " Tree";

    treeDiv.appendChild(span);
    trees.appendChild(treeDiv);

    treeDiv.addEventListener("dblclick", () => {
      fruits.splice(key, 1);

      //Here I delete all the item print divs before reprint all of them
      trees.querySelectorAll("div.tree").forEach(el => {
        trees.removeChild(el);
      });

      //Here I make the iteration to reprint
      fruits.forEach((el, index) => {
        aproxy[index];
      });
    });
    return treeDiv;
  },
  set: (target, key, value) => {
    fruits.push(value);
    aproxy[key];
    return true;
  },
});

fruits.forEach((el, index) => {
  aproxy[index];
});

let input = document.querySelector("#inputTag");

input.addEventListener("keyup", ev => {
  if (ev.code === "Enter") {
    aproxy[fruits.length] = input.value;
    console.log(fruits);
  }    
});
@import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,wght@0,200;0,300;0,400;0,600;0,700;1,200;1,300;1,400;1,600&display=swap');

body {
  background: linear-gradient(135deg,rgb(204, 64, 64),rgb(204, 64, 118));
  margin: 0 0 100px 0;
  background-attachment: fixed;
  zoom: .7;
}
.as-console-wrapper { height: 100px!important; }

#container {
  width: 100%;
  margin: auto;
  padding: 10px;
}
#input {
  width: 93%;
}

#input input {
  border:none;
  padding: 17px 20px;
  margin: 0;
  border-radius: 15px;
  display: block;
  width: 100%;
  font-size: 14pt;
  font-family: 'Nunito Sans', sans-serif;
  font-weight: 300;
  box-shadow: 0 10px 10px rgba(0,0,0,0.1);
}
#input input::placeholder {
  font-family: 'Nunito Sans', sans-serif;
  font-size: 14pt;
  font-style: italic;
}
#input input:focus {
  outline: none;
}

#trees {
  display: flex;
  margin-top: 15px;
  flex-wrap: wrap;
  width: 100%;
  justify-content: center;
}
#trees .tree {
  padding: 10px;
  background: rgba(255,255,255,1);
  margin: 7px;
  border-radius: 10px;
  cursor: pointer;
  transition: background .3s;
  user-select: none;
}
#trees .tree:hover {
  background: rgba(255,255,255,.1);
}
#trees .tree:hover span {
  color: #fff;
}
#trees .tree span {
  font-family: 'Nunito Sans', sans-serif;
  font-weight: 600;
  color: rgb(204, 64, 64);
  transition: color .3s;
}
<div id="container">
  <div id="input">
    <input placeholder="Enter a tree and press 'Enter'" type="text" name="" id="inputTag">
  </div>
  <div id="trees">
  </div>
</div>

As you can see, I reprint the divs when an item is deleted. Is there another way to do that by Proxy object without reprinting? Can I bond the array items to divs?


Solution

  • Another possible solution could be based on an hand-knitted model and controller logic.

    One would entirely separate the pure controller tasks, letting them only work directly with the DOM and with a modeled abstraction of the initially provided list items/values.

    The model itself could be e.g. a Map based registry which implements the logic of always being in control of the correct list state.

    Thus, in addition to the most obvious register/deregister methods, there will be sanitizing and check tasks that prevent e.g. double registering of (potentially) equal items/values. Such a registry model could also provide getters for special list representations of its registered items like e.g. providing the current array of just each item's text content or an array of each item's model.

    As for the latter, such a model in addition to e.g. its id and text value would also feature its own view, e.g. an elm reference of the to be rendered/removed element node.

    In order to keep each item specific DOM node and/or each item's model free of controller logic, the main controller task uses event delegation [1],[2] by listening to / handling the double-click event at the list's root-node exclusively.

    The next provided example code demonstrates how the main controller task operates both the DOM and the item list abstraction ...

    function sanitizeInputValue(value) {
      return value
        .trim()
        .replace(/\s+/g, ' ')
        .replace(/(?:\stree)+$/i, '');
    }
    
    function createItemNode(id, value) {
      const itemRoot = document.createElement('div');
    
      itemRoot.dataset.itemId = id;
      itemRoot.classList.add('item');
    
      let contentNode = document.createElement('span');
      contentNode.textContent = `${ value } Tree`;
    
      itemRoot.appendChild(contentNode);
    
      return itemRoot;
    }
    function createItemViewModel(id, value) {
      return {
        id,
        value,
        elm: createItemNode(id, value),
      };
    }
    
    function createItemListViewModel(itemList) {
      const registry = new Map;
    
      const register = value => {
        let viewModel;
    
        value = sanitizeInputValue(value);
        const id = value.toLowerCase();
    
        if (!registry.has(id)) {
          viewModel = createItemViewModel(id, value);
    
          registry.set(id, viewModel);
        }
        return viewModel;
      };
      const deregister = id => {
        let viewModel;
    
        if (registry.has(id)) {
          viewModel = registry.get(id);
    
          registry.delete(id);
        }
        return viewModel;
      };
      const getViewModels = () =>
        [...registry.values()];
      const getValueList = () =>
        [...registry].map(([id, viewModel]) => viewModel.value);
    
      // default register items from initial list.
      itemList.forEach(register);
    
      return {
        register,
        deregister,
        getViewModels,
        getValueList,
      }
    }
    
    
    function main/*Controller*/(itemList) {
      // create a view-model of the provided items by programmatically
      // adding each items own view-model to the overall items registry.
      const itemsRegistry = createItemListViewModel(itemList);
    
      let itemsRoot = document.querySelector("#item-list");
      let itemInput = document.querySelector("#item-input");
      
      // initial rendering of the provided items from each items's view model.
      itemsRegistry
        .getViewModels()
        .forEach(viewModel =>
          itemsRoot.appendChild(viewModel.elm)
        );
    
      // register controller logic for a list item's double-click handling.
      itemsRoot.addEventListener('dblclick', evt => {
        const target = evt.target.closest('.item');
        const itemId = target?.dataset.itemId;
    
        if (itemId) {
          // remove view-model from registry in case it does exist.
          const viewModel = itemsRegistry.deregister(itemId);
    
          if (viewModel) {
            // remove element node of a successfully removed view-model from DOM.
            viewModel.elm.remove();
    
            console.clear();
            console.log(itemsRegistry.getValueList());
          }
        }
      });
    
      // register controller logic for handling a potential new list item.
      itemInput.addEventListener('keyup', evt => {
        if (evt.code === "Enter") {
    
          // add view-model to registry in case it does not already exist.
          const viewModel = itemsRegistry.register(evt.currentTarget.value);
    
          if (viewModel) {
            // append element node of newly registered view-model to DOM.
            itemsRoot.appendChild(viewModel.elm);
    
            // sanitize any submitted input value.
            itemInput.value = viewModel.value;
    
            console.clear();
            console.log(itemsRegistry.getValueList());
          }
        }
      });
    };
    
    
    let fruits = [
      "Apple", "Pear", "Oak", "Orange", "Tangerine", "Melon",
      "Ananas", "Strawberry", "Cherry", "Sour Cherry","Banana",
      "Plum", "Pomegranate", "Apricot", "Peach", "Nectarine",
      "Kiwi", "Mulberry", "Fig", "Grapefruit", "Grape",
    ];
    main/*Controller*/(fruits);
    @import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,wght@0,200;0,300;0,400;0,600;0,700;1,200;1,300;1,400;1,600&display=swap');
    
    body {
      background: linear-gradient(135deg,rgb(204, 64, 64),rgb(204, 64, 118));
      margin: 0 0 100px 0;
      background-attachment: fixed;
      zoom: .7;
    }
    .as-console-wrapper { height: 100px!important; }
    
    #container {
      width: 100%;
      margin: auto;
      padding: 10px;
    }
    #input-wrapper {
      width: 93%;
    }
    
    #input-wrapper input {
      border:none;
      padding: 17px 20px;
      margin: 0;
      border-radius: 15px;
      display: block;
      width: 100%;
      font-size: 14pt;
      font-family: 'Nunito Sans', sans-serif;
      font-weight: 300;
      box-shadow: 0 10px 10px rgba(0,0,0,0.1);
    }
    #input-wrapper input::placeholder {
      font-family: 'Nunito Sans', sans-serif;
      font-size: 14pt;
      font-style: italic;
    }
    #input-wrapper input:focus {
      outline: none;
    }
    
    #item-list {
      display: flex;
      margin-top: 15px;
      flex-wrap: wrap;
      width: 100%;
      justify-content: center;
    }
    #item-list .item {
      padding: 10px;
      background: rgba(255,255,255,1);
      margin: 7px;
      border-radius: 10px;
      cursor: pointer;
      transition: background .3s;
      user-select: none;
    }
    #item-list .item:hover {
      background: rgba(255,255,255,.1);
    }
    #item-list .item:hover span {
      color: #fff;
    }
    #item-list .item span {
      font-family: 'Nunito Sans', sans-serif;
      font-weight: 600;
      color: rgb(204, 64, 64);
      transition: color .3s;
    }
    <div id="container">
      <div id="input-wrapper">
        <input placeholder="Enter a tree and press 'Enter'" type="text" id="item-input">
      </div>
      <div id="item-list">
      </div>
    </div>