Простое должно быть простым: Палки в разметку

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

Как то так получилось, что несмотря на некоторый интерес к «фронтовой» теме, основные мои задачи долгое время были только в плоскости брутального windows-десктопа. Но, тем не менее изредка приходилось что-то делать с помощью html/css/React, и каждый раз удивляться извилистым путям, ведущим к решению простейших задач.

Html/css и его вариации, предлагают могучий инструментарий для воплощения самых смелых фантазий дизайнеров. И, наверное, при регулярном и глубоком погружении в тему верстки, весь этот инструментарий «прокэширован в подкорке» и «вертится на кончиках пальцев»…

А как быть тем, кто заходит в сияющий мир CSS лишь изредка — по необходимости?

Либо нужно реализовать что-то небольшое по объему работ и усилия по изучению тонкостей просто не окупятся?

Более того, даже на восьмидесятом этаже наслоений абстракций под названием React, блуждая по кабинетам React библиотекам «MUI», «AntD» и т.д. как то не обнаруживается ожидаемая интуитивная простота в решении всех простых задач верстки!

В попытке ответить на этот вопрос, попробуем с помощью различных подходов решить задачу по разметке, схематически изображенной на этом рисунке:

Эталонная разметка (Chrome 113)

Эталонная разметка (Chrome 113)

Так как речь о приложении, а не о неком html документе, то очевидно, что разметка должна занимать ровно 100% ширины и высоты области просмотра браузера.

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

Эпизод первый, разминочный: HTML

Насколько мне известно, подобные разметки модно делать с помощью flex блоков. Так и поступим в первой попытке:

<div style="display: flex; flex-direction: column; height: 100%;">   <div style="background-color: lightblue;">Header</div>    <div style="display: flex; flex-grow: 1;"> <div style="background-color: bisque; flex-basis: content;">Left panel</div>  <div style="overflow-y: auto; flex-grow: 1;">       Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>       Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>Content<br/> </div>  <div style="background-color: bisque; flex-basis: content;">Right panel</div>   </div>      <div style="background-color: lightblue;">Footer</div> </div>

Пройдемся по коду:

  • Разметку начинает flex блок(1), ориентирующий контент в виде вертикальной колонки и занимающий 100% высоты браузера. Колонку составляет Header(2), блок(4) с неким контентом требующем прокрутку и Footer(15)

  • Flex блок(4) в свою очередь занимает все свободное пространство в родительском блоке (flex‑grow: 1) и ориентирует контент в виде строки. В эту строку входят снова три блока: левая(5) и правая(12) панели и собственно контент со скроллом;

  • Left panel(5) и Right panel(12) нужно пометить flex-basis: content, чтобы они были не меньше контента.

Вроде все складно и должно заработать как надо? Мечты, все мечты…

Это не то, чем кажется!

Это не то, чем кажется!

Что здесь не так?

У блока с контентом(7) задано overflow-y: auto, что вроде бы должно создать область прокрутки для этого блока и ограничить его же видимый размер. А теперь еще раз смотрим на скриншот — выглядит так, будто overflow-y: auto применен к корневому блоку в первой строке.

Я когда увидел это чудо, уверовал в теорию заговора, будто разработчики html решили объявить войну очевидным и как следствие простым решениям. Только хардкор.

Благо, на связи был матерый «фронтовик», он посоветовал решение костыль:

<div style="display: flex; flex-direction: column; height: 100%;">   <div style="background-color: lightblue;">Header</div>    <div style="display: flex; flex-grow: 1;"> <div style="background-color: bisque; flex-basis: content;">Left panel</div>  <div style="overflow-y: auto; flex-grow: 1;">       <div style="height: 0">         Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>         Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>       </div> </div>  <div style="background-color: bisque; flex-basis: content;">Right panel</div>   </div>      <div style="background-color: lightblue;">Footer</div> </div>

Смотрим на строки 8 и 11…

Еще раз смотрим…

Нет, не костыль. Магия! Зато работает. Скриншот в начале как раз снят с этого кода 🙂

Итого: нас убеждают, что:

  • явно заданная нулевая высота блока не должна сделать его невидимым;

  • overflow-y как старое ружье — стреляет куда попало, а для точной привязки к нужному блоку, нужно примотать его скотчем обернуть контент блоком с нулевой высотой;

