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\$
    – Robert
    CommentedJul 9, 2022 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\$CommentedJun 19, 2022 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\$CommentedJun 19, 2022 at 21:45
  • \$\begingroup\$As you said If I used OOP it would be cleaner as a class user with methods...\$\endgroup\$CommentedJun 19, 2022 at 21:47

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.