2
\$\begingroup\$

I have an array of users containing user name with their rules on my practice project
I have generated a table in HTML from that array in JS, So I can

change the status of any user rule by toggling on cell, or toggling all rules by clicking on name of user, or deny or grant rules for all users by clicking on column vertically!

BUT I want to update the original array users rules, if the rules of any user change;
In other words How can I make the array in JS synchronize with the table in HTML.

const users = [
  {
    name: "Admin",
    rules: { Create: true, Read: true, Update: true, Delete: true },
  },
  {
    name: "Developer",
    rules: { Create: true, Read: true, Update: false, Delete: true },
  },
  {
    name: "Customer",
    rules: { Create: false, Read: true, Update: false, Delete: false },
  },
];
let rules = ["Create", "Read", "Update", "Delete"];
let status = {
  "": "red",
  green: "red",
  red: "green",
};
let icons = {
  "": "🟥",
  green: "🟥",
  red: "🟩",
};
const table = document.createElement("table");
const thead = table.createTHead();
const tr = thead.insertRow();
const th = tr.appendChild(document.createElement("th"));
th.appendChild(document.createTextNode("Users"));
rules.forEach((rule, i) => {
  const th = tr.appendChild(document.createElement("th"));
  th.appendChild(document.createTextNode(rule));
  th.setAttribute("status", "");
  th.onclick = (e) => {
    const cols = [
      ...document.querySelectorAll(`tbody tr td:nth-child(${i + 2})`),
    ];
    cols.forEach((col) => {
      col.textContent = icons[e.target.getAttribute("status") ?? ""];
    });
    e.target.setAttribute(
      "status",
      status[e.target.getAttribute("status") ?? ""]
    );
  };
});
const tbody = table.createTBody();
users.forEach((user) => {
  const tr = tbody.insertRow();
  const td = tr.appendChild(document.createElement("td"));
  td.appendChild(document.createTextNode(user.name));

  td.onclick = (e) => {
    rulesUser = [...td.parentElement.children];
    rulesUser.slice(1).forEach((rule) => {
      rule.textContent = icons[e.target.getAttribute("status") ?? ""];
    });
    e.target.setAttribute(
      "status",
      status[e.target.getAttribute("status") ?? ""]
    );
  };
  rules.forEach((rule) => {
    const td = tr.insertCell();
    td.appendChild(
      document.createTextNode(
        Object.entries(user.rules)
          .filter((entry) => entry[1] == true)
          .map((entry) => entry[0])
          .includes(rule)
          ? "🟩"
          : "🟥"
      )
    );
    td.setAttribute(
      "status",
      Object.entries(user.rules)
        .filter((entry) => entry[1] == true)
        .map((entry) => entry[0])
        .includes(rule)
        ? "green"
        : "red"
    );
    td.onclick = (e) => {
      e.target.textContent = icons[e.target.getAttribute("status") ?? ""];
      e.target.setAttribute(
        "status",
        status[e.target.getAttribute("status") ?? ""]
      );
    };
  });
});
document.body.appendChild(table);
* {
  font-family: sans-serif;
}
table {
  border-collapse: collapse;
  border: 1px solid;
}
td,
th {
  padding: 5px 10px;
  text-align: center;
  border: 1px solid;
  cursor: pointer;
}

thead th {
  background-color: gray;
}
tbody th {
  background-color: lightgray;
}
thead th:first-child {
  background-color: lightblue;
}

Any feedback or suggestion to make the code better, I'd be really grateful if somebody could help me with this problem so I can continue building my practice project. Thanks ❤️

\$\endgroup\$
1
  • \$\begingroup\$ Do you have to build the table using JS createElement etc. or can you just write some html on the page? \$\endgroup\$ Jul 9 at 12:47

1 Answer 1

2
\$\begingroup\$

Personally I would break the code down into discrete functions that do specific peices of work.

I would opt to use the users object graph as the 'source of truth' as to what state each element is in, and use that to update the DOM. This can then be leveraged for both the initial setup and the click handlers.

I think generally speaking it not advised to use custom HTML attributes to convey state, or at least would should prefix them with the x- convention and possibly something to prevent attribute clashes with browser plugins that might be decorating DOM elements once the page is loaded. Another more accepted approach here is to use CSS classes and add/remove those classes to alter the visual state (show/hide elements for example).

