Со списками множественного выбора на Ангуляре всегда было неважно. Существующие решения либо обертки над 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С-программисты воспроизводили в любом случае. Доходило до слез — за одним компьютером одни люди всегда выбирают элемент из списка, у других это ни получается как бы они ни старались. Думаю, это свойство можно использовать при приеме людей на работу. Плохие программисты тест не пройдут. Проверь себя и ты, читатель.

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


  1. Zdomb
    30.08.2015 04:17

    Сделано аккуратненько, молодцы!

    Про баг
    А откуда там вообще double-click взялся?


    1. tamtakoe
      30.08.2015 05:15
      +1

      Дело там не в дабл клике, хотя он, наверное, может помочь. Нужно выбирать обычным кликом)


      1. a553
        30.08.2015 08:02
        +3

        Смог воспроизвести баг. Но как он к программистам/не-программистам относится я не понял.


        1. tamtakoe
          30.08.2015 14:01
          +1

          Это эмпирический вывод
          Суть бага в том, чтобы сделать клик менее чем за 100 мс. Получается, что у людей с богатым опытом использования компьютера (профессиональным, игровым) скорость выше. У людей старшего поколения и тех кто с компьютером на Вы ничего не выходит.


          1. owniumo
            03.09.2015 06:12

            Неточное описание поведения
            Сначала у меня получалось не всегда.
            Потом я нашёл надёжный способ выбрать любой пункт —
            1.открыть дропдаун
            2. не торопясь посмотреть на варианты
            3. тут надо успеть за 100мс: нажать/зажать ctrl или shift или alt и сразу кликнуть мышкой

            также работает если зажать кнопку заранее, потом отпустить и быстро кликнуть, но намного менее надёжно


            1. tamtakoe
              03.09.2015 09:29

              У меня и моих коллег получалось в 100% случаев обычным щелчком. Специально хотели воспроизвести баг, но не могли. Только потом, когда понял в чем суть, удалось воспроизвести)


      1. makcums
        30.08.2015 13:52
        +1

        Долго не мог понять в чём дело, но всё же баг воспроизвёл. Посему вопрос: как можно было так криво сделать?


        1. tamtakoe
          30.08.2015 14:19

          Это появилось из-за хака для всех браузеров кроме Хрома. В этих браузерах нельзя понять чем было вызвано событие blur, переводом фокуса или программно, поэтому пришлось проверять через некоторый промежуток времени что инпут действительно потерял фокус и триггерить blur на элементе селекта. Причем из-за особенностей браузерной работы с событиями этот промежуток времени не маленький. С 10 мс не работало, с 50 мс через раз. github.com/tamtakoe/oi.select/blob/0.2.8/src/services.js#L99


          1. youlose
            30.08.2015 21:10
            +1

            А какая разница как вызывается blur? Зачем этот хак?


            1. tamtakoe
              30.08.2015 22:43

              Без него в FF, IE и проч. не будет работать переход по табу. Т.е. фокус сместится с одного инпута на другой, но компонент об этом никак не узнает, т. к. нет элемента в event.relativeTarget и он подумает, что blur был вызван программно и не станет вызывать его второй раз.


      1. Magister7
        30.08.2015 19:33
        +3

        А я, кажется, второй баг нашел — на странице «Funny» выбор с клавиатуры (нажатием Enter) не работает вообще никак.
        Решил что я, наверное, совсем плохой программист )))


        1. Magister7
          30.08.2015 19:37

          вот блин, внизу уже ответили. извиняюсь, недочитал комментарии…


  1. a553
    30.08.2015 08:05

    На странице Customization не скроллится список тачем, если при открытом списке тапнуть на строку. Win 10 + Firefox 41


    1. tamtakoe
      04.09.2015 14:45

      Это на телефоне?


      1. a553
        04.09.2015 15:40

        На десктопе.


        1. tamtakoe
          04.09.2015 16:50

          Странно. В последней версии тоже?


  1. artemmalko
    30.08.2015 10:33

    Пара багов:
    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) Еще не нашел оригинального селекта на странице. Он удаляется?


    1. tamtakoe
      30.08.2015 15:00

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

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


      1. artemmalko
        30.08.2015 15:53
        +1

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

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


        1. tamtakoe
          30.08.2015 22:40

          3) Странно. У меня можно писать и в Хроме и в ФФ. Что за система/браузер?

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

          Пробовал способ, когда в основе лежит стандартный селект, скрытый из виду, и в него копируются опции из oi-select. Нужно было перекомпилировать элемент, следить чтобы связь между областями видимости не порвалась при использовании, например, ng-if на директиве. Код был сложнее для восприятия, да и смысла не было, т. к. всё равно почти вся функциональность не была привязана к стандартному селекту.


          1. artemmalko
            31.08.2015 06:26

            tamtakoe.github.io/oi.select/#/select/#grouping клик на стрелку или открытие клавишами не дает вводить ничего. Mac Ось, любой браузер.


            1. tamtakoe
              31.08.2015 07:17

              А, на эту стрелку. Поправлю в следующей версии


  1. lega
    30.08.2015 14:01

    Почему не взяли select2?


    1. tamtakoe
      30.08.2015 14:37

      Во-первых, он на jQuery, который уже как пол года назад удалось выпилить из проекта. Из jQuery нужна была только функция определения высоты элемента (меньше 100 строчек). Тащить ради нее всю библиотеку как-то странно.
      Во-вторых, никакой совместимости по АПИ там и близко нет. Пришлось бы писать огромный адаптер или мучиться.
      В-третьих, слишком много отсебятины. Для множественного выбора поле ввода находится в строке с выбранными элементами, для одиночного — в списке, чтобы удалить тег нужно попасть по маленькому крестику, какие-то крестики для одиночного ввода… Не продуманный дизайн. Такое ощущение, что его писали разные люди, которые не смогли договориться.


    1. ZOXEXIVO
      30.08.2015 23:11

      А как насчет angular-ui-select?


      1. tamtakoe
        30.08.2015 23:36

        Практически то же самое. Год назад он был самым вменяемым, но, по сути, представлял обертку над select2, тащил jQuery и был глючным. Сейчас плагины подросли, но до уровня oi.select все еще не дотягивают. Пока ни один плагин не учитывает, что у Ангуляра есть готовое АПИ для селекта и предлагает свое, а это показатель уровня разработчиков.

        P. S. Понравился в свое время brianreavis.github.io/selectize.js, с него скопировал лучшие наработки в дизайне.


        1. Methos
          02.09.2015 10:14

          Кстати, на jquery мне нравится chosen


          1. tamtakoe
            03.09.2015 01:31

            По-моему, слишком много отсебятины. Какие-то крестики для очистки модели в одиночном инпуте. Для одиночного и множественного селекта поля поиска выглядят по-разному, список не закрывается по щелчку на пустом поле ввода (родной селект так себя не ведет). И как-то всё неаккуратно.


            1. Methos
              03.09.2015 14:02

              очистка в одиночном удобно, если есть несколько зависимых списков.

              например,

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

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

              одним кликом


              1. tamtakoe
                04.09.2015 14:44

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

                Там модель очищается при клике в любом месте. Не сторонник маленьких кнопочек и крестиков, по которым фиг попадешь, особенно на мобильном устройстве)


  1. Methos
    30.08.2015 14:12

    А есть так, чтобы было как это tamtakoe.github.io/oi.select/#/select/#editableoptions, но с возможностью добавлять свои таги, а не только те, которые есть в списке?


    1. tamtakoe
      30.08.2015 14:23
      +1

      См. prompt, autocomplete. Об особенностях описано в статье


      1. Methos
        31.08.2015 16:03

        1. tamtakoe
          31.08.2015 16:09
          +1

          Поправил


          1. Methos
            31.08.2015 16:26

            вероятно, здесь в конце нужно писать newItemFn?


            1. tamtakoe
              03.09.2015 09:31

              Поправил


          1. Methos
            31.08.2015 16:30

            какой-то странный баг — добавляет только ОДНО, первое значение. а дальше — ничего…

            и только если редактируешь и опять добавляешь, тогда начинает добавлять и дальше.


            1. Methos
              31.08.2015 16:35

              Это решено, у меня были дубли в id.


          1. Methos
            31.08.2015 16:39

            Как customize delete button?


            1. tamtakoe
              31.08.2015 17:37
              +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 можно переопределить на свой фильтр и делать там что угодно


          1. Methos
            31.08.2015 16:40

            Почему, когда фокус убирается с поля ввода, элемент добавляется? Возможно ли сделать так, чтобы добавлялся только по enter?


            1. tamtakoe
              31.08.2015 17:49
              +1

              Вообще, не должен. Разве что в случае с одиночным селектом. Он показывает выбранное ранее значение. Это сделано специально, т. к. так ведет себя обычный селект


          1. Methos
            31.08.2015 16:42

            Баг — если редактируешь значение, то если в это время удалить другой элемент, то пропадают оба элемента.


            1. tamtakoe
              31.08.2015 17:59
              +1

              Сейчас если стоит режим редактирования элементов, то удалить элемент как-бы нельзя. Т. е. из модели он удаляется, но на его месте остается его текст. Если вводить какой либо текст и удалить при этом любой элемент, то вводимый ранее текст заменится на текст удаленного элемента. Должно так работать


          1. Methos
            31.08.2015 17:20

            how to customized full own template.html?


            1. tamtakoe
              31.08.2015 18:01
              +1

              template.html очень не рекомендуется заменять на свой, т. к. это может нарушить работу элемента. Постарался сделать так, чтобы внешний вид можно было полностью настроить фильтрами и стилями.


              1. Methos
                31.08.2015 22:00

                спасибо, буду пытать


      1. Methos
        31.08.2015 16:07

        и ещё

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

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

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


  1. sajgak
    30.08.2015 15:36

    по поводу бага — выбор по enter тоже не работает))


    1. tamtakoe
      30.08.2015 15:59

      Я его специально запретил в том примере, чтобы не хитрили)


  1. serf
    30.08.2015 16:20
    +1

    Вот такое еще есть mbenford.github.io/ngTagsInput/demos


    1. tamtakoe
      30.08.2015 22:56

      Неплохой мультиселект. Некоторые примеры ведут себя не очевидным образом. У меня на первых порах тоже такое было. А вот валидацию текстового ввода возьму на заметку.


  1. Dzorogh
    31.08.2015 01:10

    Спасибо за классную реализацию сохранения новой модели (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
      31.08.2015 13:35
      +1

      Ответил случайно в корень, а не в эту тему. Запилил простенькую реализацию поиска по другим полям для плоских объектов tamtakoe.github.io/oi.select/#/select/#filtered


      1. Dzorogh
        01.09.2015 05:47

        Спасибо за ответ и пример.


  1. tamtakoe
    31.08.2015 04:09
    +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. Как минимум в одном большом проекте это решение уже используется — в моём, так что если какие-то серьезные баги всплывают, узнаю об этом очень быстро)


    1. Methos
      01.09.2015 12:16

      Можно ли сделать autocomplete по ajax?


      1. tamtakoe
        01.09.2015 23:22
        +1

        Запросто. Возвращай промис в функции, формирующей список и ищи в базе по строке tamtakoe.github.io/oi.select/#/select/#lazyloading


        1. Methos
          02.09.2015 10:13

          ok, thanks


        1. Methos
          03.09.2015 14:40

          А как сделать так, чтобы oi-options первоначально принял массив, а уже потооом был бы autocomplete?


          1. tamtakoe
            04.09.2015 14:48

            функция, возвращающая список вариантов по строке должна возвращать все варианты, если строка пустая… Если правильно понял что было нужно


    1. Methos
      03.09.2015 14:05

      как повесить на Enter обработчик события?


      1. tamtakoe
        04.09.2015 14:51

        Никак нельзя (конечно, можно обернуть все в свою директиву или найти элемент селекта в контроллере, а там найти инпут и повеситься на Enter, но это нехорошо).

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


        1. Methos
          10.09.2015 13:54

          Вы могли бы добавить в код возможность подписки на событие keyDown в поле ввода?


        1. Methos
          10.09.2015 15:00

          И ещё есть баг — если в пустое поле ввода сделать фокус, то вызывается функция autocomplete с пустым запросом. Думаю, нужно сделать так, чтобы не вызывалась.


        1. Methos
          14.09.2015 12:33

          К сожалению, пришлось отказаться от вашего плагина по причине отсутствия API событий и возможности расширения.
          Сделал на коленке за часик свой редактор тагов.