I have an array ofusers containinguser name with theirrules on my practice project
I have generated a table inHTML from that array inJS, So I can
change thestatus of anyuser rule by toggling on cell, or toggling all rules by clicking onname of user, ordeny orgrant rules for all users by clicking on column vertically!
BUT I want to update the original arrayusers rules, if the rules of any user change;
In other words How can I make the array inJS synchronize with the table inHTML.
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 ❤️
- \$\begingroup\$Do you have to build the table using JS createElement etc. or can you just write some html on the page?\$\endgroup\$Robert– Robert2022-07-09 12:47:53 +00:00CommentedJul 9, 2022 at 12:47
1 Answer1
Personally I would break the code down into discrete functions that do specific peices of work.
I would opt to use theusers 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 thex- 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 forusers,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;}- \$\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\$TAHER El Mehdi– TAHER El Mehdi2022-06-19 21:43:50 +00:00CommentedJun 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\$TAHER El Mehdi– TAHER El Mehdi2022-06-19 21:45:53 +00:00CommentedJun 19, 2022 at 21:45
- \$\begingroup\$As you said If I used OOP it would be cleaner as a class user with methods...\$\endgroup\$TAHER El Mehdi– TAHER El Mehdi2022-06-19 21:47:47 +00:00CommentedJun 19, 2022 at 21:47
You mustlog in to answer this question.
Explore related questions
See similar questions with these tags.