The below code is an example, and its a bit quick and dirty to illustrate the point. Ideally I suspect I'd want to be encapsulating the whole thing in a class, passing it a DOM element to act as the container for all generated code. Rather than using globals for users, rules, state etc you'd want this in the class, or at least passed into the functions so that its clear what state each function is operating on.

const users = [
  {
    name: "Admin",
    rules: { Create: true, Read: true, Update: true, Delete: true },
  },
  {
    name: "Developer",
    rules: { Create: true, Read: true, Update: false, Delete: true },
  },
  {
    name: "Customer",
    rules: { Create: false, Read: true, Update: false, Delete: false },
  },
];
let rules = ["Create", "Read", "Update", "Delete"];

const state = ["🟥", "🟩"]

const ruleToggle = (index, enabled) => {
  users.forEach(user => user.rules[rules[index]] = enabled)
}

const userToggle = (index, enabled) => {
  rules.forEach(rule => {
    users[index].rules[rule] = enabled
  })
}

const userRuleToggle = (userIdx, ruleIdx) => {
  users[userIdx].rules[rules[ruleIdx]] = !users[userIdx].rules[rules[ruleIdx]]
}

const updateRules = (userIdx) => {
  for (let ruleIdx=0 ; ruleIdx < rules.length ; ruleIdx++) {
    const sel = `tbody tr:nth-child(${userIdx + 1}) td:nth-child(${ruleIdx + 2})`
    updateUserRule(userIdx, ruleIdx, document.querySelector(sel))
  }
}

const updateUsers = (ruleIdx) => {
  for (let userIdx = 0; userIdx < users.length ; userIdx++) {
    const sel = `tbody tr:nth-child(${userIdx + 1}) td:nth-child(${ruleIdx + 2})`
    updateUserRule(userIdx, ruleIdx, document.querySelector(sel))
  }
}

const updateUserRule = (userIdx, ruleIdx, el) => {
  el.textContent = state[users[userIdx].rules[rules[ruleIdx]] ? 1 : 0]  
}

const toggleStatus = (el) => {
  const enabled = !parseInt(el.getAttribute("status"))
  el.setAttribute("status", enabled ? 1 : 0)
  return enabled
}

const insertColumnHeadings = (tr) => {
  const th = tr.appendChild(document.createElement("th"));
  th.textContent = "Users"

  rules.forEach((rule, i) => {
    const th = tr.appendChild(document.createElement("th"));
    th.textContent = rule
    toggleStatus(th);
    th.onclick = (ev) => {
      const enabled = toggleStatus(ev.target)
      ruleToggle(i, enabled)
      updateUsers(i)
    };
  });
}

const insertRows = (tbody) => {
  users.forEach((user, userIdx) => {
    const tr = tbody.insertRow();
    const td = tr.appendChild(document.createElement("td"));
    td.textContent = user.name
    toggleStatus(td);  
    td.onclick = (ev) => {
      const enabled = toggleStatus(ev.target)
      userToggle(userIdx, enabled)
      updateRules(userIdx)
    };
    rules.forEach((rule, ruleIdx) => {
      const td = tr.insertCell();
      updateUserRule(userIdx, ruleIdx, td)
      td.onclick = (el) => {
        userRuleToggle(userIdx, ruleIdx)
        updateUserRule(userIdx, ruleIdx, el.target)
      };
    });
  });
}
const table = document.createElement("table");
const thead = table.createTHead();
const tr = thead.insertRow();
insertColumnHeadings(tr)

const tbody = table.createTBody();
insertRows(tbody)

document.body.appendChild(table);
* {
  font-family: sans-serif;
}
table {
  border-collapse: collapse;
  border: 1px solid;
}
td,
th {
  padding: 5px 10px;
  text-align: center;
  border: 1px solid;
  cursor: pointer;
}

thead th {
  background-color: gray;
}
tbody th {
  background-color: lightgray;
}
thead th:first-child {
  background-color: lightblue;
}

\$\endgroup\$
3
  • \$\begingroup\$ Thanks, Sir Meehan, at first when I coded it, I didn't realize I must update the array when DOM changed, good technique to update after each toggle! \$\endgroup\$ Jun 19 at 21:43
  • \$\begingroup\$ +thanks for the advice to not use custom HTML attributes to convey state, next time I will prefix it with something special to not clashes with browser plugins! \$\endgroup\$ Jun 19 at 21:45
  • \$\begingroup\$ As you said If I used OOP it would be cleaner as a class user with methods... \$\endgroup\$ Jun 19 at 21:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.