Custom Elements، دارای یک چرخه حیات میباشند. در طی این چرخه حیات، میتوان تعدادی متد خاص را به المان سفارشی خود اضافه کرد که به صورت خودکار توسط مرورگر فراخوانی میشوند. به این متدها Life-cycle Callbacks یا Custom Element Reactions نیز میگویند. برای درک بهتر چرخه حیات مذکور، به تکه کد زیر توجه نمائید:
در این صورت، امکان استفاده از المان سفارشی، قبل از معرفی و ثبت آن توسط متد customElements.define نیز وجود خواهد داشت. یعنی اگر در DOM شما تعدادی المان سفارشی وجود داشته باشند که به هر دلیل نیاز است پس از گذشت یک بازه زمانی کوتاهی معرفی و ثبت شوند (مثال: lazy load اسکریپتهای متناظر با المانهای سفارشی در Angular)، این المانها معتبر هستند. فرآیند فراخوانی متد define و استفاده از کلاس معرفی شده برای ارتقاء المان سفارشی موجود در DOM، اصطلاحا Element Upgrades نامیده میشود. همچنین با استفاده از متد customElements.whenDefined که یک Promise را بازگشت میدهد، میتوان از معرفی و ثبت شدن المان خاصی آگاه شد:
یا حتی امکان استفاده از سلکتور «:defined» نیز به شکل زیر وجود دارد:
در اینجا ابتدا تمام المانهای تعریف نشده، کوئری شدهاند و با استفاده از متد map و اجرای متد whenDefined، به لیستی از Promiseها رسیدهایم و در نهایت با استفاده از Promise.all منتظر اتمام مرحله upgrade المانهای مذکور هستیم.
متد get، ارجاعی به سازنده کلاس x-component را بازگشت خواهد داد.
customElements.define("x-component", class extends HTMLElement { constructor() { super(); console.log('constructed!'); } connectedCallback() { console.log('connected!'); } disconnectedCallback() { console.log('disconnected!'); } adoptedCallback() { console.log('adopted!'); } attributeChangedCallback(name, oldValue, newValue) { console.log('attirbuteChanged!', name, oldValue, newValue); } static get observedAttributes() { return ['checked','demo','label']; } });
Element Upgrades
به صورت پیشفرض، المانهای موجود در DOM که مبتنیبر استانداردهای HTML تعریف نشدهاند، توسط مرورگر به عنوان HTMLUnknownElement تجزیه و تحلیل خواهند شد. ولی این موضوع برای المانهای سفارشی که نام معتبری دارند (وجود «-»)، صدق نمیکند. برای مثال دو خط کد زیر را در کنسول مربوط به Developer tools مرورگر خود اجرا کنید:
// "tabs" is not a valid custom element name document.createElement('tabs') instanceof HTMLUnknownElement === true //true // "x-tabs" is a valid custom element name document.createElement('x-tabs') instanceof HTMLElement === true //true
customElements.whenDefined('x-component').then(() => { console.log('x-component defined'); });
<share-buttons> <social-button type="twitter"><a href="...">Twitter</a></social-button> <social-button type="fb"><a href="...">Facebook</a></social-button> <social-button type="plus"><a href="...">G+</a></social-button> </share-buttons> // Fetch all the children of <share-buttons> that are not defined yet. let undefinedButtons = buttons.querySelectorAll(':not(:defined)'); let promises = [...undefinedButtons].map(socialButton => { return customElements.whenDefined(socialButton.localName); )); // Wait for all the social-buttons to be upgraded. Promise.all(promises).then(() => { // All social-button children are ready. });
سازنده مرتبط با کلاس المان سفارشی x-component، در هنگام وهلهسازی یا فرآیند upgrades فراخوانی میشود و میتواند برای مقداردهی اولیه خواص وهله جاری، تنظیم رخدادگردانها (Event Listeners) و یا ایجاد و اتصال ShadowDOM با استفاده از متد attachShadow، محل مناسبی باشد. طبق مستندات مرتبط، فراخوانی ()super بدون ارسال هیچ آرگومانی باید در اولین خط این سازنده انجام شود.
برای وهلهسازی المان سفارشی نیز میتوان از متد customeElements.get به شکل زیر استفاده کرد:
customeElements.define('x-component',...) let XComponent = customElements.get('x-component'); document.body.appendChild(new XComponent())
در تکه کد بالا، لیست المانهای img موجود در داخل iframe کوئری شده و سپس با پیمایش بر روی لیست بدست آمده و در زمان فراخوانی متد adoptNode که کار تغییر ownerDocument مرتبط با یک المان را انجام میدهد، متد adoptedCallback ما نیز اجرا خواهد شد.
مشخص است که امکان تعریف انواع و اقسام متدها با پارامترها و خروجیهای مختلفی و حتی نسخههای همزمان یا ناهمزمان آنها وجود دارد.
انتظار چنین خروجی داریم:
این موضوع، تحت عنوان «Reflecting Properties to Attributes» مطرح میباشد. در این صورت علاوه بر اینکه با یک نگاه به DOM، از مقادیر خصوصیات یک المان آگاه خواهیم بود، امکان استفاده از این صفات به عنوان سلکتورهایی در زمان استایلدهی نیز وجود دارد. حال از این مکانیزم برای اعمال یکسری صفات دسترسیپذیری مانند صفات ARIA به المان سفارشی خود نیز میتوان استفاده کرد. برای مثال:
یا حتی به شکل زیر:
در اینجا از همان getter که طبق پیادهسازی ما، در پشت صحنه از همان مقدار صفت disabled استفاده میکند، برای تنظیم یکسری صفات دیگر استفاده کردهایم. به عنوان مثال اگر المان ما غیرفعال شده بود، صفت tabindex آن را با «-1» مقداردهی میکنیم تا از توالی پیمایش مبتنیبر Tab خارج شود.
در این صورت اگر لود اسکریپت، معرفی و ثبت این المان سفارشی به صورت Lazy انجام شود، امکان آن وجود دارد که فریمورک، عملیات binding را قبل از مرحله upgrades انجام دهد. خوب... این موضوع چه مشکلی را ایجاد میکند؟ در این صورت چون مرحله upgrades تمام نشده است، پیادهسازی بدنه متد setter متناظر با خصوصیات المان سفارشی، توسط پراپرتی جدیدی که توسط فریمورک برروی وهله موجود تعریف میشود، بیاستفاده خواهد ماند. برای مثال، اگر سعی کنیم قبل از مرحله upgrades خصوصیت disabled المان x-component را مقداردهی کنیم، عملیات مکانیزم همگامسازی مدنظر ما اجرا نخواهد شد:
با این خروجی مواجه خواهیم شد که هیچ اثری از صفت disabled دیده نمیشود:
connectedCallback
اولین متد بعد از فراخوانی سازنده، connectedCallback نام دارد و زمانی رخ میدهد که وهلهای از یک المان سفارشی به Light DOM افزوده شدهاست و یا توسط Parser مرورگر، در DOM شناسایی شود. این متد، محل پیشنهاد شده برای اجرای کدهای زمان راهاندازی مانند دریافت منابع از سرور و یا رندر کردن محتوایی خاص، میباشد.
نکته: این متد ممکن است بیش از یکبار نیز فراخوانی شود! هنگامیکه یک المان موجود در DOM از طریق کد از DOM جداشده و سپس اضافه شود:
const el = document.createElement('x-component'); document.body.appendChild(el); // connectedCallback() called el.remove(); // disconnectedCallback() document.body.appendChild(el); // connectedCallback() called again
disconnectedCallback
این متد نیز هر وقت المانی از DOM حذف شود، اجرا خواهد شد و مانند متد connectedCallback ممکن است چندین بار فراخوانی شود. همچنین محل مناسبی برای آزادسازی منابع استفاده شده در پیادهسازی المان سفارشی، میباشد. مانند: استفاده از متد clearInterval برای پاکسازی یک تایمر که با متد setInterval ایجاد شدهاست.
نکته: هیچ تعهدی به اجرای متد disconnectedCallback در تمام حالاتی که یک المان از DOM حذف میشود، وجود ندارد. به عنوان مثال هنگامیکه یک برگهی مرورگر بسته شود، این متد فراخوانی نخواهد شد.
attributeChangedCallback
این متد هنگامی فراخوانی خواهد شد که خصوصیات مشخص شده از طریق observedAttributes به عنوان یک static getter، اضافه، حذف، ویرایش و یا جایگزین شوند. همچنین در مرحله Upgrades برای مقادیر اولیه خصوصیات که توسط استفاده کننده از المان سفارشی، مشخص شدهاست نیز فراخوانی میشود.
adoptedCallback
متدهای قبلی بیشترین استفاده را دارند؛ این متد خاص نیز زمانیکه یک المان سفارشی به یک DOM دیگری منتقل میشود، اجرا خواهد شد. برای مثال:
const iframe = document.querySelector('iframe'); const iframeImages = iframe.contentDocument.querySelectorAll('img'); const newParent = document.getElementById('images'); iframeImages.forEach(function(imgEl) { newParent.appendChild(document.adoptNode(imgEl)); });
Methods
اگر المانهای بومی و استاندارد موجود را بررسی کنید، همه آنها دارای یکسری متد، پراپرتی و صفات مشخصی هستند. در اینجا نیز تعریف رفتاری برای یک المان سفارشی و کپسوله، نکته خاصی ندارد و به شکل زیر قابل تعریف و استفاده میباشد:
class XComponent extends HTMLElement { constructor() { super(); } doSomething(){ console.log('doSomething'); } }
let component = document.querySelector('x-component'); component.doSomething();
Attributes & Properties
در HTML خیلی رایج است که مقادیر پراپرتیهای یک المان در قالب یکسری صفات، نمودی در DOM هم داشته باشند. برای مثال:
div.id = 'id-value'; div.hidden = true;
<div id="id-value" hidden>
class XComponent extends HTMLElement { constructor() { super(); } connectedCallback() { this._render(); } get disabled() { return this.hasAttribute('disabled'); } set disabled(val) { // Reflect the value of `disabled` as an attribute. if (val) { this.setAttribute('disabled', ''); } else { this.removeAttribute('disabled'); } this._render(); } _render() { //... } }
ایده کار خیلی ساده است؛ پراپرتیهای یک المانسفارشی را از طریق متدهای getter و setter تعریف کرده و در بدنه پیادهسازی آنها، صفات HTML ای المان جاری را تغییر داده و یا از مقادیر آنها استفاده کنیم.
اینبار با مقداردهی پراپرتی disabled برروی وهلهای از المان سفارشی ما، این مقادیر نمودی در DOM هم خواهند داشت. با استفاده از متدهای setAttribute یا removeAttribute کار همگامسازی پراپرتیها با صفات را انجام دادهایم.همچین با استفاده از متد attributeChangedCallback نیز میتوان برای اعمال صفات ARIA که اشاره شد، به شکل زیر استفاده کرد:
attributeChangedCallback(name, oldValue, newValue) { switch (name) { case 'checked': // Note the attributeChangedCallback is only handling the *side effects* // of setting the attribute. this.setAttribute('aria-disabled', !!newValue); break; ... }
attributeChangedCallback(name, oldValue, newValue) { // When the component is disabled, update keyboard/screen reader behavior. if (this.disabled) { this.setAttribute('tabindex', '-1'); this.setAttribute('aria-disabled', 'true'); } else { this.setAttribute('tabindex', '0'); this.setAttribute('aria-disabled', 'false'); } // TODO: also react to the other attribute changing. }
نکته: پیشنهاد میشود از مکانیزم همگامسازی پراپرتیها و صفات، برای انواع دادهای اولیه (رشته، عدد و ...) استفاده شود و برای دریافت مقادیری مانند objects و یا arrays، از متدها یا پراپرتیها استفاده کنید. در غیر این صورت نیاز خواهد بود که این مقادیر را سریالایز و در داخل المان سفارشی، عملیات معکوس آن را انجام دهید که میتواند هزینهی زیادی داشته باشد. عملیات سریالایز نیز خود باعث از دست دادن ارجاعات به آن مقادیر خواهد شد. به صورت کلی هیچکدام از المانهای بومی موجود، چنین اطلاعاتی را دریافت نمیکند. برای مثال:
constructor() { super(); this._data = []; } get data() { return _data; } set data(value) { if (this_data === value) return; this._data = value; this._render(); }
Lazy Properties
همانطور که اشاره شد حتی قبل از مرحله upgrades مربوط به المانهای سفارشی استفاده شده در سند HTML برنامه شما، به عنوان المانهای معتبری هستند که امکان کوئری کردن و مقداردهی اولیه خصوصیات آنها از طریق کد نیز ممکن است. این موضوع زمانیکه از فریمورکی مثل Angular استفاده میشود، المان موردنظر به صورت خودکار توسط فریمورک لود و به صفحه اضافه شده و در انتهای عملیات، binding پراپرتیهای آن به خصوصیات موجود در کامپوننت Angular ای انجام خواهد شد. به مثال زیر توجه کنید:
<x-component [disabled]="model.disabled"></x-component>
let el = document.querySelector('x-component'); el.disabled = true; customElements.define("x-component", class extends HTMLElement { constructor() { super(); } get disabled() { return this.hasAttribute('disabled'); } set disabled(val) { // Reflect the value of `disabled` as an attribute. if (val) { this.setAttribute('disabled', ''); } else { this.removeAttribute('disabled'); } this._render(); } });
یکی از روشهای پیشنهاد شده برای حل این مشکل، مقداردهی مجدد پراپرتیها بعد از مرحله upgrades و پس از اینکه متد setter تعریف شدهاست، میباشد:
let el = document.querySelector('x-component'); el.disabled = true; customElements.define("x-component", class extends HTMLElement { constructor() { super(); } connectedCallback() { this._upgradeProp('disabled'); } get disabled() { return this.hasAttribute('disabled'); } set disabled(val) { // Reflect the value of `disabled` as an attribute. if (val) { this.setAttribute('disabled', ''); } else { this.removeAttribute('disabled'); } this._render(); } _upgradeProp(prop) { if (this.hasOwnProperty(prop)) { let value = this[prop]; delete this[prop]; //delete instance property this[prop] = value; // set prototype property } } });
ایده کار به این صورت است که مقدار پراپرتی مورد نظر را که قبل از مرحله upgrades برروی وهله جاری (instance property) تنظیم شدهاست، در متغییری نگهداری کرده و آن پراپرتی را حذف و سپس پراپرتی تعریف شده در کلاس (prototype property) را برای وهله جاری مقداردهی کنیم.
نکته: به یاد داشته باشید که قبل از اینکه یکسری صفات خاص را مقدار دهی کنید، بررسی شود که استفاده کننده از المان سفارشی، مقداری را تنظیم نکرده باشد. برای مثال اگر قصد دارید المان سفارشی شما قابلیت focus را داشته باشد، نیاز است شما حداقل tabindex=-1 را تنظیم کنید؛ حتی اگر استفاده کننده، آن را مقداردهی نکرده باشد:
connectedCallback() { if (!this.hasAttribute('role')) this.setAttribute('role', 'checkbox'); if (!this.hasAttribute('tabindex')) this.setAttribute('tabindex', -1); //element is not reachable via sequential keyboard navigation, but could be focused }