Про то, что ширина и высота в html живут по разным правилам, даже и не заикаюсь: заметили, что высоту 100% для flex контейнера(1) нужно было указывать, а ширину — нет? Откуда здесь ноги растут понятно (ориентация на прокрутку высоченных страниц и т.д.), но так ли нужно было ломать законы логики?

Вообще тема с прокруткой, весьма широка.
  • Проблема с overflow-y не решена и в tailwindcss с тегом overflow-y-auto;

  • На многих совершенно разных сайтах, заголовок вместе с меню и прочим функционалом, находится в области прокрутки и движется вместе с контентом. В результате, чтобы перейти на другую страницу, пользователю приходится скроллить к началу. Но говорят, это «модно», так же как и сопутствующая кнопка «в начало». Юзабилити? Нет, не слышали;

  • Также популярно решение, когда заголовок, хотя и находится визуально в области действия скроллбара, на самом деле не управляется им #рукалицо;

  • Про «дикие танцы» баннеров, живущих своей странноскролльной жизнью во время прокрутки сайтов, просто нет сил уже…

Помимо вышеописанных «шероховатостей», о которых нужно помнить (а вы о них забудете, если подобные вещи не делать каждый день), код выше несколько «рыхловат», много букв на «квадратный блок».

Эпизод второй, позитивный: CSS

Но что это за придирки к «старичку» html? Попробуем вынуть палки из html, создав вспомогательный файл layout.css:

html, body {    margin: 0; }  [col], [row] { display: flex; flex-basis: content;  }  [col] { flex-direction: column; height: 100%; }  [row] { flex-direction: row; }  [expand] {   flex-grow: 1; }  [scroll] { overflow-y: auto; background: white; }  [scroll] > * { height: 0; }  [bar] {     background-color: lightblue; }  [panel] {     background-color: bisque; }  [action] { background-color: aqua; }

Здесь мы определили несколько аттрибутов, каждый из которых будет задавать свой аспект поведения блоков в разметки:

  • col — блок размещающий контент в виде вертикальной колонки;

  • row — блок размещающий контент в виде горизонтальной строки;

  • expand — расширение дочернего блока у row или col до максимально возможного;

  • scroll — добавление области прокрутки (включая необходимую магию(25), задающую свойства дочернего блока);

  • bar, panel, action — просто примеры неких прочих пользовательских аттрибутов стилизующих блоки.

Диалог за кадром

— «Такой способ использования аттрибутов не является стандартным. Также кастомные аттрибуты поддерживаются не во всех браузерах! А вдруг IE дремучей версии случится?!»;

— «Ну да, 100501-й способ усложнить жизнь разработчику… А что предлагаешь?»;

— «Классы, конечно!»

Теперь пишем новый html, не забывая подключать наш css:

<link rel="stylesheet" href="layout.css" />  <div col>   <div bar>Header</div>    <div row expand> <div panel>Left panel</div>  <div scroll expand>   <div>         Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>         Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>     </div> </div>  <div panel>Right panel</div>   </div>    <div bar>Footer</div> </div>

Да, это рабочий html, генерирующий картинку как на первом скриншоте. Код короче, и вся магия ушла в css, помнить о ней не нужно (почти).

Проясним использование атрибутов:

  • Корневой блок(3) помечаем атрибутом col, ибо он колонка;

  • Дочерний блок(4) помечаем bar — просто оформление цветом;

  • Дочерний блок(6) — контейнер, помечаем row, чтобы он был горизонтальным и expand, чтобы занял все свободное место между панелями Left panel и Right panel;

  • Блок с контентом(9) помечаем как scroll и он становится областью прокрутки, а добавление expand, позволяет занять все доступное пространство;

  • и т.д…

Единственное, что не удалось решить в рамках css, это требование наличия блока вокруг контента scroll. Просто текст не получится. Но это не беда, далее все будет!

И хотя поставленная задача решена, простота полученного кода так и призывает «навернуть» что-нибудь еще:

