Мультиселект и автокомплит на AngularJS

    Со списками множественного выбора на Ангуляре всегда было неважно. Существующие решения либо обертки над jQuery-плагином, либо выглядят как не пойми что, либо просто корявы. И у всех естественно особое уникальное АПИ, как будто пользователям делать больше нечего как вникать в ход мыслей разработчиков каждого плагина. Меня такое положение дел не устроило, поэтому написал свой велосипед. Спустя год он дозрел до публикации.

    Та-дам! (и забавная история вконце)

    tamtakoe.github.io/oi.select
    image

    Прежде всего решил: никакого своего АПИ, никаких сторонних библиотек и никакого своего дизайна. Селект должен быть максимально приближен к стандартному только с возможностью автодополнения и создания в нем новых опций. АПИ так же должен быть совместим с Angular select, который в свою очередь совместим с HTML select. Стандартный внешний вид использовать не получилось, т. к. он определяется браузером, поэтому взял за основу наиболее распространенный Bootstrap. По-сути, получился не новый компонент, а расширение существующего.

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


    АПИ получилось сделать полностью совместимым, за исключением задания опций через <option>. В HTML такой способ используется из-за того, что другого нет. В Ангуляре же есть контроллеры и модели, там этот способ был оставлен для совместимости. Мне было лень делать такую совместимость. Может быть когда нибудь…

    В самом начале возникло три пути создания подобного компонента: реализация на чистом JS с манипуляциями DOM и проч., расширение ангуляровской директивы select, реализация преимущественно на Ангуляре. Т.к. я не настолько умен, чтобы принимать такие решения в голове, то просто взял и реализовал все три способа:

    1. Самое заманчивое — написать свой select по аналогии с ангуляровским. Чистый JS, максимальная производительность, всё под контролем. Но сделать такое оказалось не просто: слишком много нюансов, требуется знание Ангуляра на самом низком уровне, много копипаста функций, которые Ангуляр реализует внутри. В итоге все это вываливается в многие тысячи строчек кода со всеми вытекающими. Отказался от этой затеи, хотя для простых компонентов она бы подошла.
    2. Можно использовать в своей директиве Directive Definition Object ангуляровского select. Расширить его, переопределить методы и т. п. Звучит хорошо, но на деле получается слишком костыльно. Все-таки Ангуляр пока не дает возможность для расширения своих компонентов, особенно директив (к сожалению), поэтому расширяя средствами JS вы завязываетесь на внутреннюю реализацию и рискуете потерей обратной совместимости. Такой способ допустим, в директивах, где расширение предусмотрено разработчиками, например Angular-bootstrap popover.
    3. Проще и нагляднее оказалась реализация средствами Ангуляра. Из копипаста только Regexp для парсинга параметров из ng-options. Код проще чем на чистом JS и не требует знания внутреннего устройства ангуляровского селекта. Производительность хорошая (за что я больше всего боялся). Думаю, этот способ подойдет для реализации большинства компонентов.


    Не смотря на совместимость по АПИ, уникальных параметров хватает. Полагаю, там еще не всё гладко и очевидно, так что буду рад замечаниям в комментариях. Много чего относится к созданию новых элементов из строки ввода. После некоторого анализа пришел к выводу, что существует два случая использования поля ввода с подсказками:

    • prompt — поле работает как обычный инпут, а в списке просто выводятся подсказки. По нажатию Enter в модель попадает значение из поля. Поле можно очистить и тогда в модели будет пустая строка. Такое поведение характерно для поисковой формы.
    • autocomplete — поле работает как список с вариантами. По нажатию Enter в модель попадает первый вариант из списка и только если там ничего не было — содержимое поле ввода. Записать в модель пустую строку нельзя. Такое поведение характерно для формы ввода тегов.

    Очередной спорный пункт: кастомное оформление опций. В стандартном компоненте (даже ангуляровском, даже бутстраповском) ничего подобного нет. Но всем хочется. Пришел к компромиссному решению — сделать поддержку фильтров для всего и вся. Конечно, придется писать HTML в JS, зато и быстродействие выше и значительно проще чем заморачиваться с поддержкой шаблонов (да и какие могут быть шаблоны для элемента списка?). Впрочем, когда-нибудь…

    А теперь обещанная забавная история для осиливших страницу текста без картинок.

    В одной из версий была бага, которую никто из разработчиков веб-отдела не мог воспроизвести. Зато остальные сотрудники: менеджеры, аналитики, тестировщики, 1С-программисты воспроизводили в любом случае. Доходило до слез — за одним компьютером одни люди всегда выбирают элемент из списка, у других это ни получается как бы они ни старались. Думаю, это свойство можно использовать при приеме людей на работу. Плохие программисты тест не пройдут. Проверь себя и ты, читатель.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 71
    • 0
      Сделано аккуратненько, молодцы!
      Про баг
      А откуда там вообще double-click взялся?
      • +1
        Дело там не в дабл клике, хотя он, наверное, может помочь. Нужно выбирать обычным кликом)
        • +3
          Смог воспроизвести баг. Но как он к программистам/не-программистам относится я не понял.
          • +1
            Это эмпирический вывод
            Суть бага в том, чтобы сделать клик менее чем за 100 мс. Получается, что у людей с богатым опытом использования компьютера (профессиональным, игровым) скорость выше. У людей старшего поколения и тех кто с компьютером на Вы ничего не выходит.
            • 0
              Неточное описание поведения
              Сначала у меня получалось не всегда.
              Потом я нашёл надёжный способ выбрать любой пункт —
              1.открыть дропдаун
              2. не торопясь посмотреть на варианты
              3. тут надо успеть за 100мс: нажать/зажать ctrl или shift или alt и сразу кликнуть мышкой

              также работает если зажать кнопку заранее, потом отпустить и быстро кликнуть, но намного менее надёжно
              • 0
                У меня и моих коллег получалось в 100% случаев обычным щелчком. Специально хотели воспроизвести баг, но не могли. Только потом, когда понял в чем суть, удалось воспроизвести)
          • +1
            Долго не мог понять в чём дело, но всё же баг воспроизвёл. Посему вопрос: как можно было так криво сделать?
            • 0
              Это появилось из-за хака для всех браузеров кроме Хрома. В этих браузерах нельзя понять чем было вызвано событие blur, переводом фокуса или программно, поэтому пришлось проверять через некоторый промежуток времени что инпут действительно потерял фокус и триггерить blur на элементе селекта. Причем из-за особенностей браузерной работы с событиями этот промежуток времени не маленький. С 10 мс не работало, с 50 мс через раз. github.com/tamtakoe/oi.select/blob/0.2.8/src/services.js#L99
              • +1
                А какая разница как вызывается blur? Зачем этот хак?
                • 0
                  Без него в FF, IE и проч. не будет работать переход по табу. Т.е. фокус сместится с одного инпута на другой, но компонент об этом никак не узнает, т. к. нет элемента в event.relativeTarget и он подумает, что blur был вызван программно и не станет вызывать его второй раз.
            • +3
              А я, кажется, второй баг нашел — на странице «Funny» выбор с клавиатуры (нажатием Enter) не работает вообще никак.
              Решил что я, наверное, совсем плохой программист )))
              • 0
                вот блин, внизу уже ответили. извиняюсь, недочитал комментарии…
          • 0
            На странице Customization не скроллится список тачем, если при открытом списке тапнуть на строку. Win 10 + Firefox 41
            • 0
              Это на телефоне?
              • 0
                На десктопе.
                • 0
                  Странно. В последней версии тоже?
            • 0
              Пара багов:
              1) 938 пикселей по ширине экран www.dropbox.com/s/qupszykt884axpb/Screenshot%202015-08-30%2013.24.55.png?dl=0
              2) Не открывается вверх, если места снизу нет www.dropbox.com/s/k8vbs275eirphyt/Screenshot%202015-08-30%2013.25.31.png?dl=0
              3) Если открыть выпадашку стрелками, то автокамплит не работает, нельзя ввести значение, можно только выбрать. Хотя, это может быть и фича такая.

              Пара замечаний:
              1) На мобильных устройствах желательно оставлять оригинальный селект.
              2) Если у контейнера, в котором лежит селект будет overflow:hidden, то могут быть неприятные баги. Например, обрезанная выпадашка.
              3) Еще не нашел оригинального селекта на странице. Он удаляется?
              • 0
                Баги:
                1) поправлю
                2) Стандартный селект не поднимается наверх
                image
                Но идея хорошая, нужно будет сделать с помощью CSS.
                3) не удалось повторить. Это в каком примере наблюдается?

                Замечания:
                1) В мобильных устройствах так же нет автокомплита, поэтому стандартный селект не прокатит.
                2) Не знаю как это побороть и надо ли. Тут, наверное, будет лучше сделать вариант с выпадашкой наверх.
                3) Стандартного селекта там нет. С ним получалось слишком костыльно.
                • +1
                  2) Открывается наверх, попробуйте сделайте селект в самом низу страницы, без открытых дев-тулзов. Он откроется вверх.
                  3) В любом. Просто делаете фокус на селект и нажимаете стрелку вниз. Селект откроется, а написать ничего нельзя.

                  Про замечания:
                  1) Видимо стоит только для этого случая оставить кастомный. В остальных случаях 100% удобнее пользоваться нативным.
                  2) Стоит, неизвестно, в каком месте селект будет вызываться. Побороть легко, просто держите выпадашку в самом низу body и подцепляйте ее при открытии к селекту.
                  3) А в чем были костыли?
                  • 0
                    3) Странно. У меня можно писать и в Хроме и в ФФ. Что за система/браузер?

                    Подумаю, чтобы аппендить выпадашку к body или сделать такую опцию как в angular-bootstap для тултипов

                    Пробовал способ, когда в основе лежит стандартный селект, скрытый из виду, и в него копируются опции из oi-select. Нужно было перекомпилировать элемент, следить чтобы связь между областями видимости не порвалась при использовании, например, ng-if на директиве. Код был сложнее для восприятия, да и смысла не было, т. к. всё равно почти вся функциональность не была привязана к стандартному селекту.
            • 0
              Почему не взяли select2?
              • 0
                Во-первых, он на jQuery, который уже как пол года назад удалось выпилить из проекта. Из jQuery нужна была только функция определения высоты элемента (меньше 100 строчек). Тащить ради нее всю библиотеку как-то странно.
                Во-вторых, никакой совместимости по АПИ там и близко нет. Пришлось бы писать огромный адаптер или мучиться.
                В-третьих, слишком много отсебятины. Для множественного выбора поле ввода находится в строке с выбранными элементами, для одиночного — в списке, чтобы удалить тег нужно попасть по маленькому крестику, какие-то крестики для одиночного ввода… Не продуманный дизайн. Такое ощущение, что его писали разные люди, которые не смогли договориться.
                • 0
                  А как насчет angular-ui-select?
                  • 0
                    Практически то же самое. Год назад он был самым вменяемым, но, по сути, представлял обертку над select2, тащил jQuery и был глючным. Сейчас плагины подросли, но до уровня oi.select все еще не дотягивают. Пока ни один плагин не учитывает, что у Ангуляра есть готовое АПИ для селекта и предлагает свое, а это показатель уровня разработчиков.

                    P. S. Понравился в свое время brianreavis.github.io/selectize.js, с него скопировал лучшие наработки в дизайне.
                    • 0
                      Кстати, на jquery мне нравится chosen
                      • 0
                        По-моему, слишком много отсебятины. Какие-то крестики для очистки модели в одиночном инпуте. Для одиночного и множественного селекта поля поиска выглядят по-разному, список не закрывается по щелчку на пустом поле ввода (родной селект так себя не ведет). И как-то всё неаккуратно.
                        • 0
                          очистка в одиночном удобно, если есть несколько зависимых списков.

                          например,

                          страна — город — улица

                          тогда при очистке страны очищается и страна, и все зависимые.

                          одним кликом
                          • 0
                            Показываю крестик в варианте cleanModel: true (http://tamtakoe.github.io/oi.select/#/select/#cleanmodel)

                            Там модель очищается при клике в любом месте. Не сторонник маленьких кнопочек и крестиков, по которым фиг попадешь, особенно на мобильном устройстве)
                • 0
                  А есть так, чтобы было как это tamtakoe.github.io/oi.select/#/select/#editableoptions, но с возможностью добавлять свои таги, а не только те, которые есть в списке?
                  • +1
                    См. prompt, autocomplete. Об особенностях описано в статье
                      • +1
                        Поправил
                        • 0
                          вероятно, здесь в конце нужно писать newItemFn?
                        • 0
                          какой-то странный баг — добавляет только ОДНО, первое значение. а дальше — ничего…

                          и только если редактируешь и опять добавляешь, тогда начинает добавлять и дальше.
                          • 0
                            Это решено, у меня были дубли в id.
                          • 0
                            Как customize delete button?
                            • +1
                              delete button на самом деле нет. Ее добавляет фильтр по-умолчанию

                              .filter('oiSelectCloseIcon', ['$sce', function($sce) {
                                  return function(label) {
                                      var closeIcon = '<span class="close select-search-list-item_selection-remove">×</span>';
                              
                                      return $sce.trustAsHtml(label + closeIcon);
                                  };
                              }])
                              

                              В selectFilter можно переопределить на свой фильтр и делать там что угодно
                            • 0
                              Почему, когда фокус убирается с поля ввода, элемент добавляется? Возможно ли сделать так, чтобы добавлялся только по enter?
                              • +1
                                Вообще, не должен. Разве что в случае с одиночным селектом. Он показывает выбранное ранее значение. Это сделано специально, т. к. так ведет себя обычный селект
                              • 0
                                Баг — если редактируешь значение, то если в это время удалить другой элемент, то пропадают оба элемента.
                                • +1
                                  Сейчас если стоит режим редактирования элементов, то удалить элемент как-бы нельзя. Т. е. из модели он удаляется, но на его месте остается его текст. Если вводить какой либо текст и удалить при этом любой элемент, то вводимый ранее текст заменится на текст удаленного элемента. Должно так работать
                                • 0
                                  how to customized full own template.html?
                                  • +1
                                    template.html очень не рекомендуется заменять на свой, т. к. это может нарушить работу элемента. Постарался сделать так, чтобы внешний вид можно было полностью настроить фильтрами и стилями.
                                    • 0
                                      спасибо, буду пытать
                              • 0
                                и ещё

                                newItemModel: {id: null, name: $query},

                                как-бы id автоинкремент сделать?

                                update: понял, использовать newItemFn

                            • 0
                              по поводу бага — выбор по enter тоже не работает))
                              • 0
                                Я его специально запретил в том примере, чтобы не хитрили)
                              • +1
                                Вот такое еще есть mbenford.github.io/ngTagsInput/demos
                                • 0
                                  Неплохой мультиселект. Некоторые примеры ведут себя не очевидным образом. У меня на первых порах тоже такое было. А вот валидацию текстового ввода возьму на заметку.
                                • 0
                                  Спасибо за классную реализацию сохранения новой модели (http://tamtakoe.github.io/oi.select/#/select/#prompt).
                                  Давно пытался найти такой плагин.

                                  Сейчас очень серьезно размышляю о переходе с angular-ui/ui-select на ваше решение в своем большом проекте.

                                  Подскажите, можно ли сделать вывод списка вот в таком формате:


                                  Скриншот из демки ui-select

                                  Нужно, чтобы в выпадающем меню выводилось несколько разных параметров, согласно моему шаблону, и чтобы работал фильтр (поиск) по всем параметрам (или, как это можно в ui-select, по тем, которые нужны).
                                  Я нашел только такой пример — http://tamtakoe.github.io/oi.select/#/select/#customization, но там фильтр не работает совсем.
                                  • +1
                                    Ответил случайно в корень, а не в эту тему. Запилил простенькую реализацию поиска по другим полям для плоских объектов tamtakoe.github.io/oi.select/#/select/#filtered
                                    • 0
                                      Спасибо за ответ и пример.
                                  • +2
                                    Для форматирования выбранных тегов нужно использовать searchFilter, для вариантов в списке — dropdownFilter. Поиск осуществляется только по одному полю, указанному в oi-options (что полностью соответствуют ng-options), но можно самому формировать поисковую выдачу:
                                    — задать в качестве списка функцию, принимающую поисковую строку и возвращающую список tamtakoe.github.io/oi.select/#/select/#lazyloading
                                    — переопределить listFilter в который передается поисковая строка и список в которой так же можно как угодно этот список фильтровать tamtakoe.github.io/oi.select/#/select/#customization

                                    Вообще, подумаю, чтобы расширить встроенный listFilter, чтобы туда передавать параметры для поиска по другим полям.

                                    P. S. Как минимум в одном большом проекте это решение уже используется — в моём, так что если какие-то серьезные баги всплывают, узнаю об этом очень быстро)
                                    • 0
                                      Можно ли сделать autocomplete по ajax?
                                      • +1
                                        Запросто. Возвращай промис в функции, формирующей список и ищи в базе по строке tamtakoe.github.io/oi.select/#/select/#lazyloading
                                        • 0
                                          ok, thanks
                                          • 0
                                            А как сделать так, чтобы oi-options первоначально принял массив, а уже потооом был бы autocomplete?
                                            • 0
                                              функция, возвращающая список вариантов по строке должна возвращать все варианты, если строка пустая… Если правильно понял что было нужно
                                        • 0
                                          как повесить на Enter обработчик события?
                                          • 0
                                            Никак нельзя (конечно, можно обернуть все в свою директиву или найти элемент селекта в контроллере, а там найти инпут и повеситься на Enter, но это нехорошо).

                                            Нужно следить за изменением модели с помощью watch и выполнять нужные действия. Так код будет максимально независим от элемента селекта.
                                            • 0
                                              Вы могли бы добавить в код возможность подписки на событие keyDown в поле ввода?

                                              • 0
                                                В будущем перенесу нативные события инпута на елемент компонента
                                              • 0
                                                И ещё есть баг — если в пустое поле ввода сделать фокус, то вызывается функция autocomplete с пустым запросом. Думаю, нужно сделать так, чтобы не вызывалась.
                                                • 0
                                                  Так специально сделано. При пустом запросе часто возвращают либо весь список либо популярные значения
                                                • 0
                                                  К сожалению, пришлось отказаться от вашего плагина по причине отсутствия API событий и возможности расширения.
                                                  Сделал на коленке за часик свой редактор тагов.
                                                  • 0
                                                    События это зло. Их просто использовать, но невозможно отлаживать и они сильно ломают логику приложения. Лучше избегать их по возможности. Над расширением подумаю
                                            • 0
                                              Спасибо вам за плагин. Постепенно осваиваем как прозрачную замену чёрному ящику из костылей select2.

                                              Пока возник вопрос с динамическими интерполяциями.

                                              В нашем случае на форме при смене типа автомобиля меняется источник данных для списка трансмиссий, и одновременно обязательность этого поля. Если поле становится необязательным, пользователь должен уметь его очистить. В директиве есть опция cleanModel, но она проверяется один раз при компиляции, так что oi-select-options="{cleanModel:isRequired(vehicleType)}" выполнится только для исходного состояния формы. Смена vehicleType не приведёт к вычислению нового значения cleanModel и не разрешит очищать поле.

                                              Есть ли возможность реализовать разбор elementOptions динамически и навесить этот разбор на нужные $watch (в нашем случае, смена источника данных)? Перекомпилировать всю директиву ради этого выглядит большим оверхедом.

                                              Спасибо!
                                              • 0
                                                Посмотрю что можно сделать. Не обещаю, но постараюсь до нового года

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