Pull to refresh

Декларативные обработчики событий на веб странице

Reading time13 min
Views585
Плотно работая с Backbase AJAX фреймворком, проникся удобством использования декларативных обработчиков событий, когда обработчик определяется в том месте, где используется.
Вот пример обработчика click события в кнопке:
<button>
    <tb:handler event='click'>
        alert(' click ')
    </tb:handler>
</button>


* This source code was highlighted with Source Code Highlighter.
Кратенько опишу основу реализации. Для объявления обработчика используем нестандартный тег в своем пространстве имен ( для корректной работы в xhtml). На загрузке страницы находим все наши элементы и создаем реальные обработчики событий. Кроме удобства использования, такой подход позволяет расширить возможности обработчиков и унифицировать работу в разных браузерах, например, обработку событий в IE или привести к одному знаменателю клавиатурные события.

Первая версия демонстрирующая основные идеи реализации:
if (!window.ev){
    //глобальный объект, который содержит реализацию декларативных обработчиков событий
    window.ev = {};
    //наше простраство имен
    ev.namespaceURI = 'http://tedbeer.net';
    //вспомогательные флаги, определяющие особенности текущего браузера
    ev.browser = {ie : false, standard : false};
    ev.browser.standard = Boolean(document.addEventListener);
    ev.browser.ie = '\v'=='v'; //for other browsers it's a vertical tab - \u000B

    //основная процедура генерации обработчиков событий
    ev.init = function(event){
        var arr = [];
        if (ev.browser.ie) {
            arr = document.getElementsByTagName('handler');
            for (var i = 0; i < arr.length; i++) {
                if (arr[i].tagUrn == ev.namespaceURI) {
                    arr[i].parentNode['on' + arr[i].getAttribute('event')] =
                            new Function('event', arr[i].innerText);
                    arr[i].innerText = '';//clean up to avoid displaying
                }
            }
        } else {
            arr = document.getElementsByTagNameNS(ev.namespaceURI, 'handler');//xhtml
            if (!arr.length) //html
                arr = document.getElementsByTagName('tb:handler');
            for (var i = 0; i < arr.length; i++) {
                arr[i].parentNode.addEventListener( arr[i].getAttribute('event'),
                        new Function('event', arr[i].textContent), false);
                arr[i].textContent = '';//clean up to avoid displaying
            }
        }
    };

    if (ev.browser.standard)
        window.addEventListener('load', ev.init, false);
    else
        window.onload = ev.init;
}


* This source code was highlighted with Source Code Highlighter.

Тестовая страничка:
 <style type="text/css">
    .test-div {
        width: 200px;
        height: 200px;
        background-color: #6699FF;
    }
 </style>
 <script type="text/javascript" src="ev.js"></script>
 <div class="test-div">
    <tb:handler event='click'>
        alert('click');
    </tb:handler>
 </div>

* This source code was highlighted with Source Code Highlighter.

онлайн тест код ev.js

Код достаточно прозрачен, чтобы легко понять, что он делает, но в нем есть некоторые, сознательно допущенные, косяки:
  • браузер съедает пробелы и переводы строк, поэтому все операторы надо заканчивать ";"
  • по этой же причине "//" закоментирует не только текущую линию, но и весь код после нее
  • вместо использованного способа присоединения обработчиков в IE, как это делают многие разработчики, надо пользовать .attachEvent. В этом случае к элементу корректно прицепляются несколько обработчиков.
  • обработчик в IE не получает локального параметра event, как это происходит в других браузерах
  • не обрабатываются ошибки синтаксиса
Грамотные разработчики найдут еще косяки, оставлю это в качестве домашнего задания :-), чтобы было что написать в комментариях. В следующем коде исправим эти косяки и добавим следующие возможности:
  • передачу в обработчик объекта event для IE
  • добавим стандартное свойство event.target для IE
  • расширим функциональность — добавим аттрибут match, чтобы срабатывали только обработчики для определенных дочерних элементов