<link rel="stylesheet" href="layout.css" />  <div col>   <div bar>Header</div>    <div row expand> <div col panel>Left panel</div>  <div scroll expand>   <div>         Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>         Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>     </div> </div>  <div panel col>       <div>         Right panel       </div>        <div scroll expand>         <div>           Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>           Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>         </div>       </div>              <div>         Right footer       </div> </div>   </div>      <div bar row>     <div expand>       Footer     </div>      <div action>       [Icon]     </div>      <div action>       [Action]     </div>   </div> </div>

Здесь мы доработали следующее:

  • Right panel(16) добавлением атрибута col, легко превращается в вертикальный контейнер, также добавляем область прокрутки(21) с контентом по образцу в строке 9. Обратите внимание: эти два блока с контентом в вертикальном и горизонтальном контейнере настраиваются абсолютно одинаково;

  • Footer(34) также просто становится горизонтальным контейнером с помощью атрибута row. На десерт, в этом блоке пробуем максимально наглядный способ размещение блоков в контейнере: если некий блок(35) с помощью expand занимает все доступное пространство, остальные(39, 43) — сиротливо теснятся в стороне (справа в данном случае). Куда еще «материальнее» ?

Вот окончательный рендер:

Усложненный пример

Усложненный пример

Понятно, что такой «дизайн» нужен далеко не всем. И вообще, творить визуальную нетленку, дело утонченных дизайнеров, а не суровых разработчиков. Хотя, местами, они жертвуют юзабилити какому-то своему божеству. Но, имея простой инструмент, логику которого не нужно вспоминать (привет блоку с нулевой высотой), будет гораздо легче воспринимать вашего дизайнера.

Эпизод третий, фееричный: MUI и Ant design

Разумеется, на чистом html + css мало кто сейчас работает. Поэтому посмотрим, что предлагают в обсуждаемой области две популярнейших библиотеки MUI и Ant design:

Что ожидаешь, обращаясь к популярным продуктам с «именем»?

Ну вероятно, что они то умеют в разметку. Сделают все удобно и лаконично. Продумают все варианты. Это были мечты. Далее суровая реальность.

Компоненты разметки MUI:

  • Container — просто центрирование по горизонтали. Это тег, не атрибут. Странно, что нет тегов Right и Left;

  • Grid — похоже мимо. Больше подходит для контента с массивом элементов;

  • Stack — горизонтальный\вертикальный контейнер, это что-то близкое к задаче. Функционал компонента Stack ограничен только выстраиванием дочерних блоков в столбик или линию. Все. Внезапно. Занавес.

Компоненты разметки AntD:

  • Space — более продвинуты аналог Container из MUI;

  • Layout — глядя на примеры, кажется, что это 100% попадание. Кажется. Механизм заполнения блоком клиентской области не продуман, т.е. без инжектирования css повторить разметку подобную целевой, не получится;

  • Grid — более продвинутый (вероятно) аналога Grid в MUI, тоже мимо;

Другими словами повторить целевую разметку без использования css не получается!

Где аналог expand на основе flex-grow или вроде того?

И\или выравнивание элементов контейнеров лево-центр-право?

До последнего момента не ожидал такого фиаско.

Итого:

Готовой функциональности по разметке для приложений, сложнее «пролайкать котиков» нам не дают, оставляя лазейку в виде низкоуровневого html + css.

Где-то это уже было? Может в html + css?

А для чего тогда эти библиотеки? Ах, да — контролы и документо-ориентированная разметка…

#совсем-рука-лицо

Я очень хочу заблуждаться, в надежде, что уважаемое комьюнити ткнет меня, как слепого котенка, в нужный компонент из состава MUI или Ant Design.

В противном случае, базовая вещь — разметка, остается не простой. И дело не в особой сложности flex контейнеров, а в том, что css содержит множество фич, объединение которых в одной задаче, может быть весьма нетривиальным делом. Поэтому наличие простой согласованной высокоуровневой компонентной обертки над html+css считаю залогом качественной React библиотеки. Такой библиотеки, которая не будет заставлять меня гадать, какие из фич css конфликтуют друг с другом вместо предоставления набора «рычагов» каждый из которых будет работать ожидаемо вне зависимости от других.

Эпизод последний: Box, just Box

В начале разработки UI в проекте GeekLoad, сразу была заложена возможность смены библиотеки компонентов (вдруг не подойдет). А так как теги разметки, самые частые в UI, то хотелось избежать массовой переделки всего и вся в этом случае.

Это, а также неудовлетворительная проработка компонентов-контейнеров в рассматриваемых библиотеках побудили к реализации очередного «велосипеда» — React компонента <Box/>.

Главное требование к компоненту было: не вставлять палки в колеса разметку, а конструировать ее понятным и предсказуемым образом.

Посмотрим, как данный компонент, реализует принцип «Простое должно быть простым…» и как можно реализовать различные кейсы в разметке с его помощью.

Для начала — аналог компонента MUI:Container:

<Box class='bar'>Content</Box>
Использование классов здесь - только для подсветки блоков, а для разметки совсем не обязательно!

Использование классов здесь — только для подсветки блоков, а для разметки совсем не обязательно!

Добавлением атрибута expand — занимаем всю клиентскую область браузера:

<Box expand class='bar'>Content</Box>
Контент в центре видимой области браузера

Контент в центре видимой области браузера

Чтобы получить прямой аналог AntD:Space, нужно просто воспользоваться свойствами hAlign и vAlign, не думая о том какая ориентация контейнера использована: горизонтальная или вертикальная (по умолчанию — горизонтальная, как в примере выше):

<Box hAlign = {Align.End}      vAlign = {Align.End}      class = 'bar' >Content</Box>

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

Пример позиционирования дочерних элементов. Да, их может быть несколько.

Пример позиционирования дочерних элементов. Да, их может быть несколько.

Теперь кратко поясним логику компонента Box:

  • Aтрибут col распределяет контент и растягивает сам блок по вертикали, если его нет, то контент распределяется и сам блок растягивается по горизонтали;

  • Body html документа «за кадром» ведет себя как <Box col> дополнительно растянутый по горизонтали;

  • Атрибут expand растягивает Box по направлению его контейнера. Если таких блоков более одного, они делят доступное пространство поровну;

  • Атрибут scroll автоматически применяет col блоку и превращает его в область прокрутки, без дополнительных требований к контенту;

  • Атрибут gap включает отступы между блоками контента, может быть значением отступа в единицах ‘rem’. По умолчанию = 1;

  • Для выравнивания контента служат атрибуты hAlign и vAlign:

    • Align.Start — выравнивание по верхнему или левому краю;

    • Align.Center (по умолчанию) — выравнивание по центру;

    • Align.End — выравнивание по нижнему или правому краю;

    • Align.Fill — растягивание блоков отличных от Box перпендикулярно направлению блока.

  • Атрибут class — задает необходимые css классы блоку.

И затем, реализуем «целевую разметку»:

<Box expand>   <Box col expand>     <Box class = 'bar'>Header</Box>      <Box expand>       <Box col class = 'panel'>Left panel</Box>        <Box scroll expand>         Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>         Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>       </Box>        <Box col class = 'panel'>Right panel</Box>     </Box>      <Box class = 'bar'>Footer</Box>   </Box> </Box>

В этом примере следующая логика:

  • Корневой Box(1) служит для растягивания используемого пространства по горизонтали. Это понадобилось сделать, чтобы не нарушать логику компонента в остальных случаях.

  • Box(2) col, распределяет блоки Header(3), Content(5) и Footer(13) вертикально, а expand захватывает пространство по направлению контейнера, т.е. горизонтально;

  • Box(3) — горизонтальный блок «Header»;

  • Box(5), распределяет блоки Left panel(6), Content(8) и Right panel(13) горизонтально

  • Left panel(6) и Right panel(13) помечены атрибутом col, чтобы быть вертикально растянутыми;

Отличие от целевой разметки - только в выравнивании по умолчанию по центру.

Отличие от целевой разметки — только в выравнивании по умолчанию по центру.

И, на последок, усложняем немного задачу:

<Box expand>   <Box col expand>     <Box gap = {2} class = 'bar'>       <Box>Header</Box>       <Box expand = {2} class = 'action'>[Menu]</Box>       <Box expand class = 'action'>[Login]</Box>     </Box>      <Box expand>       <Box col class = 'panel'>Left panel</Box>        <Box scroll expand>         Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>         Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>       </Box>        <Box col>           <Box class = 'panel'>Right header</Box>            <Box scroll expand>             Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>             Content<br/>Content<br/>Content<br/>Content<br/>Content<br/>           </Box>            <Box class = 'panel'>Right footer</Box>       </Box>     </Box>      <Box gap class = 'bar'>         <Box expand class = 'bar'>Footer</Box>          <Box class = 'action'>[Icon]</Box>         <Box class = 'action'>[Action]</Box>     </Box>   </Box> </Box>

