Как стать автором
Обновить

FormStamp − библиотека виджетов для AngularJS

Время на прочтение7 мин
Количество просмотров14K
AngularJS − это стремительно набирающий популярность JS-фреймворк, упрощающий разработку сложных и динамичных веб-приложений. Наша команда использует AngularJS в ряде проектов со сложным пользовательским интерфейсом, и в процессе работы мы остро ощутили нехватку хорошей библиотеки, предоставляющей набор единообразных виджетов, таких как datetime picker, select, multiple select и так далее. Конечно, нам было известно о проекте Angular UI, но некоторых виджетов, которые нам были нужны, AngularUI не предоставлял.

Кроме того, мы хотели иметь аналог рельсового form builder-а, но на фронтенде. Form builder позволяет программисту описать форму декларативно, беря на себя генерацию разметки и вывод ошибок.

Решением этих проблем стала разработанная нами библиотека FormStamp, которая предоставляет:
  • Form Builder − наивысший уровень для работы с формами, созданный по аналогии с генераторами форм из экосистемы Ruby on Rails;
  • набор виджетов, покрывающих 80% задач, встречающихся при работе с формами и не решаемых стандартными элементами HTML5;
  • низкоуровневые компоненты, позволяющие собирать новые виджеты.

При разработке в библиотеку были заложены следующие принципы:
  • все виджеты написаны с нуля с использованием директив AngularJS, что позволяет сократить код и сделать его более читаемым;
  • полная интеграция с AngularJS (поддержка ngModel, ngRequired...);
  • стилизация по умолчанию с помощью Bootstrap.


Инструкция по установке

FormStamp может быть подключен в ваш проект с помощью пакетной системы Bower:
bower install angular-formstamp

Form Builder

Выразительный декларативный подход AngularJS снижает количество кода, который нужно написать для создания UI. Однако даже с использованием этого подхода при создании простой формы с проверками заполненности полей и отображением сообщений об ошибках приходится писать много повторяющегося кода:
Код
<form class="form-horizontal" role="form" name="form" ng-app="form-demo">

    <div class="form-group" ng-class="{'has-error': form.username.$invalid}">
        <label for="username" class="col-sm-2 control-label">Username</label>
        <div class="col-sm-10">
            <input type="text"
                   class="form-control"
                   id="username"
                   placeholder="Username"
                   required="required"
                   ng-pattern="/awesome/"
                   name="username"
                   ng-model="username" />
            <p class="alert alert-danger" ng-show='form.username.$error.pattern'>
                Username should be awesome
            </p>
        </div>
    </div>

    <div class="form-group" ng-class="{'has-error': form.email.$invalid}">
        <label for="email" class="col-sm-2 control-label">Email</label>
        <div class="col-sm-10">
            <input type="email"
                   class="form-control"
                   id="email"
                   placeholder="Email"
                   required="required"
                   name="email"
                   ng-model="email" />
            <p class="alert alert-danger" ng-show='form.email.$error.email'>
                Email should be valid
            </p>
        </div>
    </div>

    <div class="form-group" ng-class="{'has-error': form.password.$invalid}">
        <label for="password" class="col-sm-2 control-label">Password</label>
        <div class="col-sm-10">
            <input type="password"
                   class="form-control"
                   id="password"
                   placeholder="Password"
                   required="required"
                   name="password"
                   ng-model="password"
                   ng-minlength='6' />
            <p class="alert alert-danger" ng-show='form.password.$error.minlength'>
                Password should be longer
            </p>
        </div>
    </div>

    <div class="form-group">
        <label for="birthDate" class="col-sm-2 control-label">Birth Date</label>
        <div class="col-sm-10">
            <input type="date"
                   class="form-control"
                   id="birthDate"
                   placeholder="Birth Date"
                   ng-model="birthDate" />
        </div>
    </div>
    <div class="form-group">
        <div class="col-sm-offset-2 col-sm-10">
            <button type="submit" class="btn btn-default">Sign up</button>
        </div>
    </div>
</form>

Эту проблему решает компонент Form Builder − для создания формы достаточно указать:
  • модель, с которой связана форма;
  • атрибуты модели, которые отображаются в форме;
  • типы элементов формы, соответствующие каждому из отображаемых в форме атрибутов.

С помощью Form Builder указанную выше форму с подсветкой ошибок можно создать намного меньшим количеством кода:
<fs-form-for model="samurai">
  <fieldset class="form-horizontal">
    <fs-input as="text" name="username" required="" label="Name"></fs-input>
    <fs-input as="email" name="email" required="" label="Email"></fs-input>
    <fs-input as="password" name="password" required="" label="Email"></fs-input>
    <fs-input as="fs-date" name="birthdate" required="" label="Date of Birth"></fs-input>
  </fieldset>
</fs-form-for>

Пояснения:
  • fsFormFor − директива, создающая форму, атрибут model указывает на модель, для которой создается форма;
  • fsInput − директива, описывающая каждый элемент в форме со следующими атрибутами:
    • as − тип элемента формы;
    • name − имя атрибута модели;
    • label − текст метки.

Все остальные атрибуты делегируются элементу формы, указанному в атрибуте as.

Набор виджетов

Чем сложнее ваше приложение, тем меньше вам будет хватать стандартных элементов форм и тем скорее вам понадобятся дополнительные виджеты. На данный момент существует не так много виджетов, рассчитанных на интеграцию с AngularJS, а из тех, что есть, часть является оберткой над jQuery-виджетами. Библиотека FormStamp содержит написанные с нуля с использованием API AngularJS виджеты, решающие те задачи, с которыми мы сталкивались чаще всего в нашей работе:
  • select с возможностью фильтрации по введенному значению;
  • select с поддержкой free text (combo box) ;
  • multiselect с возможностью фильтрации по введенному значению;
  • multiselect с поддержкой free text (tags input);
  • radio group;
  • checkbox group;
  • виджеты для работы с датой и/или временем и календарь.

