Сегодня хотим рассказать о том, как мы решили отдать долг open source сообществу и создали библиотеку right-angled. Только вчера мы перевели ее в статус beta и решили поделиться этой отличной новостью с Хабрасообществом c самым первым.
Что делает right-angled
В первую очередь, данная библиотека предназначена для построения гридов (aka списков, aka таблиц) в приложениях на angular 2.
Во вторую, это весьма продвинутая модель selection. Работающая не обязательно в связке с гридами. Это просто selection. Чего угодно.
Еще одной (пока не до конца оформившейся) идеей является декларативная настройка свойств компонентов для:
- сохранения
- восстановления
- сброса в значения по умолчанию
- отправки в параметрах запроса на сервер
без написания кода вручную.
Бесплатная?
Да, right-angled распространяется по лицензии MIT. Исходный код библиотеки доступен на github.
Также мы разместили на github-pages демо-приложение, детально описывающее возможности библиотеки с живыми демо и примерами кода. Если вам вдруг захочется посмотреть на исходники демо-приложения, они здесь.
Зачем мы сделали “еще одну библиотеку гридов”?
Когда мы выбирали для работы имеющиеся библиотеки гридов под angular 2, то пришли к выводу, что они слишком “тяжелые” и сложные. Например, шаблон простейшего грида с типичной библиотекой гридов для angular 2 выглядит примерно так:
<grid-component [dataSource]="data">
<grid-column-component fieldName="Id" title="Id">
</grid-column-component>
<grid-column-component fieldName="Name" title="Name">
</grid-column-component>
<grid-column-component field="Price" title="Price" width="230">
</grid-column-component>
<grid-column-component field="IsDiscounted" title="Is Discounted" width="120">
<row-template let-dataItem>
<input type="checkbox" [checked]="dataItem.IsDiscounted" />
</row-template>
</grid-column-component>
</grid-component>
В данном шаблоне слишком мало HTML и слишком много “библиотеки гридов”. Много компонентов, много настроек, слишком много того, что придется запомнить.
Эта сложность показалась нам излишней при современном подходе к разработке. И мы решили попробовать сделать что-то более легковесное и приятное.
Также, следствием первого недостатка является второй – верстка, которую генерируют подобные компоненты.
Грид – это достаточно сложный контрол. И HTML, генерируемый такими компонентами, не всегда выглядит хорошо будучи встроенным в конечное приложение. Не говоря уже о том, что этот HTML может быть просто откровенно плохим.
Обратной крайностью является универсальная разметка, которая учитывает все возможные варианты, но такая разметка не всегда быстро отрисовывается и всегда трудно стилизуется. Можно потратить многие часы на стилизацию, а грид все равно будет выглядеть в вашем дизайне как «не родной».
Простая стилизация, к слову, является одним из самых важных моментов, поскольку “клонированные” сайты в стиле bootstrap заказчиков уже давно не устраивают. И каждый новый проект – это нередко и новый, уникальный дизайн.
Осмыслив все выше сказанное, мы решили создать свою библиотеку, и заложить в нее следующие принципы:
1. Минимум компонентов
Библиотека должна содержать минимум компонентов и встраиваться в верстку конечного приложения, а не генерировать свою. То же самое касается стилей — right-angled не содержит какого-либо css и внешний вид вашего приложения остается целиком за вами.
Ниже приведен пример шаблона простейшего списка. Как вы можете заметить, это обычная верстка с использованием bootstrap (его использование совсем не обязательно, он взят просто для примера) и совсем немного кастомных директив.
<table class="table table-striped" [rtList]="getData" #list="rtList">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Price in USD</th>
<th>Is Discounted</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of list.items">
<td>{{item.Id}}</td>
<td>{{item.Name}}</td>
<td>{{item.Price}}</td>
<td><input type="checkbox" [checked]="item.IsDiscounted" /></td>
</tr>
</tbody>
</table>
Такой шаблон выглядит достаточно простым, поскольку мы не стали добавлять в нашу библиотеку такие понятия как «строка», «столбец», «шаблон просмотра», «шаблон редактирования» и прочие, столь привычные для библиотек гридов. Подобные абстракции (а вместе с ними и компоненты) часто добавляются в библиотеки гридов. Но, на наш взгляд, они избыточны и вносят ненужную сложность.
2. Простая функциональность
Чтобы “минимум компонентов” не превратился в “минимум функционала”, мы укомплектовали библиотеку набором функциональных сервисов. На них опираются компоненты самой библиотеки и их же пользователь может внедрить в свои компоненты при помощи Dependency Injection, чтобы реализовать нужное поведение самостоятельно.
Если же делать отдельные компоненты лениво, то доступ к этим сервисам можно получить прямо в шаблоне, обращаясь к директивам-хостам. Всего таких директив четыре — rtList с функционалом списков, rtSelectionArea с функционалом работы с selection и компоненты rt-buffered-pager и rt-pager-pager для работы с paging.
Например, вместо готового pager-а с кучей опций, мы обошлись примитивными компонентами-обертками и парой вспомогательных директив. И составили детальный пример, помогающий пользователю библиотеки реализовать собственный полнофункциональный pager.
Ниже вы можете увидеть код шаблона списка из финального примера в quick tour нашего приложения. В него добавлены:
- selection
- сортировки
- фильтры
- кнопки для загрузки данных/отмены запроса/сброса параметров
- упомянутый выше выше paging
- сериализация состояния списка в query string
В данном шаблоне мы как раз обращаемся к сервисам описанным выше «ленивым» способом — напрямую в шаблоне.
Шаблон получился пухлый, но ведь и функционала добавлено немало. И это по прежнему довольно-таки чистый, стилизуемый HTML, который вдобавок очень легко разбить на компактные и переиспользуемые компоненты (в демо-приложении этого не сделано для простоты восприятия примера).
<form>
<div class="row">
<div class="col-md-4 col-sm-6">
<div class="form-group">
<label>Airport name</label>
<input type="text" class="form-control" [(ngModel)]="airportName" name="airportName" />
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="form-group">
<label>Country</label>
<input type="text" class="form-control" [(ngModel)]="countryName" name="countryName" />
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="form-group">
<input (click)="list.loadData()" [disabled]="list.busy" type="submit" class="btn btn-load" title="Load data" />
<input (click)="list.cancelRequests()" [disabled]="list.ready" type="button" class="btn btn-cancel" title="Cancel loading"
/>
<button (click)="list.resetSettings()" [disabled]="list.busy" type="button" class="btn btn-reset" title="Reset settings"></button>
</div>
</div>
</div>
</form>
<div class="table-responsive">
<table class="table table-striped" [rtList]="getAirports" #list="rtList" [loadOnInit]="false" rtDemoSerializeToQueryString
(onListInit)="onListInit($event)">
<thead>
<tr>
<th><span rtSort="iataCode">IATA</span></th>
<th><span rtSort="name">Airport name</span></th>
<th><span rtSort="countryName">Country</span></th>
</tr>
</thead>
<tbody rtSelectionArea>
<tr *ngFor="let airport of list.items" [class.selected]="rts.selected" [rtSelectable]="airport" #rts="rtSelectable">
<td>{{airport.iataCode}}</td>
<td>{{airport.name}}</td>
<td>{{airport.countryName}}</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3">
<rt-demo-paged-footer>
</rt-demo-paged-footer>
</td>
</tr>
</tfoot>
</table>
</div>
Наверняка не все в этом шаблоне выглядит понятным. Но, чтобы сохранить статью компактной, мы не стали копировать сюда описание функционала из демо-приложения, поэтому все пояснения можно посмотреть в нем.
3. Минимум зависимостей
right-angled не зависит от таких библиотек, как bootstrap, jquery, jquery UI и т.п. Данные библиотеки, безусловно, хороши и полезны, но решение об их использовании лучше принимать конечному пользователю библиотеки. А при реализации гридов без них вполне можно обойтись.
Единственной зависимостью, помимо angular, является написанная нами же библиотека e2e4, которая и поставляет абстрактные от конкретных presentation фреймворков сервиса для реализации всего функционала.
e2e4, в свою очередь, вообще не имеет зависимостей. Но, если вы работаете в браузере, не поддерживающем es6, то вам понадобится какой-либо es6 полифил. Например, es6 shim или core js. Впрочем, shim и так нужен для работы angular.
Дальнейший план
Только вчера мы перевели библиотеку в статус беты, поэтому работы еще очень много. Ближайшие планы следующие:
- Стабилизировать библиотеку и вывести ее в релиз.
- Перевести демо-приложение на английский язык и составить полноценную документацию с дальнейшим выводом библиотеки в мировое сообщество.
- Доработать демо-приложение, вытащив наружу все возможности нашей библиотеки, поскольку пара фич пока еще спрятана.
Еще мы хотим обратиться к сообществу с просьбой
Если вы заметили ошибку или вам что-то показалось непонятным, пожалуйста, напишите нам об этом. Особенно это касается демо-приложения. Мы отлично понимаем, что написать хорошую, внятную документацию, это настоящее мастерство, которым мы пока не владеем в совершенстве.
Помимо создания issue на github, вы можете связаться с автором статьи и главным разработчиком библиотеки прямо здесь, на Хабре. Также вы можете подписаться в twitter на аккаунт right-angled, в котором мы будем публиковать новости о нашей библиотеке.
На этом прощаемся. Спасибо за внимание :)
Комментарии (6)
Ai_boy
20.11.2016 09:37+1Прорекламировал статью в Facebook группе и Telegram чате по Angular 2.
От себя могу предложить 1 feature request. Во время загрузки данных не удалять старые а показывать маску загрузки поверх них. Чтобы таблица не «прыгала»fshchudlo
20.11.2016 11:19Добрый день. За рекламу большое спасибо )
По поводу вашего предложения — я только что добавил новый сэмпл в демо-приложение вот сюда, позже допишу документацию, но уже после перевода на английский язык.
Суть сэмпла в том, что управление массивом элементов можно взять на себя. В сэмпле записи копируются в поле «myOwnItemsCollection» и в шаблоне теперь используется это поле, вместо обычного «list.items».
Теперь имеющиеся записи не уничтожаются, а перетираются новыми. Плюс на время запроса за данными добавлена маска, как вы и написали.
Вообще RTList никакой магии с коллекцией записей не делает. Он лишь опционально вызывает метод destroy элементов при уничтожении (про него мы вот тут написали). И еще он определяет, почистить или нет уже загруженные записи для случая когда список буферный. Эту логику очень легко воспроизвести в своем коде и реализовать нужное поведение по остальным вопросам. Мы решили выбрать такой подход, вместо добавления сомнительных настроек типа «уничтожать записи до/после загрузки»,
А текущее поведение сделали основным поскольку уничтожение имеющихся записей, пока идет загрузка новых, позволяет уменьшить «лаг» при отрисовке большого количества данных (особенно если логика уничтожения записей в методе destroy «тяжелая»), что тоже важно.
yar3333
Вроде бы добротно! Для тех, кому не критична легковесность, могу порекомендовать DataTable из PrimeNG. Поддерживается почти всё, что только можно себе представить.
sentyaev
Отличный компонент, но есть проблемы производительности на больших таблицах.
https://github.com/primefaces/primeng/issues/706