Надеюсь, этот пример после предыдущих уже понятен. Если не особо — пишите в комментариях — поправлю статью.

Дизайнеры за такое убьют. Но мы им не покажем ;)

Дизайнеры за такое убьют. Но мы им не покажем 😉

Ниже, под катом, исходники упрощенной версии описываемого компонента. Разумеется какие-то решения покажутся спорными, но, уверен, кому то идея зайдет, как минимум в качестве прототипа.

Исходники <Box/>

Box.css:

#root, html, body {     margin: 0;     width: 100%;     height: 100%;     overflow: hidden; }  #root {     display: flex;     flex-direction: column;     align-items: center; }  .box {     flex-basis: content;     display: flex; }  .row {     flex-direction: row;     width: 100%; }  .col {     flex-direction: column;     height: 100%; }  .scroll {     overflow-y: auto;     overflow-x: hidden;     width: 100%;     height: 100%; }  .bar {     background-color: lightblue; }  .panel {     background-color: bisque; }  .action {     background-color: aqua; }

Box.tsx:

import {ReactNode} from 'react'; import './Box.css'  export enum Align {     Start = 'start',     End = 'end',     Center = 'center',     Fill = 'stretch' }  export default (props: {     class?: string     scroll?: boolean     hAlign?: Align     vAlign?: Align     col?: boolean     expand?: boolean | number     children?: ReactNode     gap?: boolean | number }) => {     const hAlign = props.scroll ? Align.Fill : props.hAlign || Align.Center     const vAlign = props.scroll ? Align.Fill : props.vAlign || Align.Center     const gap = (props.gap || false) ? (((typeof props.gap) == 'boolean' ? 1 : props.gap) + 'rem') : undefined     const expand = Number((props.expand || false) ? (((typeof props.expand) == 'boolean' ? 1 : props.expand)) : undefined)      return <div className = {         ('box ' +             (props.col ? 'col ' : 'row ') +             (props.scroll ? 'scroll ' : '') +             (props.class ? props.class : '')         ).trim()     }                 style = {{                     flexGrow: expand,                     justifyContent: props.col ? vAlign : hAlign,                     alignItems: props.col ? hAlign : vAlign,                      rowGap: props.col ? gap : undefined,                     columnGap: props.col ? undefined : gap                 }}     >         {props.scroll             ? <div style = {{height: 0}}>{props.children}</div>             : props.children}     </div> }  export function AppScreen(props: { children: ReactNode }) {     return <div style = {{height: '100%'}}>{props.children}</div> }

— «А в чем была суть вообще?»

На самом деле, различных усложнений в html + css разметке можно насобирать еще порядочно, но хотелось взять какой то кейс и довести его до решения, которое показалось бы достаточным.

Так как считаю разметку в большинстве случаев простой задачей, ожидаю от инструментов для ее реализации — простоты. И в ситуациях, когда Box перестает «дружить» с каким то вариантом разметки, он дорабатывается так, чтобы и предыдущие и новые кейсы лаконично поддерживались.

Здесь и кроется ответ на вопрос в начале статьи: Если в некой предметной области вы не частый гость, то хорошим подходом будет консолидация «сложности&магии» в неких точках. Box — как раз один из таких. Вместо того, чтобы по всему коду приложения раскидывать некие решения задач разметки, лучше собрать их в одном месте, а далее везде использовать краткие и понятные атрибуты. Даже если вы забудете суть найденных решений, логичный фасад вокруг них, избавит вас от мучительных попыток «вспомнить все».

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Стоит ли продолжить цикл статей в подобном ключе, по разным темам?
33.33% Да 2
33.33% Нет 2
33.33% Сначала писать научись 2
Проголосовали 6 пользователей. Воздержались 5 пользователей.

ссылка на оригинал статьи https://habr.com/ru/articles/730342/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *