212 lines
12 KiB
Markdown
212 lines
12 KiB
Markdown
# Вёрстка графических компонентов
|
||
|
||
При создании графических компонент ("виджетов") в первую очередь придумывается их HTML/CSS-структура.
|
||
|
||
Как будет выглядеть виджет в обычном состоянии? Как будет меняться в процессе взаимодействия с посетителем?
|
||
|
||
Чтобы разработка виджета была удобной, при вёрстке полезно соблюдать несколько простых, но очень важных соглашений.
|
||
|
||
[cut]
|
||
|
||
## Семантическая вёрстка
|
||
|
||
HTML-разметка и названия CSS-классов должны отражать не оформление, а смысл.
|
||
|
||
Например, сообщение об ошибке можно сверстать так:
|
||
|
||
```html
|
||
<div *!*style="color:red; border: 1px solid red"*/!*>
|
||
Плохая вёрстка сообщения об ошибке: атрибут style!
|
||
</div>
|
||
```
|
||
|
||
...Или так:
|
||
|
||
```html
|
||
<div *!*class="red red-border"*/!*>
|
||
Плохая вёрстка сообщения об ошибке: несемантический class!
|
||
</div>
|
||
```
|
||
|
||
В обоих случаях вёрстка не является семантической. В первом случае -- стиль, а во втором -- класс содержат информацию об *оформлении*.
|
||
|
||
**При семантической вёрстке классы описывают смысл ("что это?" -- меню, кнопка...) и состояние (открыто, закрыто, отключено...) компонента.**
|
||
|
||
Например:
|
||
|
||
```html
|
||
<div *!*class="error"*/!*>
|
||
Сообщение об ошибке (error), правильная вёрстка!
|
||
</div>
|
||
```
|
||
|
||
У предупреждения будет класс `message` и так далее, по смыслу.
|
||
|
||
```html
|
||
<div *!*class="warning"*/!*>
|
||
Предупреждение (warning), правильная вёрстка!
|
||
</div>
|
||
```
|
||
|
||
Семантическая верстка упрощает поддержку и развитие CSS, упрощает взаимодействие между членами команды.
|
||
|
||
Такая верстка удобна для организации JS-кода. В коде мы просто ставим нужный класс, остальное делает CSS.
|
||
|
||
## Состояние виджета -- класс на элементе
|
||
|
||
Зачастую компонент может иметь несколько состояний. Например, меню может быть открыто или закрыто.
|
||
|
||
**Состояние должно добавляться CSS-классом не на тот элемент, который нужно скрыть/показать/..., а на тот, к которому оно "по смыслу" относится, обычно -- на корневой элемент.**
|
||
|
||
Например, меню в закрытом состоянии скрывает свой список элементов. Класс `open` нужно добавлять не к списку опций `<ul>`, который скрывается-показывается, а к *корневому элементу* виджета, поскольку это состояние касается всего меню:
|
||
|
||
```html
|
||
<div class="menu *!*open*/!*">
|
||
<span class="title">Заголовок меню</span>
|
||
<ul>
|
||
<li>Список элементов</li>
|
||
</ul>
|
||
</div>
|
||
```
|
||
|
||
Или, к примеру, разметка для индикатора загрузки может выглядеть так:
|
||
|
||
```html
|
||
<div class="indicator *!*loading*/!*">
|
||
<span class="progress">Тут показывается прогресс</span>
|
||
</div>
|
||
```
|
||
|
||
Состояние индикатора может быть "в процессе" (loading) или "загрузка завершена" (complete). С точки зрения оформления оно может влиять только на показ внутреннего `span`, но ставить его нужно всё равно на внешний элемент, ведь это -- состояние всего компонента.
|
||
|
||
Из примеров выше можно подумать, что классы, описывающие состояние, всегда ставятся на корневой элемент. Но это не так.
|
||
|
||
Возможно и такое, что состояние относится к внутреннему элементу. Например, для дерева состояние открыт/закрыт относится к узлу, соответственно, класс должен быть на узле.
|
||
|
||
Например:
|
||
|
||
```html
|
||
<ul class="tree">
|
||
<li class="*!*closed*/!*">
|
||
Закрытый узел дерева
|
||
</li>
|
||
<li class="*!*open*/!*">
|
||
Открытый узел дерева
|
||
</li>
|
||
...
|
||
</ul>
|
||
```
|
||
|
||
## Префиксы компонента у классов
|
||
|
||
Рассмотрим пример вёрстки "диалогового окна":
|
||
|
||
```html
|
||
<!--+ autorun height=100 -->
|
||
<div class="dialog">
|
||
<h2 class="title">Заголовок</h2>
|
||
<div class="content">
|
||
HTML-содержимое.
|
||
</div>
|
||
<div class="close">Закрыть</div>
|
||
</div>
|
||
|
||
<style>
|
||
.dialog {
|
||
background: lightgreen;
|
||
border: lime 2px solid;
|
||
border-radius: 10px;
|
||
padding: 4px;
|
||
position: relative;
|
||
}
|
||
|
||
*!*
|
||
.dialog .title {
|
||
margin: 0;
|
||
font-size: 24px;
|
||
color: darkgreen;
|
||
}
|
||
*/!*
|
||
|
||
.dialog .content {
|
||
padding: 10px 0 0 0;
|
||
}
|
||
|
||
.dialog .close {
|
||
position: absolute;
|
||
right: 4px;
|
||
top: 4px;
|
||
font-size: 10px;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
Диалоговое окно может иметь любое HTML-содержимое.
|
||
|
||
А что будет, если в этом содержимом окажется меню -- да-да, то самое, которое рассмотрели выше, со `<span class="title">` ?
|
||
|
||
Правило `.dialog .title` применяется ко всем `.title` внутри `.dialog`, а значит -- и к нашему меню тоже. Будет конфликт стилей с непредсказуемыми последствиями.
|
||
|
||
Конечно, можно попытаться бороться с этим. Например, жёстко задать вложенность -- использовать класс `.dialog > .title`. Это сработает в данном конкретном примере, но как быть в тех местах, где между `.dialog` и `.title` есть другие элементы? Длинные цепочки вида `.dialog > ... > .title` страшновато выглядят и делают вёрстку ужасно негибкой. К счастью, есть альтернативный путь.
|
||
|
||
**Чтобы избежать возможных проблем, все классы внутри виджета начинают с его имени.**
|
||
|
||
Здесь имя `dialog`, так что все, относящиеся к диалогу, будем начинать с `dialog__`
|
||
|
||
Получится так:
|
||
|
||
```html
|
||
<div class="*!*dialog*/!*">
|
||
<h2 class="dialog__title">Заголовок</h2>
|
||
<div class="dialog__content">
|
||
HTML-содержимое.
|
||
</div>
|
||
<div class="dialog__close">Закрыть</div>
|
||
</div>
|
||
|
||
<style>
|
||
.dialog { ... }
|
||
.dialog__title { стиль заголовка }
|
||
.dialog__content { стиль содержимого }
|
||
...
|
||
</style>
|
||
```
|
||
|
||
Здесь двойное подчёркивание `__` служит "стандартным" разделителем. Можно выбрать и другой разделитель, но при этом стоит иметь в виду, что иногда имя класса может состоять из нескольких слов. Например `title-picture`. С двойным подчёркиванием: `dialog__title-picture`, очень наглядно видно где что.
|
||
|
||
Есть ещё одно полезное правило, которое заключается в том, что стили должны вешаться на класс, а не на тег. То есть, не `h2 { ... }`, а `.dialog__title { ... }`, где `.dialog__title` -- класс на соответствующем заголовке.
|
||
|
||
Это позволяет и избежать конфликтов на вложенных `h2`, и использовать всегда те теги, которые имеют правильный смысл, не оглядываясь на встроенные стили (которые можно обнулить своими).
|
||
|
||
|
||
[smart header="Без фанатизма"]
|
||
На практике из этих правил зачастую делают исключения. Можно "вешать" стили на теги и использовать CSS-каскады без префиксов, если мы при этом твёрдо понимаем, что конфликты заведомо исключены.
|
||
|
||
Например, когда мы точно знаем, что никакого произвольного HTML внутри элемента (или внутри данного поддерева DOM) не будет.
|
||
[/smart]
|
||
|
||
## БЭМ
|
||
|
||
Описанное выше правило именования элементов является частью более общей концепции "БЭМ", которая разработана в Яндексе.
|
||
|
||
БЭМ предлагает способ организации HTML/CSS/JS в виде независимых "блоков" -- компонент, которые можно легко перемещать по файловой системе и между проектами.
|
||
|
||
Можно как взять часть идеологии, например систему именования классов, так и полностью перейти на инструментарий БЭМ, который даёт инструменты сборки для HTML/JS/CSS, описанных по БЭМ-методу.
|
||
|
||
Более подробное описание основ БЭМ можно почитать в статье [](https://ru.bem.info/articles/bem-for-small-projects/), а о системе вообще -- на сайте [](http://ru.bem.info).
|
||
|
||
## Итого
|
||
|
||
<ul>
|
||
<li>Вёрстка должна быть семантической, использовать соответствующие смыслу информации теги и классы.</li>
|
||
<li>Класс, описывающий состояние всего компонента, нужно ставить на его корневом элементе, а не на том, который нужно "украсить" в этом состоянии. Если состояние относится не ко всему компоненту, а к его части -- то на соответствующем "по смыслу" DOM-узле.</li>
|
||
<li>Классы внутри компонента должны начинаться с префикса -- имени компонента.
|
||
|
||
Это не всегда строго необходимо, но позволяет избежать проблем в случаях, когда компонент может содержать произвольный DOM, как например диалоговое окно с произвольным HTML-текстом.
|
||
|
||
Использование `.dialog__title` вместо `.dialog .title` гарантирует, что CSS не применится по ошибке к какому-нибудь другому `.title` внутри диалога.
|
||
</li>
|
||
</ul>
|
||
|
||
|
||
|