Рассмотрим работу с select виджетом, для создания которого используется директива fsSelect. Директива поддерживает атрибуты freetext, items, ng-model, ng-required, ng-disabled.

freetext
Атрибут (по умолчанию false) определяет поведение виджета. При freetext=false виджет ведет себя как select, то есть позволяет выбрать один элемент из списка вариантов. При freetext=true виджет ведет себя как combo box, то есть позволяет выбрать значение из списка вариантов или ввести любое другое.

items
Атрибут указывает, какое свойство скоупа содержит список вариантов, отображаемых в виджете. При freetext=false варианты могут быть как объектами, так и примитивными типами. При freetext=true варианты могут быть только строками.

ng-model
Атрибут является стандартной директивой ngModel.

ng-disabled
Атрибут указывает, какое свойство скоупа определяет, будет ли виджет disabled/enabled.

Для создания combo box, варианты которого содержатся в $scope.arrayOfOptions, выбранный вариант связан со $scope.selectedOption, а состояние disabled/enabled зависит от $scope.flag, запишем директиву следующим образом:
<div fs-select items=”arrayOfOptions”
               ng-disabled=”flag”
               ng-model=”selectedOption”
               freetext=”true”></div>


Примеры работы с остальными виджетами и Form Builder размещены на странице библиотеки.

Директивы

Для того чтобы облегчить написание дополнительных виджетов, мы начали выделять части функциональности в низкоуровневые директивы:
  • fsList − отображает список элементов, позволяет выделять элемент в списке и перемещать выделение с клавиатуры;
  • fsNullForm − скрывает элемент формы, связанный с ngModel, от родительской формы;
  • fsInput − упрощает обработку событий клавиатуры и смены фокуса;
  • fsCalendar − отображает календарь и позволяет помечать дату как выбранную.

Для примера создадим плей-лист для плеера, используя fsList и fsInput. Работа с fsList происходит с помощью взаимодействия с listInterface свойством на $scope. listInterface имеет следующие свойства:
  • selectedItem − текущее выбранное значение. Только на чтение.
  • onSelect(value) − обработчик события выбора значения. Должен быть реализован пользователем.
  • move(d) − функция, которая перемещает указатель на указанное количество элементов.

Создадим директиву, которая будет оборачивать в себя audio тег из html5:
  app.directive("demoAudio", function() {
    return {
      restrict: "E",
      scope: {
        track: '='
      },
      template: "<audio controls />",
      replace: true,
      link: function($scope, $element, $attrs) {
        return $scope.$watch('track', function(track) {
          $element.attr('src', track.stream_url + "?client_id=8399f2e0577e0acb4eee4d65d6c6cce6");
          return $element.get(0).play();
        });
      }
    };
  });


Подключим SoundCloud SDK
<script src="http://connect.soundcloud.com/sdk.js"></script>

Далее создадим контроллер для связывания этих элементов:
function ListDemoCtrl($scope) {
  // Инициализация SoundCloud SDK
  SC.initialize({
    client_id: '8399f2e0577e0acb4eee4d65d6c6cce6'
  });

  // Реализация поиска по SoundCloud
  $scope.$watch('search', function () {
    SC.get('/tracks',
           { q: $scope.search, license: 'cc-by-sa' },
           function(tracks) {
             $scope.$apply(function() { $scope.tracks = tracks })
           })
  });

  $scope.search = 'bach';
  $scope.tracks = [];

  // Оборачиваем функцию для перемещения выбранного значения в fsList
  $scope.move = function (d) {
    $scope.listInterface.move(d);
  };

  // Добавляем обработчик выбранного значения в fsList
  $scope.listInterface = {
    onSelect: function (selectedItem) {
      $scope.select(selectedItem)
    }
  };

  $scope.select = function(selectedItem) {
    $scope.selectedTrack = selectedItem || $scope.listInterface.selectedItem;
  };
}


И само приложение:
<div ng-controller="ListDemoCtrl" style="postion: relative;">
  <div class="row">
    <div class="col-xs-7">
      <!-- Инициализируем fsInput и добавляем обработчики клавиатуры  -->
      <input class="form-control" autofocus="1" fs-input
             fs-up="move(-1)" fs-down="move(1)" fs-enter="select()"
             ng-model="search">
      <!-- Инициализируем fsList и передаем ему список треков -->
      <div fs-list="" items="tracks" class="no-popup">
        <!-- Описываем внутренний темплейт переданных треков -->
        <img src="{{ item.artwork_url }}" width="30" height="30">
        {{item.title}} <small class="text-muted">{{item.genre}}</small>
      </div>
    </div>
    <div class="col-xs-5">
      <!-- Связываем выбранное значение в fsList с аудио-плеером -->
      <demo-audio track="selectedTrack"></demo-audio>
      <pre style="margin-top: 20px;">Selected Item: {{ selectedTrack | json }}</pre>
    </div>
  </div>
</div>


В результате получаем вот такой плеер:

Живой пример вы можете посмотреть здесь.

В следующей статье мы более подробно рассмотрим создание формы с использованием FormStamp.

Демо библиотеки

Код библиотеки
Теги:
Хабы:
Всего голосов 20: ↑18 и ↓2+16
Комментарии12

Публикации

Истории

Работа

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань