Заметки за январь 2009 года

Привязка данных к объектам страницы

Мне почти каждый день приходится писать различные виджеты для манипуляции с данными — динамические таблицы, списки из красиво оформленных однотипных элементов и т.п. Конечно же, в этих элементах были кнопки или поля ввода, которые влияли на данные, связанные с этим элементом.

Для небольших виджетов я нашел оптимальным отделять этап создания представления и подключения контролера.

var obj = [], i;
obj.push('<div class="wrapper">');
for (i = 0; i < model.lenght; i++) {
    obj.push('<div class="item">');
    obj.push('<h1>', model[i].title, '</h1>');
    obj.push('<div class="input"><input type="text" value="', model[i].value, '" /></div>');
    obj.push('</div>');
}
obj.push('</div>');
$(root).html(obj.join(''));
$('div.input input', root).change(function () {
    // нужно сохранить значение в модель
});

В обработчике нужно сохранить значение поля ввода в соответствующее поле модели. Но как узнать, какое поле было изменено? Некоторое время я при создании представления использовал атрибут id, по которому можно было впоследствии определить порядковый индекс в модели или перебором найти аналогичное значение у идентификационного поля элемента модели. Тут в бой вступали сложные регулярные выражения, различные проверки и т.п. Внутри я понимал, что это далеко не самый лучший способ и решил найти более эффективное решение. Оно в итоге действительности оказалось простым.

(function () {
    var uidx = 1, storage = {};

    /**
     * Generate an id that is unique among application
     * @method guid
     * @param pre {String} optional guid prefix
     * @return {String} the guid
     */
    APP.guid = function (pre) {
        var p = (pre) || 'app';
        return p + '-' + uidx++;
    };

    /**
     * Make a save point for linking DOMNodes and Objects via IDs
     * @param obj {Object}
     * @param pre {String} optional guid prefix
     * @return {String} the guid
     */
    APP.savepoint = function (obj, pre) {
        var guid = APP.guid(pre);
        storage[guid] = obj;
        return guid;
    };

    /**
     * Recall data for save point
     * @param guid {String} id of save point
     * @return {Object} stored data
     */
    APP.recall = function (guid) {
        return (guid in storage) ? storage[guid] : null;
    };
})();

Теперь поле ввода я создаю с уникальным id.

obj.push('<div class="input"><input type="text" value="', model[i].value, '" id="', APP.savepoint(model[i]), '" /></div>');

А реализация обработчика выглядит не менее сложной.

function () {
    var o = APP.recall(this.id);
    o.value = this.value;
}

В jQuery есть методы для связывания уже существующих объектов с моделью. Но чтобы подключить контроллер пришлось бы делать еще один цикл по элементам, что в общем случае может быть крайне сложным.

Оставте свой комментарий

Динамическая загрузка JavaScript файлов

Техника динамической загрузки данных через XHR (в простонародии, AJAX) очень популярна в современном WEB-е. Но вот динамическую загрузку программных компонент используют пока крайне редко. Компактный модуль ScriptLoader наглядно демонстрирует то, как это может быть просто и функционально.

/**
 * JavaScript loader
 * @param modules {Array} массив имен js файлов
 */
var ScriptLoader = function (modules) {
    this.modules = modules.slice();
    this.queue = new Queue();
    this._init();
};

ScriptLoader.prototype = {
    run: function () {
        // test for header ready
        var head = document.getElementsByTagName("head");
        if (head.length > 0) {
            this.queue.iterate();
        } else {
            setTimeout(arguments.callee, 500);
        }
    },

    _init: function () {
        var i, me = this;
        for (i = 0; i < me.modules.length; i++) {
            (function (name) {
                me.queue.add(function () {
                    var head, script;
                    head = document.getElementsByTagName("head");
                    if (head.length > 0) {
                        head = head[0];
                        script = document.createElement("script");
                        script.src = name;
                        script.type = "text/javascript";
                        script.onload = script.onreadystatechange = function () {
                            if ((!this.readyState || this.readyState == "loaded" || this.readyState == "complete") ) {
                                me.queue.iterate();
                                script.onload = script.onreadystatechange = null;
                                head.removeChild(script);
                            }
                        };
                        head.appendChild(script);
                    }
                });
            })(me.modules[i]);
        }
    }
};

Обратите внимание, что в этом модуле используется Queue.

Пример использования.

var loader = new ScriptLoader(["file1.js", "file2.js", "http://server.org/js/framework.js"]);
// на этом этапе в очередь могут быть добавлены дополнительные вызовы
loader.queue.add(function () {
    Module1.init();
});
loader.run();
Комментарии к заметке: 2

Очередь: синхронное выполнение функций

С использованием событийной (event-driven) модели программирования появляется множество асинхронных вызовов, которые не всегда должны выполняться асинхронно. Чтобы упорядочить этот хаос я написал небольшой модуль, реализующий очередь.

/**
 * @method  add       добавить функцию в очередь
 * @method  interate  выполнить первую в очереди функцию и удалить ее из очереди
 * @method  clear     очистить очередь
 */
var Queue = function () {
    this.members = [];
};
Queue.prototype = {
    add: function (f) {
        if (f instanceof Function) {
            this.members.push(f);
        }
    },
    iterate: function () {
        if (this.members.length > 0) {
            var func = this.members.shift();
            func.call(this);
        }
    },
    clear: function () {
        this.members = [];
    }
};

При вызове функции, контекстом ее выполнения становится очередь. Перед тем как завершить свое выполнение каждая асинхронная функция должна позаботиться о вызове следующего элемента очереди через метод iterate.

function () {
    // сохраняем контекст (очередь) в локальную переменную
    var queue = this;
    $.get('data.xml', function (data) {
        // в этом месте this уже не ссылается на очередь
        process(data);
        // подолжаем исполнение очереди
        queue.iterate();
    });
}
Комментарии к заметке: 1

Добавить в один массив другой массив

Родной метод concat не годится, так как он делает копию массива после слияния, а нужно во чтобы то не стало сохранить исходный объект. Метод push тоже не подходит, так как добавляет массив как вложенный. Самое очевидное решение — циклом добавить элементы.

Но есть более элегантный метод!

var a = [1,2];
Array.prototype.push.apply(a, [11,12,13]);

Трюк основан на вызове через apply стандартного метода push объекта Array. Аргументы для push разворачиваются из массива. Контекстом исполнения, разумеется, является наш модифицируемый массив. Тот самый цикл, который мы могли написать явным образом и который бы исполнялся интерпретатором команда за командой, выполнится неявно и с гораздо большей скоростью.

Оставте свой комментарий

Определить координаты события (DOM Event)

Любое DOM-событие содержит в себе информацию о том, где находился указать мышки в момент возникновения этого события. Беда в том, что в различных браузерах эти поля называются по-разному и содержат информацию в различных системах отчета.

Эта функция нормализует информацию и приводит координаты в одну систему отсчета — относительно левого верхнего угла страницы.

function (e) {
    var point = {};
    if (e.pageX && e.pageY) {
        point.x = e.pageX;
        point.y = e.pageY;
    } else {
        point.x = e.clientX + (document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft);
        point.y = e.clientY + (document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop);
    }
    return point;
}

Если используется jQuery, то аналогичные данные можно получить из полей pageX и pageY объекта, который передается в обработчик обытия.

Комментарии к заметке: 1