Код становится немногим сложнее:
if (!window.ev){
    //глобальный объект, который содержит реализацию декларативных обработчиков событий
    window.ev = {};
    //наше простраство имен
    ev.namespaceURI = 'http://tedbeer.net';
    //вспомогательные флаги, определяющие особенности текущего браузера
    ev.browser = {ie : false, standard : false};
    ev.browser.standard = Boolean(document.addEventListener);
    ev.browser.ie = '\v'=='v'; //for other browsers it's a vertical tab - \u000B

    //присоединить обработчик на указанный узел
    ev.setEventHandler = function(oNode, oEventNode){
        if (this.browser.ie) {
            var vHandler = function(){
                var self = arguments.callee;
                var ev = window.event;
                ev.target = ev.srcElement;
                ev.currentTarget = self.node.parentNode;
                var sMatch = self.node.getAttribute('match');
                if (sMatch === null || Sizzle.matches(sMatch, [ev.target]).length > 0)
                    return self.handler(ev);
            }
            vHandler.node = oEventNode;
            var sBody = oEventNode.innerText;
            if (!sBody.length) {//ищем код в CDATA секциях(xhtml) и в комментариях
                for(var i = 0; i < oEventNode.childNodes.length; i++)
                    if (oEventNode.childNodes[i].nodeType) {//4 or 8 - CDATA or comment
                        sBody = oEventNode.childNodes[i].nodeValue;
                        break;//берем первый найденный
                    }
            } else //чистим текст кода только при необходимости
                oEventNode.innerText = '';
            //создаем обработчик
            try {
                vHandler.handler = new Function('event', sBody);
            } catch (e) {//на случай ошибки синтаксиса
                alert(e.message + ':\n' + sBody)
            }
            oNode.attachEvent('on' + oEventNode.getAttribute('event'), vHandler);
        } else {
            var vHandler = function(event){
                var self = arguments.callee;
                var sMatch = self.node.getAttribute('match');
                if (sMatch === null || Sizzle.matches(sMatch, [event.target]).length > 0)
                    return self.handler(event);
            }
            vHandler.node = oEventNode;
            var sBody = oEventNode.textContent;
            if (!sBody.length) {//ищем код в CDATA секциях(xhtml) и в комментариях
                for(var i = 0; i < oEventNode.childNodes.length; i++) {
                    if (oEventNode.childNodes[i].nodeType) {//4 or 8 - CDATA or comment
                        sBody = oEventNode.childNodes[i].nodeValue;
                        break;//берем первый найденный
                    }
                }
            } else //чистим текст кода только при необходимости
                oEventNode.textContent = '';
            //создаем обработчик
            try {
                vHandler.handler = new Function('event', sBody);
            } catch (e) {//на случай ошибки синтаксиса
                alert(e.message + ':\n' + sBody)
            }
            oNode.addEventListener( oEventNode.getAttribute('event'), vHandler, false);
        }
    };

    ev.init = function(event){
        var arr = [];
        if (document.getElementsByTagNameNS) {//xhtml
             arr = document.getElementsByTagNameNS(ev.namespaceURI, 'handler')
        }
        if (!arr.length) {
            if (ev.browser.ie) { //ie
                arr = []; //convert a nodelist to array
                var arr2 = document.getElementsByTagName('handler');
                for (var i = 0; i < arr2.length; i++) {
                    if (arr2[i].tagUrn == ev.namespaceURI) {//accept our namespace only
                        arr.push(arr2[i]);
                    }
                }
            } else //html
                arr = document.getElementsByTagName('tb:handler');
        }
        for (var i = 0; i < arr.length; i++) {
            ev.setEventHandler(arr[i].parentNode, arr[i]);
        }
    };
    if (ev.browser.standard) {
        window.addEventListener('load', ev.init, false);
    } else
        window.attachEvent( 'onload', ev.init);
}


* This source code was highlighted with Source Code Highlighter.
Тестовая страничка:
    .test-div {
        width: 200px;
        height: 200px;
        background-color: #6699FF;
        border-bottom: 1px dotted #666666;
    }
    .inner-div {
        width: 100px;
        height: 100px;
        background-color: #66CCCC;
    }
    .test-div2 {
        width: 200px;
        height: 200px;
        background-color: #6699FF;
    }
    .inner-div2 {
        width: 100px;
        height: 100px;
        background-color: #66CCCC;
    }
 </style>
 <script type="text/javascript" src="sizzle.js"></script>
 <script type="text/javascript" src="ev2.js"></script>
</head>
<body>
 Click on boxes to see what event handlers are called.
 <div class="test-div">
    <tb:handler event='click' match='.inner-div'><!--
        alert('A: ' + event.target.className)
        //comment
        alert('<A2>')
    --></tb:handler>
    <tb:handler event='click' match='.test-div'><!--
        alert('B: ' + event.target.className)
        //comment
        alert('<B2>
')
    --></tb:handler>
    <div class="inner-div">XXX</div>
 </div>
 <div class="test-div2">
    <tb:handler event='click' match='.inner-div2'>
        alert('C: ' + event.target.className)
    </tb:handler>
    <div class="inner-div2">YYY</div>
 </div>


* This source code was highlighted with Source Code Highlighter.

онлайн html тест xhtml тест код ev2.js
Код находится внутри комментария, чтобы избежать парсинга браузером и сохранить пробелы и переводы строк. В случае xhtml помещайте код в CDATA секцию.
Заметьте использование библиотеки sizzle для работы CSS селекторов. Аналогично вы можете добавить свои расширения функциональности обработчиков событий.

Заключение:
Приведенный код является лишь небольшой демонстрацией. Если вы ищите хороший фреймворк для серьезных проектов и вам не претят декларативные языки, то рекомендую взглянуть на Backbase и на AmpleSDK. Собственно над обоими фреймворками поработал один человек. О последнем (AmpleSDK) вскоре должен быть анонс на аяксине. Автор пока поражен в своих правах старым контрактом и не может открыть исходный код. Но как только срок этот пройдет (уже скоро), обещал сделать фреймворк опенсорсным. Во фреймворке реализованы множество других модулей(svg, smil, xinclude, schema ...), все это кроссбраузерно(включая IE) и, что самое главное, использует стандартный API, а не доморощенные интерфейсы. Поэтому начать использовать просто — достаточно полистать стандарт.

ЗЫ. Код можно использовать без ограничений для любых своих проектов. Он ни откуда не скопирован. Я принципиально не заглядывал в реализацию доступных фреймворков, чтобы избежать этого.
UPD. вот и анонс AmpleSDK поспел: ajaxian.com/archives/ample-sdk-browser-in-a-browser
Tags:
Hubs:
Total votes 9: ↑6 and ↓3+3
Comments12

Articles