Всем привет. Совсем недавно, в одном проекте понадобилось сделать «запоминание e-mail».

Требования простые: после того, как пользователь ввел e-mail, его нужно запомнить в cookie и автоматически заполнять при следующих обращениях к сайту.

AngularJS, не подвел! Решение получилось компактным и надеюсь, наглядным.

  1. В Javascript определяем директиву
  2. В html используем директиву чтобы указать что нужно «запомнить»

Для работы с cookie использовался jquery и плагин jquery-cookie.

Вот собственно демонстрация http://jsfiddle.net/dzb5rcsw/


Подробности


Дальше я буду разжевывать предложенное решение. Если заскучаете — переходите к коду в конце статьи.

IMHO самый лаконичный способ объявить какие e-mail должны запоминаться — это использовать директиву.

<body ng-app='habr-demo-app'>
    ...
    <input type='email' habr-remember-in-cookie='cookie-name-for-email1'/>
    ...
</body>


В моем случае использовался JSF, там объявление директивы немного отличается, но суть та же.

<h:inputText value="#{mybean.email}"
    pt:habr-remember-in-cookie="cookie-name-for-email1"/>


Директива


У меня уже был модуль AngularJS, поэтому я просто добавил к нему новую директиву. Но для демонстрации модуль придется создать.

Для начала обернем наш код в вызов анонимной функции (чтобы не захламлять глобальное пространство имен. А еще, потому, что так рекомендуют в Angular Style Guide https://github.com/johnpapa/angular-styleguide)

(function() {
    // Здесь будет наш код
)();


Затем объявим модуль и директиву

var app = angular.module('habr-demo-app', []);

app.directive('habrRememberInCookie', habrRememberInCookie);

function habrRememberInCookie() {
    function link(scope, element, attrs) {
    }
    return {
        restrict: 'A',
        link: link
    };
}


Как видите, никаких сложностей нет — все по документации AngularJS. От нас потребовалось только указать restrict: 'A' для того, чтобы директиву можно было использовать только как атрибут.

AngularJS вызывает функцию link для каждой директивы на странице. Про нее хорошо написано в документации.

В принципе весь церемониальный код уже написан, осталось написать саму функцию link. В ней нужно записать в поле ввода значение из cookie и настроить обработчик события change. При возникновении события change записываем новое значение в cookie.

function link(scope, element, attrs) {
    if(!element.val()) {
        var savedValue = $.cookie(attrs.habrRememberInCookie);
        element.val(savedValue);
    }
    element.on('change', function(event) {
        var newValue = element.val();
        $.cookie(attrs.habrRememberInCookie, newValue, {expires: 360});
    });
}


Вот так, все просто. По-хорошему нужно еще написать обработчик $destroy, но в приведенном примере в нем совершенно нечего делать. А вообще, он выглядит вот так.

element.on('$destroy', function() {
    console.log('Element deleted');
});


Код


HTML
<body ng-app='habr-demo-app'>
    <input type='email' habr-remember-in-cookie='cookie-name-for-email1'/>
</body>


JavaScript
(function() {
    var app = angular.module('habr-demo-app', []);
    
    app.directive('habrRememberInCookie', habrRememberInCookie);
    
    function habrRememberInCookie() {
        function link(scope, element, attrs) {
            if(!element.val()) {
                var savedValue = $.cookie(attrs.habrRememberInCookie);
                element.val(savedValue);
            }
            
            element.on('change', function(event) {
                var newValue = element.val();
                $.cookie(attrs.habrRememberInCookie, newValue, {expires: 360});
            });
        }
        return {
            restrict: 'A',
            link: link
        };
    }
})();

Спасибо за внимание. Критика приветствуется.

Комментарии (15)


  1. dannyzubarev
    18.06.2015 01:42
    +1

    Зря тянете целую jQuery + плагин для работы исключительно с печеньками, если все можно реализовать используя встроенный jqLite и angular-cookies. :)

    Можно сделать вот так: embed.plnkr.co/QFYrwgLha5NAalWlXAjD/preview


    1. SamSol Автор
      18.06.2015 09:24
      +1

      Здесь я использую jquery-cookie для наглядности. Так как angular-cookie (когда я в последний раз на него смотрел) не позволяет кратко задать срок действия cookie.

      В вашем примере (с использованием angular-cookie) testCookie является сессионным cookie. И при повторных входах пользователей значение не будет восстановлено. Такое решение мне не подходит.

      В angular-1.4.0-beta.6 модуль angular-cookie уже доработан, поэтому когда он выпустится в релиз, можно будет отметить это здесь.


      1. dannyzubarev
        18.06.2015 13:13

        Да, вы правы насчет того, что в моем примере session cookie – я даже об этом не задумывался, если честно. Но это все фиксится дополнительным аргументом при установке cookie. И насчет модуля ngCookies вы совершенно верно дополнили, что нормальная реализация как раз с версии 1.4. В моем примере как раз и используется AngularJS 1.4.1. :)


  1. saggid
    18.06.2015 02:31
    +3

    Ммм… А вам реально там надо делать такую мелочь через какой-то суперсовременный фреймворк? Это же делается в 10 строк на обычном JS + немного jQuery.

    Вот вам пример той же функциональности без всяких ангуляров: jsfiddle.net/dzb5rcsw/1

    Ко всем нужным инпутам на странице добавляем атрибут saveable, и уаля: отныне его значение будет всегда сохраняться в куках и восстанавливаться при обновлении страницы.


    1. noder
      18.06.2015 07:39

      Я так понимаю что AngularJS уже был в проекте, вот автор и использовал его.


    1. youlose
      18.06.2015 07:48
      -1

      Одна разница:
      в ангуляре потом очень легко найти и подправить (если нужно), этот функционал. А эти хандлеры для jQuery утонут в тоннах такого же неструктурированного кода.


    1. SamSol Автор
      18.06.2015 09:57
      +1

      Хороший код. Вот что бы я еще с ним сделал:

      — для наглядности я бы прописал имя куки в атрибут saveable (и чтобы отвязаться от имени поля в именах кук)
      — исправил ошибку с восстановлением значения только в случае если поле не заполнено
      — и пожалуй переименовал бы атрибут во что-нибудь вроде save-to-cookie (или data-my-save-to-cookie)

      jsfiddle.net/dzb5rcsw/2

      Получился отличный jQuery код. От решения на AngularJS отличается только тем, что здесь код помещается в общую кучу, а в AngularJS он оформляется во вполне определенный компонент (директиву), и немного лучше подходит для повторного использования или модульного тестирования.

      Нужен ли AngularJS ради такой мелочи?

      А если таких мелочей хотя бы штук 5? Тогда в AngularJS будет 5 простых директив, а в JQuery 10 фрагментов. Которые со временем мигрируют в один блок $(document).ready(funtion() {}); и будет потеряна возможность модульного тестирования каждого фрагмента по отдельности.


      1. saggid
        19.06.2015 02:41

        Чтобы не смешивать в одной куче такие небольшие кусочки JS-кода, я обычно размещаю каждый из них в своём отдельном js-файле, а также помещаю весь функционал в отдельную родительскую js-функцию с ясным названием. Таким образом, можно сказать, мы создаём свой компонент на чистом JS. По моему личному опыту, в 90% случаев этого более чем достаточно.

        В данном нашем случае мы можем написать, скажем, компонент под названием SaveableInputs, поместить его в отдельный js-файл под названием saveable-inputs.js, и если понадобится, позже спокойно рефакторить и дорабатывать его. А впоследствии его можно будет даже залить на гитхаб и спокойно подключать его последние версии через bower ко всем своим проектам, нуждающимся в подобном функционале :)

        Вот пример кода данного компонента: jsfiddle.net/dzb5rcsw/11

        При необходимости, мы также можем вызвать метод SaveableInputs.loadFromCookies() из любого места нашего приложения (в том числе и где-то в ангуляровском коде). Например, после динамичной подгрузки и отображения какой-то формы, чтобы попытаться заполнить её поля.

        И еще, насчёт названий кук. Я не согласен с вами. Автоматически генерировать название куки удобнее, особенно если этих инпутов у вас будет много на странице. Еще это не даст запутаться. Правда, если у вас на странице будет два инпута с одинаковым значением name, то заполнятся оба из них одним и тем же текстом… Ну тогда уж всегда можно будет доработать компонент)

        Я, кстати, как вы наверное заметили, не использую некоторые известные методы jQuery, вроде el.val() и el.attr(). Просто стараюсь иногда почитывать документацию ванильной JS и использовать её, благо сейчас необходимость в jQuery-методах уже практически отпала: все браузеры уже давно реализовали и поддерживают основную JS-спецификацию, и её можно спокойно использовать как основу для работы с JS.


    1. andrewiWD
      19.06.2015 10:21
      +1

      А вам действительно нужен jQuery? Тем более только для связи с печеньками и подстановкой в инпут? Почему бы не написать на пару строк больше и сделать независимый функционал.


      1. saggid
        20.06.2015 16:18
        -1

        Ну как сказать) Согласитесь: на текущий 2015-й год jQuery имеется на подавляющем большинстве современных сайтов. Это первая причина, по которой нет особого смысла полностью от него отказываться. Вторая причина заключается в том, что есть множество сторонних плагинов и библиотек, прекрасно дополняющих jQuery и — опять же, — не способных работать без него. Третья причина заключается в том, что вместе с ним наш код, всё-таки, как ни крути, но становится более простым и лаконичным. Четвёртая причина заключается в том, что свои велосипеды редко получаются реально хорошо написанными и понятными другим разработчикам, и его будет труднее поддерживать. Пятая причина заключается в том, что jQuery и его плагины уже были протестированы большим сообществом, что не скажешь о моих (и ваших) велосипедах.

        Но если вы реально гуру Javascript и можете писать крутой и лёгкий для восприятия код без jQuery — тогда конечно можно попробовать. Но я о себе такое врятли смогу сказать. Хоть я и написал уже кучу всего на JS, всё равно не считаю себя великим знатоком данного языка.

        Ну так вот, подводя итоги… Какой смысл отказываться полностью от данного удобного инструмента, если отказаться от него всё равно будет очень трудно, да и пользы особой от этого не будет? Да и не будем мы тратить своё личное время только ради того, чтобы потенциально сделать проект чуть более легким и независимым (да и не факт совсем, что это у нас получится).

        Поэтому, ладно уж. Пусть будет и немного jQuery :)


  1. lega
    18.06.2015 10:33
    +6

    Если подключить ng-model то перестает работать в Chrome.

    Почему Cookie, а не localStorage?


    1. hell0w0rd
      18.06.2015 20:40

      Чтобы при перехвате трафика могли пользователей распознавать.


    1. SamSol Автор
      20.06.2015 02:24

      О, это хороший вопрос! Как это сделать технически уже показал AMar4enko.

      А вот с точки зрения архитектуры…
      Если есть модель данных, то начальные значения этих данных должны определяться в том месте где эта модель создается (или там где заполняется — если данных загружаются асинхронно). Получается, что в случае, когда поле ввода «привязано» к модели данных с помощью ng-model конструкция habr-remember-in-cookie окажется вредной :-(

      Особенный вред проявится, если данные для модели загружаются асинхронно. М-да… спасибо, критика оказалась полезной.


  1. AMar4enko
    19.06.2015 02:06

    Версия с поддержкой ngModel. Писал навскидку, мог где-то ошибиться.
    Также поддержка интерполяции внутри атрибута и сохранение в куку, только если введенный текст — email.

    (function() {
        var app = angular.module('habr-demo-app', []);
        
        app.directive('habrRememberInCookie', habrRememberInCookie);
        
        function habrRememberInCookie($interpolate) {
            function link(scope, element, attrs, ngModel) {
                var cookieName = $interpolate(attrs.habrRememberInCookie)($scope); 
                if(ngModel) {
                    if(!ngModel.$viewValue) {
                        ngModel.$setViewValue($.cookie(cookieName));
                    }
                    ngModel.$viewChangeListeners.push(function(value) {
                        if(!ngModel.$error.email){
                            setCookieValue(value);    
                        }
                    }); 
                     
                    return;  
                }
    
                if(!element.val()) {
                    var savedValue = $.cookie(cookieName);
                    element.val(savedValue);
                }
                
                element.on('change', function(event) {
                    var newValue = element.val();
                    setCookieValue(newValue);
                });
                
                function setCookieValue(value) {
                   $.cookie(cookieName, value, {expires: 360}); 
                } 
            }
            return {
                restrict: 'A',
                require: '?ngModel',
                link: link
            };
        }
    })();
    


  1. glagola
    19.06.2015 23:37

    Почему не Cookie/localStorage/sessionStorage/…?

    Мне кажется, что код сохранения/изъятия значения из хранилища стоит вынести в отдельный сервис, который будет подключаться через Dependency Injection, так его можно будет легко подменить, что упростит переиспользование кода.