किसी व्यू की DOM सामग्री बदलने के बाद भी क्लिक हैंडलर काम करते रहें, इसके लिए किसी ऐसे पैरेंट एलिमेंट पर एक लिसनर जोड़ें जो अपडेट के बाद भी बना रहे। यह गाइड दिखाती है कि Mac पर Safari में एक छोटे, कृत्रिम टाइल पिकर का इस्तेमाल करके इस पैटर्न को कैसे लागू और जाँचा जाए।
काम का दायरा स्पष्ट है: JavaScript व्यू की सामग्री फिर से बनाते हुए उसके बटनों को सक्रिय और चयन को बरकरार रखना। इसके लिए JavaScript का बुनियादी ज्ञान और Safari का मौजूदा ब्राउज़र चाहिए। उदाहरण में मानक ब्राउज़र API का इस्तेमाल किया गया है और किसी फ़्रेमवर्क या पैकेज को इंस्टॉल करने की ज़रूरत नहीं है।
DOM बदलने के बाद क्लिक काम करना क्यों बंद कर देते हैं?
addEventListener() से पंजीकृत लिसनर उसी इवेंट टार्गेट से जुड़ा होता है जिस पर उसे पंजीकृत किया गया था। किसी कंटेनर के innerHTML को असाइन करने पर दिया गया मार्कअप पार्स होता है और उसके वंशज बदल दिए जाते हैं। इसलिए नए बने बटन को पुराने बटन पर जुड़े लिसनर विरासत में नहीं मिलते, भले ही उसकी ID और लेबल समान हों। MDN का innerHTML संदर्भ और DOM Standard का इवेंट लिसनर मॉडल देखें।
कंटेनर की सामग्री बदलने और कंटेनर को ही बदलने के बीच का अंतर उपयोगी है। यदि आपका कोड कंटेनर को बनाए रखता है और केवल उसके चाइल्ड फिर से बनाता है, तो उस कंटेनर पर लगा लिसनर अपनी जगह बना रह सकता है। यदि बाहरी रेंडर कंटेनर को भी बदल देता है, तो आपको किसी ऐसे पूर्वज को चुनना होगा जो बना रहे, या नए कंटेनर के आरंभीकरण के दौरान लिसनर जोड़ना होगा।
सामान्य बबल होने वाले क्लिक इवेंट के लिए, इवेंट डेलीगेशन से पैरेंट अपने वंशजों से शुरू हुई इंटरैक्शन संभाल सकता है। हर बार बदलने वाले बटन पर लिसनर जोड़ने के बजाय, स्थिर पैरेंट पर लिसनर जोड़ें और इवेंट आने पर संबंधित बटन की पहचान करें। MDN की इवेंट बबलिंग गाइड इस पैटर्न का विवरण देती है।
लिसनर कहाँ होना चाहिए?
सबसे पहले पता लगाएँ कि आपका रेंडर ऑपरेशन DOM की किस सटीक सीमा को बदलता है। फिर उस सीमा के बाहर सबसे छोटा उपयुक्त पैरेंट चुनें। नीचे दिया गया निर्णय-सहायक, आम अपडेट पैटर्न पर दस्तावेज़ित प्रतिस्थापन और लिसनर व्यवहार लागू करता है। MDN: replaceChildren(), DOM Standard: EventTarget।
| आपका अपडेट | लिसनर की जगह | लाइफ़साइकल की ज़रूरत |
|---|---|---|
| मौजूदा व्यू रूट के चाइल्ड फिर से बनाना | उस रूट से क्लिक डेलीगेट करें | रूट बनाते समय एक बार पंजीकृत करें |
| व्यू रूट को भी बदलना | किसी ऐसे बाहरी कंटेनर से डेलीगेट करें जो बना रहे, या नए रूट पर लिसनर जोड़ें | पिछले व्यू का पंजीकरण साफ़ करें |
| ऐसे बटन बनाना जिन्हें अलग-अलग लिसनर चाहिए | निर्माण के दौरान हर नए बटन पर लिसनर जोड़ें | नए कंट्रोल उपलब्ध कराने से पहले लिसनर जोड़ना पूरा करें |
| पूरा डॉक्यूमेंट फिर से लोड करना | नए डॉक्यूमेंट में लिसनर आरंभ करें | पेज का सामान्य आरंभीकरण फिर से चलाएँ |
नीचे दिए गए उदाहरण में संरचना एक व्यू रूट की है जिसमें शीर्षक, स्थिति, टाइल विकल्पों का क्रमबद्ध सेट और रीफ़्रेश कंट्रोल शामिल हैं। हर रेंडर इन चाइल्ड को उसी क्रम में फिर से बनाता है। केवल व्यू रूट, लिसनर और JavaScript स्टेट बरकरार रहते हैं।
Safari में ऐसा पूरा व्यू बनाएँ जो प्रतिस्थापन के बाद भी काम करे
सबसे पहले, ब्राउज़र का एक अस्थायी टैब तैयार करें। अपने Mac पर Safari में Settings… खोलें, Advanced चुनें और Show features for web developers चालू करें। Develop → Show Web Inspector चुनकर या Option-Command-I दबाकर Web Inspector खोलें। ये चरण Web Inspector चालू करने के लिए WebKit की गाइड में दिए गए हैं।
about:blank पर जाएँ, Web Inspector खोलें और Console चुनें। नीचे दिया गया पूरा प्रोग्राम उसके प्रॉम्प्ट में पेस्ट करके चलाएँ। WebKit बताता है कि Console Prompt, निरीक्षित पेज के चुने हुए एक्ज़िक्यूशन कॉन्टेक्स्ट में JavaScript चलाता है। खाली टैब का इस्तेमाल करें ताकि डेमो आपके ऐप्लिकेशन से अलग रहे।
यह ब्राउज़र का एक पूरा प्रोग्राम है: यह अपना DOM बनाता है, नमूना डेटा परिभाषित करता है, व्यू माउंट करता है और एक क्लीनअप मेथड उपलब्ध कराता है। यह replaceChildren() का इस्तेमाल करके व्यू के चाइल्ड बदलता है। यह मेथड नोड ऑब्जेक्ट स्वीकार करता है और मौजूदा चाइल्ड सेट को बदल देता है। इस ऑपरेशन का विवरण MDN के मेथड संदर्भ में है।
(() => {
window.domClickDemo?.destroy();
const root = document.createElement("section");
root.id = "dom-click-demo";
const controller = new AbortController();
const items = [
{ id: "a", label: "Tile A" },
{ id: "b", label: "Tile B" },
{ id: "c", label: "Tile C" }
];
const state = { selected: "None", refreshes: 0 };
function makeButton(action, label, id) {
const button = document.createElement("button");
button.type = "button";
button.dataset.action = action;
if (id !== undefined) button.dataset.id = id;
const caption = document.createElement("span");
caption.textContent = label;
button.append(caption);
return button;
}
function render() {
const heading = document.createElement("h2");
heading.textContent = "Tile picker";
const status = document.createElement("p");
status.textContent =
`Selected: ${state.selected}; refreshes: ${state.refreshes}`;
const list = document.createElement("ul");
for (const item of items) {
const row = document.createElement("li");
row.append(makeButton("select", item.label, item.id));
list.append(row);
}
const refresh = makeButton("refresh", "Replace view content");
root.replaceChildren(heading, status, list, refresh);
}
function handleClick(event) {
if (!(event.target instanceof Element)) return;
const button = event.target.closest("button[data-action]");
if (!button || !root.contains(button) || button.disabled) return;
if (button.dataset.action === "select") {
const item = items.find(item => item.id === button.dataset.id);
if (!item) return;
state.selected = item.label;
} else if (button.dataset.action === "refresh") {
state.refreshes += 1;
} else {
return;
}
render();
}
root.addEventListener("click", handleClick, {
signal: controller.signal
});
render();
document.body.append(root);
window.domClickDemo = {
root,
destroy() {
controller.abort();
root.remove();
}
};
})();
लिसनर को render() के बाहर पंजीकृत किया गया है। हर चयन और रीफ़्रेश नए बटन बनाता है, लेकिन वे सभी बटन मूल रूट के भीतर ही रहते हैं। स्टेट ऑब्जेक्ट भी रेंडर किए गए सबट्री के बाहर रहता है, इसलिए स्थिति पैराग्राफ बदलने पर चुनी हुई टाइल या रीफ़्रेश की गिनती रीसेट नहीं होती।
अंदरूनी कैप्शन जानबूझकर रखा गया है। बटन के टेक्स्ट पर क्लिक करने से इवेंट टार्गेट उसका चाइल्ड एलिमेंट हो सकता है। closest("button[data-action]") टार्गेट और उसके पूर्वजों में मेल खाता कंट्रोल ढूँढ़ता है, जिससे ऐसा हैंडलर बनने से बचता है जो तभी काम करे जब बटन खुद इवेंट टार्गेट हो। MDN का closest() संदर्भ इस खोज और तब मिलने वाले null परिणाम का विवरण देता है जब कोई मेल न मिले।
कंटेनमेंट जाँच यह सुनिश्चित करती है कि मेल खाता कंट्रोल इस व्यू की सीमा के भीतर हो। DOM Standard contains() को इस आधार पर परिभाषित करता है कि दिया गया नोड स्वयं वह नोड है या उसके वंशजों में से एक। DOM Standard: Node.contains()।
आरंभीकरण, रेंडरिंग और निपटान को अलग रखें
इस उदाहरण की तीन स्पष्ट ज़िम्मेदारियाँ हैं। आरंभीकरण रूट बनाता है और उसका लिसनर पंजीकृत करता है। रेंडरिंग स्टेट के आधार पर रूट के चाइल्ड अपडेट करती है। निपटान लिसनर और रूट हटाता है। यही इस कार्यान्वयन का लाइफ़साइकल अनुबंध है; इसे अपने ऐप्लिकेशन के मुताबिक ढालते समय समकक्ष ज़िम्मेदारियाँ बनाए रखें।
लिसनर को AbortSignal मिलता है, और destroy() उसका कंट्रोलर abort करता है। इससे संबंधित पंजीकरण हट जाता है, जैसा MDN के addEventListener() संदर्भ में बताया गया है। पूरा प्रोग्राम फिर से चलाने पर पहले पिछला डेमो निपटाया जाता है, ताकि कई डेमो व्यू इकट्ठे न हों।
हर रेंडर के भीतर नया अनाम लिसनर न बनाएँ। अनाम फ़ंक्शन एक्सप्रेशन को बार-बार चलाने पर अलग-अलग फ़ंक्शन ऑब्जेक्ट बनते हैं, इसलिए किसी बने रहने वाले रूट पर उनके पंजीकरण जमा हो सकते हैं। एक ही कॉलबैक, इवेंट टाइप और कैप्चर सेटिंग दोबारा इस्तेमाल करने से डुप्लिकेट पंजीकरण नहीं होता, लेकिन बाइंडिंग को आरंभीकरण में रखने से उसका स्वामित्व अधिक स्पष्ट रहता है। MDN: addEventListener(), DOM Standard: लिसनर जोड़ना।
बार-बार होने वाले प्रतिस्थापन, चयन और क्लीनअप की जाँच करें
उदाहरण को जाँचने के लिए यह क्रम अपनाएँ। अपेक्षित नतीजे प्रोग्राम के स्पष्ट स्टेट अपडेट से निकलते हैं; ये Safari पर जाँच पूरी होने के दावे नहीं हैं।
Tile Bपर क्लिक करें। स्थिति मेंSelected: Tile B; refreshes: 0दिखना चाहिए।Replace view contentपर तीन बार क्लिक करें। स्थिति मेंSelected: Tile B; refreshes: 3दिखना चाहिए।Tile Cके टेक्स्ट पर क्लिक करें। स्थिति बदलकरSelected: Tile C; refreshes: 3होनी चाहिए।- शीर्षक या स्थिति पैराग्राफ पर क्लिक करें। दोनों मान नहीं बदलने चाहिए।
- Console में
window.domClickDemo.destroy()चलाएँ। डेमो गायब हो जाना चाहिए। - पूरा प्रोग्राम फिर से चलाएँ। एक नया व्यू
Selected: None; refreshes: 0के साथ दिखाई देना चाहिए।
पहली तीन जाँचें प्रतिस्थापन, बरकरार स्टेट और अंदरूनी क्लिक टार्गेट कवर करती हैं। चौथी जाँचती है कि ऐक्शन फ़िल्टर हो रहे हैं। आखिरी दो निपटान और आरंभीकरण का रास्ता जाँचती हैं। ये जाँचें केवल यह पुष्टि नहीं करतीं कि शुरुआती बटन काम करते हैं, बल्कि वास्तविक लाइफ़साइकल को परखती हैं।
प्रोग्राम के JavaScript सिंटैक्स को Node से जाँचा गया, और एक अलग, नियतात्मक EventTarget जाँच ने कॉलबैक डिडुप्लिकेशन तथा abort signal के ज़रिए उन्हें हटाने की पुष्टि की। उस जाँच में ब्राउज़र DOM रेंडरिंग और Safari इंटरैक्शन नहीं चलाए गए; अपने Mac पर उन्हें सत्यापित करने के लिए ऊपर दिए गए चरण अपनाएँ।
अगर आपके ऐप्लिकेशन में हैंडलर फिर भी काम न करे तो?
सबसे पहले उस बिंदु की जाँच करें जहाँ व्यवहार अलग होना शुरू होता है। अपने ऐप्लिकेशन की प्रतिस्थापन सीमा, इवेंट पाथ और स्टेट अपडेट जाँचते समय उदाहरण का छोटा दायरा बनाए रखें।
| लक्षण | पहले यह जाँचें | उपयुक्त सुधार |
|---|---|---|
| बाहरी व्यू रीफ़्रेश होने तक क्लिक काम करते हैं | क्या लिसनर का रूट भी बदल गया था? | आरंभीकरण के दौरान नए रूट पर लिसनर जोड़ें या किसी ऐसे पैरेंट को चुनें जो बना रहे |
| क्लिक आता है, लेकिन कोई ऐक्शन नहीं चलता | क्या नया कंट्रोल अब भी सिलेक्टर और ऐक्शन वैल्यू से मेल खाता है? | कंट्रोल का सिलेक्टर और ऐक्शन अनुबंध बनाए रखें |
| बटन के टेक्स्ट पर क्लिक करने से अलग व्यवहार होता है | क्या हैंडलर केवल सटीक टार्गेट की जाँच कर रहा है? | closest() से कंट्रोल ढूँढ़ें |
| एक क्लिक से स्टेट कई बार अपडेट होता है | क्या कई पंजीकरण या डायरेक्ट और डेलीगेटेड, दोनों हैंडलर सक्रिय हैं? | ऐक्शन का स्वामित्व एक ही आरंभीकरण पाथ को दें |
| हैंडलर चलता है, लेकिन डिस्प्ले रीसेट हो जाता है | क्या रेंडरिंग के भीतर स्टेट फिर से बनाई जा रही है? | ज़रूरी स्टेट को बदली जाने वाली सबट्री के बाहर रखें |
प्रोपेगेशन भी देखें। वंशज का कोई हैंडलर stopPropagation() चलाकर बबल होने वाले इवेंट को आपके डेलीगेटेड लिसनर तक पहुँचने से रोक सकता है। MDN प्रोपेगेशन रोकने और इवेंट डेलीगेशन दोनों समझाता है। लिसनर के फ़ेज़ बदलने से पहले देखें कि प्रोपेगेशन क्यों रोका जा रहा है, क्योंकि फ़ेज़ बदलने से निष्पादन का क्रम भी बदल जाता है।
यदि आपको सीधे बटन पर लिसनर लगाने हैं, तो हर बार नए बटन बनाते समय उन्हें जोड़ें। एक बार की क्वेरी और बाइंडिंग प्रक्रिया बाद के रेंडर में बने नोड को आरंभ नहीं कर सकती: पंजीकरण अपने इवेंट टार्गेट से जुड़े होते हैं। इसी तरह, डेलीगेशन उस रूट पर लिसनर को बरकरार नहीं रख सकता जिसे आपका रेंडरर हटा देता है। ये सीमाएँ DOM Standard के लिसनर मॉडल से निकलती हैं।
इस काम के लिए टिकाऊ व्यवस्था है: स्थिर व्यू रूट, एक डेलीगेटेड लिसनर, रेंडर किए गए चाइल्ड के बाहर रखा गया स्टेट और क्लीनअप का स्पष्ट रास्ता। पहचानें कि हर अपडेट के बाद कौन से नोड बने रहते हैं, उसके अनुसार लिसनर रखें और बदलाव को बड़े व्यू में जोड़ने से पहले लगातार कई प्रतिस्थापनों की जाँच करें।
स्रोत
- Web Inspector चालू करना — WebKit।
- Web Inspector इंटरफ़ेस — WebKit।
- Element: innerHTML प्रॉपर्टी — MDN Web Docs।
- इवेंट बबलिंग — MDN Web Docs।
- Element: replaceChildren() मेथड — MDN Web Docs।
- Element: closest() मेथड — MDN Web Docs।
- EventTarget: addEventListener() मेथड — MDN Web Docs।
- DOM Standard — WHATWG।