Reputation: 177
I read about unobstrusive JavaScript and how Javascript should be not explicitly placed inside HTML code. Fine! But I have these hundreds of search results where I placed click events in the HTML structure because the function is the same, but the arguments passed are totally different from one entry to the next. For example:
HTML:
<button id="button_details_0"
class="search_details__button"
onclick="get_details('002124482_2242.jpg','4','0','Brasil, São Paulo, Atibaia, Inventários e Testamentos, Parte A')">
<span id="search_details_span_0">˅</span>
</button>
Also, when I open the details of a particular entry, the user has the option to get more information. Then, I call another function with even more different options.
The easy way of doing this is to add onclick in the HTML when the page is generated by Perl CGI. If I want to be true to unobstrusive Javascript, what do I place in the HTML to pass all this information to Javascript besides creating a form for each entry?
I tried moving the Javascript out of the HTML, but the new created details have new buttons. The progression is an entry, for example, with barebone christening data, then you click on details and more info about parents, godparents, clergy will appear. Then each name of the parents, godparents, clergy can be clicked on to obtain even more data. The initial data is presented:
HTML:
<button id="search_details_button_12" class="search_details__button" data-image_number="58" data-record_number="6" data-row_number="12" data-locality="Brasil, São Paulo, Atibaia, São João Batista, Batismos 1719-1752">
<span id="search_details_span_12">˅</span>
</button>
Then I have this at the end of the HTML so I assure the page is loaded fully: JS:
> // When user clicks on details of search
> const search_details_buttons = document.querySelectorAll('[id^=search_details_button');
> console.log(search_details_buttons); for (let j = 0; j <
> search_details_buttons.length; j++) {
> search_details_buttons[j].addEventListener('click', e => {
> get_details(search_details_buttons[j]);
> });
}
Then in the details area (after it is clicked), I get these buttons to which I want to addEventListeners:
<button id="details_ascend_button_11_1_1" class="details_ascend__button" data-row="11" data-role="1" data-individual="1">
<span class="details_ascend__span">↑</span>
</button>
But these new buttons in details are not created when the page is loaded but after the user clicks on details.
If Arrow 1 is clicked after details shows then the innerHTML changes back to the "barebone" information and the new created buttons disappears until clicked details is clicked again.
Where do I check for the existence of these new buttons and add event handlers?
Thanks you!
Upvotes: 1
Views: 172
Reputation: 21505
I would recommend not using data attributes as a substitute; it wouldn't change much. All it does is shuffle things around a bit, your data is still embedded in your HTML.
It may be better separation of concerns to extract the data you want to assign to each button into an array (which is probably how you're retrieving it from a database anyway), and to keep it all in your javascript instead of your html:
// your function parameters, per button:
const data = [
['002124482_2242.jpg', '4', '0', 'Brasil, São Paulo, Atibaia, Inventários e Testamentos, Parte A'],
['125y28512j4123.jpg', '3', '6', 'Lorem Ipsum'],
['and_so_on.jpg', 4, 0, 'somewhere']
]
// add the click handlers for each button, using each entry in data as its params
const buttons = document.querySelectorAll('.search_details__button')
for (let i = 0; i < buttons.length; i++) {
buttons[i].onclick = () => {
get_details(...data[i])
}
}
// just for demo
const get_details = (a, b, c, d) => {
console.log(a, b, c, d)
}
/* Note the CSS is also best kept out of the html */
.search_details__button {
background-color: #FFFFFF;
border: none;
}
.search_details__button span {
color: rgb(0, 102, 204)
}
<button id="button_details_0" class="search_details__button">
<span>˅</span>
</button>
<button id="button_details_1" class="search_details__button">
<span>˅</span>
</button>
<button id="button_details_2" class="search_details__button">
<span>˅</span>
</button>
(In real life, for robustness against redesigns you'd probably want label each of those data elements with the corresponding button it belongs to, instead of just simply assigning them in document order, but this is good enough to demonstrate the idea.)
All that said: only do this if it helps keep things simple.
If you're generating this via a CGI script, and the way you're currently doing it works for you, there's nothing wrong with the HTML being untidy -- doing it the "correct" way would probably make your perl code more complex, and since that's where you're doing your maintenance work that may be the part you want to keep as simple as possible, not the HTML.
Upvotes: 1
Reputation: 13432
One should follow the already given advice of utilizing both, data-*
global attributes and each its related dataset
property.
A less verbose implementation, than the ones already provided, would go with just a single data-*
attribute like data-details-params
which serves as configuration for all of the get_details
function's parameters and as selector for querying any of the OP's buttons.
I addition, neither markup nor code rely on id's; the only thing necessary, is the reference of the triggering button.
function get_details(currentTarget, ...args) {
// - user implementation which in addition
// gets passed the current event target.
console.log({ args, currentTarget });
}
function getSanitizedArgumentsFromParamsConfig(params) {
return params
.split(/'\s*,\s*'/g)
.map(param => param.replace(/^'|'$/, ''));
}
function handleDetailsQuery({ currentTarget }) {
const args = getSanitizedArgumentsFromParamsConfig(
currentTarget.dataset.detailsParams
);
get_details(currentTarget, ...args);
}
document
.querySelectorAll('[data-details-params]')
.forEach(elm =>
elm.addEventListener('click', handleDetailsQuery)
);
button.query-details {
background-color: #fff;
border: 1px solid rgb(0, 102, 204);
cursor: pointer;
}
button.query-details:hover {
background-color: rgba(0, 102, 204, 20%);
}
button.query-details > span {
position: relative;
display: inline-block;
width: 1em;
height: 1em;
overflow: hidden;
text-indent: 1.1em;
color: rgb(0, 102, 204);
}
button.query-details > span:after {
content: "˅";
position: absolute;
left: 0;
top: 1px;
width: 1em;
text-indent: 0;
}
.as-console-wrapper {
max-height: 85%!important;
}
body { margin: 0; }
<button
type="button"
title="get details"
class="query-details"
data-details-params="'002124482_2242.jpg','4','0','Brasil, São Paulo, Atibaia, Inventários e Testamentos, Parte A'"
>
<span>get details</span>
</button>
<button
type="button"
title="get details"
class="query-details"
data-details-params="'002124482_9999.jpg','6','9','Brasil, São Paulo, Atibaia, Inventários e Testamentos, Parte B'"
>
<span>get details</span>
</button>
<button
type="button"
title="get details"
class="query-details"
data-details-params="'002124482_6666.jpg','8','2','Brasil, São Paulo, Atibaia, Inventários e Testamentos, Parte C'"
>
<span>get details</span>
</button>
Edit ... taking into account following of the OP's comment ...
A side effect of using datasets in this case is that the newly created "details" which is just a change of innerHTML inside a table has new buttons. These new buttons now need to have an event listener attached to them. They didn't exist before the "details" were shown. I was trying to attach the event listener to the new buttons after the innerHTML is executed. I am not able to select the new buttons with querySelectorAll. Is that the right place to do it? – Marcos Camargo
As already mentioned, and especially for the OP's problem/requirements, the event-handling has to be changed to event-delegation exclusively.
Additionally one should store every fetched markup into a WeakMap
instance. Thus, one has to make any button-related detail-query API-call exactly once.
The above provided code example then might get refactored to something similar to the next provided solution ...
function fetchContent() {
return `
<div class="details">
Lorem ipsum dolor sit amet
<button type="button" class="close-details" title="close details">
<span>close details</span>
</button>
</div>`;
}
async function mockedFetch(args) {
console.log({ args });
return new Promise(
resolve => setTimeout(resolve, 1_500, fetchContent()),
// reject => { /* handle reject */ },
);
}
// - a map based storage for every fetched content.
const detailContentStorage = new WeakMap;
async function get_details(getDetailsButton, ...args) {
// - user implementation which in addition
// gets passed the detail fetching button.
// - guard which prevents re-fetching while
// a previous fetch is still pending.
if (getDetailsButton.matches('.pending-fetch')) {
return;
}
getDetailsButton.classList.add('pending-fetch');
console.log({ getDetailsButton });
let detailsMarkup
= detailContentStorage.get(getDetailsButton);
if (!detailsMarkup) {
// - mocked API call which returns
// a specific detail's content.
detailsMarkup = await mockedFetch(args);
console.log({ detailsMarkup });
detailContentStorage.set(getDetailsButton, detailsMarkup);
}
getDetailsButton
.closest('td')
.appendChild(
new DOMParser()
.parseFromString(detailsMarkup, "text/html")
.body
);
getDetailsButton.classList.remove('pending-fetch');
}
function getSanitizedArgumentsFromParamsConfig(params) {
return params
.split(/'\s*,\s*'/g)
.map(param => param.replace(/^'|'$/, ''));
}
function handleDetailsQuery(getDetailsButton) {
const args = getSanitizedArgumentsFromParamsConfig(
getDetailsButton.dataset.detailsParams
);
get_details(getDetailsButton, ...args);
}
function closeDetailsContent(elmCloseDetails) {
console.log({ elmCloseDetails });
elmCloseDetails
.closest('td')
.querySelector('.details')
.remove();
}
// - handle any detail related behavior via event-delegation.
function handleDetail({ target }) {
const whichButtonElm = target.closest('button');
if (whichButtonElm?.matches('[data-details-params]')) {
handleDetailsQuery(whichButtonElm);
} else if (whichButtonElm?.matches('.close-details')) {
closeDetailsContent(whichButtonElm);
}
}
// - register exactly one handler by subscribing
// to any 'click' event of the single queried element.
document
.querySelector('table.overview tbody')
.addEventListener('click', handleDetail);
table.overview {
width: 49%;
}
table.overview tbody td,
table.overview tbody td .details {
position: relative;
padding: 30px 40px 10px 10px;
}
table.overview tbody td .details {
position: absolute;
top: 0;
right: 0;
color: #fff;
background-color: rgba(0, 102, 204, 90%);
}
table.overview button.query-details,
table.overview button.close-details {
position: absolute;
top: 10px;
right: 10px;
}
button.query-details,
button.close-details {
background-color: #fff;
border: 1px solid rgb(0, 102, 204);
cursor: pointer;
}
button.query-details:hover,
button.close-details:hover {
background-color: rgba(0, 102, 204, 20%);
}
.details button.close-details:hover {
background-color: rgba(255, 255, 255, 80%);
}
button.query-details > span,
button.close-details > span {
position: relative;
display: inline-block;
width: 1em;
height: 1em;
overflow: hidden;
text-indent: 1.1em;
color: rgb(0, 102, 204);
}
button.query-details > span:after,
button.close-details > span:after {
content: "˅";
position: absolute;
left: 0;
top: 1px;
width: 1em;
text-indent: 0;
}
button.close-details > span:after {
content: "x";
top: -1px;
}
@keyframes rotate-while-fetching {
from{
transform: rotate(0deg);
}
to{
transform: rotate(360deg);
}
}
button.query-details.pending-fetch > span:after {
content: "x";
top: 0;
animation: rotate-while-fetching 2s linear infinite;
transform-origin: 49% 54%;
}
.as-console-wrapper {
left: auto!important;
right: 0;
width: 50%;
min-height: 100%;
}
body { margin: 0; }
<table class="overview">
<!--
<thead>
</thead>
//-->
<tbody>
<tr>
<td>
foo bar baz, biz buzz booz
<button
type="button"
title="get details"
class="query-details"
data-details-params="'002124482_2242.jpg','4','0','Brasil, São Paulo, Atibaia, Inventários e Testamentos, Parte A'"
>
<span>get details</span>
</button>
</td>
<td>
the quick brown fox
<button
type="button"
title="get details"
class="query-details"
data-details-params="'002124482_9999.jpg','6','9','Brasil, São Paulo, Atibaia, Inventários e Testamentos, Parte B'"
>
<span>get details</span>
</button>
</td>
<td>
jumps over the lazy dog
<button
type="button"
title="get details"
class="query-details"
data-details-params="'002124482_6666.jpg','8','2','Brasil, São Paulo, Atibaia, Inventários e Testamentos, Parte C'"
>
<span>get details</span>
</button>
</td>
</tr>
</tbody>
</table>
Upvotes: 1
Reputation: 28226
You can even go one step further by creating the buttons themselves on the basis of the data
array. The attributes of the buttons can be kept to a minimum as their onclick function will reference the corresponding elements of the data
array for all necessary details:
// your function parameters, per button:
const data = [
['002124482_2242.jpg', '4', '0', 'Brasil, São Paulo, Atibaia, Inventários e Testamentos, Parte A'],
['125y28512j4123.jpg', '3', '6', 'Lorem Ipsum'],
['and_so_on.jpg', 4, 0, 'somewhere']
]
// add the buttons and assign click handlers accordingly
const cont=document.getElementById("buttons");
cont.innerHTML=data.map((d,i)=>`<button>${i+1}</button>`).join(" ");
cont.querySelectorAll("button").forEach((btn,i)=>btn.onclick=get_details(data[i]));
// just for demo
function get_details([a,b,c,d]){
return ()=>console.log(a, b, c, d);
}
/* Note the CSS is also best kept out of the html */
#buttons { border: 2px solid grey;}
#buttons button {
margin: 10px;
font-weight:900;
color: red;
}
<div id="buttons"></div>
Upvotes: 0
Reputation: 13145
You can use the data-* custom data attributes for creating the HTML markup and then access the attributes using the dataset property.
The event listener for the click event on the form is just a general even listener. This will be called if any element in the form is clicked. The advantage is that we only need one event listener for the click event, the disadvantage is that we don't know what element was clicked... But in this case with potentially many buttons I guess it is a good idea. Therefor I first test if a button is clicked, and if so, switch on the name of the button. There could be more button with the same name and with other names for calling other functions.
document.forms.form01.addEventListener('click', e => {
let button = e.target.closest('button');
if (button) {
switch (button.name) {
case 'button_details':
get_details(button);
break;
}
}
});
function get_details(button) {
console.log(button.dataset.img, button.dataset.place);
}
.search_details__button {
background-color: #ffffff;
border: none;
}
.search_details_span_0 {
color: rgb(0, 102, 204);
}
<form name="form01">
<button type="button" name="button_details" class="search_details__button"
data-img="002124482_2242.jpg"
data-id="4"
data-something="0"
data-place="Brasil, São Paulo, Atibaia, Inventários e Testamentos, Parte A">
<span class="search_details_span_0">Buttom</span>
</button>
</form>
Upvotes: 1