Русский
Русский
English
Статистика
Реклама

Dialog

Паттерн порталов в Angular для чего нужен root-компонент в Taiga UI

26.01.2021 14:17:08 | Автор: admin

Мой коллега Роман недавно объявил о выходе нашей новой библиотеки компонентов под Angular Taiga UI. В инструкциях Getting started сказано, что приложение нужно обернуть в некий tui-root. Давайте разберемся, что он делает, узнаем, как и зачем мы используем порталы и что это вообще такое.

Что такое портал?

Представьте себе компонент select. У него есть выпадашка с вариантами на выбор. Если хранить ее в том же месте в DOM, что и сам компонент, можно нарваться на ряд проблем. Нижестоящие элементы могут выскочить поверх, а контейнер срезать содержимое:

Проблемы глубины обычно решаются через z-index, что запускает Войну Миров Z в вашем приложении. Часто можно встретить значения 100, 10000, 10001. Но даже если суметь это грамотно разрулить, от overflow: hidden все равно не убежишь. Что же делать?

Вместо размещения выпадашки рядом с хостом мы поместим ее в специальный контейнер поверх всего приложения. Тогда остальные элементы будут находиться в своем изолированном контексте и проблемы с z-index отпадут. Этот контейнер и есть портал. Root-компонент в Taiga UI нужен как раз для создания подобных порталов. Рассмотрим его шаблон:

<tui-scroll-controls></tui-scroll-controls><tui-portal-host>    <div class="content"><ng-content></ng-content></div>    <tui-dialog-host></tui-dialog-host>    <ng-content select="tuiOverDialogs"></ng-content>    <tui-notifications-host></tui-notifications-host>    <ng-content select="tuiOverNotifications"></ng-content></tui-portal-host><ng-content select="tuiOverPortals"></ng-content><tui-hints-host></tui-hints-host><ng-content select="tuiOverHints"></ng-content>

Общие и специализированные порталы

И tui-dialog-host, и tui-portal-host по сути своей порталы. Но работают они по-разному. Для начала взглянем на второй. В Taiga UI он используется для показа выпадашек. Но это портал общего назначения. Он управляется очень простым сервисом:

@Injectable({   providedIn: 'root',})export class TuiPortalService {   private host: TuiPortalHostComponent;   add<C>(componentFactory: ComponentFactory<C>, injector: Injector): ComponentRef<C> {       return this.host.addComponentChild(componentFactory, injector);   }   remove<C>({hostView}: ComponentRef<C>) {       hostView.destroy();   }   addTemplate<C>(templateRef: TemplateRef<C>, context?: C): EmbeddedViewRef<C> {       return this.host.addTemplateChild(templateRef, context);   }   removeTemplate<C>(viewRef: EmbeddedViewRef<C>) {       viewRef.destroy();   }}

Сам компонент тоже незамысловат. Всё, что он делает, выводит шаблоны и динамические компоненты поверх приложения. Это значит, что позиционирование, закрытие и вся иная логика лежит на плечах самих выводимых элементов.

Такой универсальный портал полезен, если потребуется что-то особенное. Например, кнопка Наверх, всегда видимая над контентом.

Выпадашки

Создавая решение для выпадающих элементов, нужно задуматься над позиционированием. Тут у нас несколько вариантов:

  1. Позиционировать единожды и блокировать скролл. Так работает Material по умолчанию.

  2. Позиционировать единожды и закрывать, если произошел скролл. Так ведут себя нативные компоненты.

  3. Следить за положением хоста.

Мы решили пойти по третьему пути. Оказалось, он не так прост. Невозможно полностью синхронизировать продолжение двух элементов, даже через requestAnimationFrame. Поскольку запрос положения хоста вызывает рефлоу к концу кадра, когда выпадашка поставлена в новое место хост еще немного изменит свою позицию. Это вызывает видимые скачки даже на быстрых машинах. Мы обошли это, использовав абсолютное позиционирование вместо фиксированного. Поскольку контейнер портала оборачивает все приложение, во время прокрутки положение выпадашки не меняется. Если хост сам находится в фиксированном контейнере, это не сработает. Но мы можем заметить это в момент открытия и позиционировать выпадашку также фиксировано.

Ну и есть еще это:

Если хост покидает видимую зону нужно закрыть выпадашку. Этим занимается сервис Obscured. Он следит за хостом и закрывает выпадающий элемент при полном перекрытии.

Диалоги

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

Вот так выглядит компонент с диалогами:

<section   *ngFor="let item of dialogs$ | async"   polymorpheus-outlet   tuiFocusTrap   tuiOverscroll="all"   class="dialog"   role="dialog"   aria-modal="true"   [attr.aria-labelledby]="item.id"   [content]="item.component"   [context]="item"   [@tuiParentAnimation]></section><div class="overlay"></div>

Как видите, в нем есть цикл ngFor, перебирающий конкретные элементы. Это позволяет заложить определенную логику, типа захвата фокуса или блокировки скролла. Также тут используется хитрое внедрение зависимостей, позволяющее диалогам не зависеть от модели данных и конкретного внешнего вида. Хост собирает потоки с диалогами с помощью специального мультитокена, объединяет их в один и выводит результат.

Таким образом, в одном приложении могут уживаться несколько видов диалогов. В Taiga UI заложено два дизайна всплывающих окон базовый и имитирующий нативные алерты на мобильных устройствах. Последний выглядит по-разному на iOS и Android. Вы можете легко добавить свою реализацию. Давайте посмотрим, как это сделать.

Сервис показа модальных окон возвращает Observable. При подписке на него окно выводится, при отписке закрывается. В этот поток диалог также может передавать данные. Для начала создадим свой компонент диалога. Всё, что тут важно, он может брать из DI-токена POLYMORPHEUS_CONTEXT. В этом объекте будет поле content с содержимым модалки и observer конкретного инстанса диалога. Его можно завершить через complete, что закроет диалог, и можно вернуть данные с помощью метода next. Кроме того, в этом объекте будут содержаться все параметры, которые вы пожелаете добавить к вашей реализации диалогов. Сервис для их показа наследуется от абстрактного класса:

const DIALOG = new PolymorpheusComponent(MyDialogComponent);const DEFAULT_OPTIONS: MyDialogOptions = {   label: '',   size: 's',};@Injectable({   providedIn: 'root',})export class MyDialogService extends AbstractTuiDialogService<MyDialogOptions> {   protected readonly component = DIALOG;   protected readonly defaultOptions = DEFAULT_OPTIONS;}

Вам нужно лишь задать опции по умолчанию и ваш компонент.

Диалоги, как и все в Taiga UI, используют ng-polymorpheus для кастомизации контента. Узнать больше о том, как создавать гибкие, не зависящие от модели данных компоненты, вы можете из этой статьи.

Захват фокуса реализован директивой tuiFocusTrap. Поскольку фокус может попадать в выпадашки, стоящие позже по DOM, и мы можем иметь несколько открытых диалогов, ловушки игнорируют переход фокуса на нижестоящие элементы. Если же фокус перемещается куда-то выше по дереву мы возвращаем его назад:

@HostListener('window:focusin.silent', ['$event.target'])onFocusIn(node: Node) {   if (containsOrAfter(this.elementRef.nativeElement, node)) {       return;   }   const focusable = getClosestKeyboardFocusable(       this.elementRef.nativeElement,       false,       this.elementRef.nativeElement,   );   if (focusable) {       focusable.focus();   }}

Для блокировки скролла используется комбинация из директивы и небольшой логики внутри root-компонента. От root требуется скрыть скроллбар при открытии диалога, в то время как директива tuiOverscroll берет на себя скролл с помощью тача или колеса мыши. Существует CSS-правило overscroll-behavior. Однако его недостаточно. Оно не поможет, если диалог слишком маленький, чтобы не иметь внутреннего скролла. Поэтому мы создали специальную директиву с дополнительной логикой, блокирующей скролл, если он будет происходить в родительских контейнерах.

Бонус: что еще делает tui-root?

Мы обсудили все, что касается порталов. Давайте посмотрим, что еще заложено в root-компонент. Вы видели в шаблоне tui-scroll-controls. Это кастомный скроллбар, контролирующий глобальную прокрутку окна. Также вы могли заметить именованную проекцию контента типа <ng-content select="tuiOverDialogs"></ng-content>. С помощью этих слотов вы можете подсунуть свое содержимое между слоями Taiga UI. Например, если вы используете другие решения для нотификаций и хотите правильно поместить их по глубине приложения.

Еще root регистрирует несколько event manager plugin`ов в DI. Подробнее об этом в отдельной статье. Важно, чтобы TuiRootModule шел после BrowserModule, чтобы плагины были зарегистрированы в правильном порядке. Но не волнуйтесь: если вы ошибетесь увидите сообщение в консоли.

Это все, что я хотел рассказать о порталах и root-компоненте. Taiga UI уже в open-source, и вы можете взглянуть на нее на Гитхабе и утянуть с npm. Вы можете погулять по демопорталу с документацией или поиграться вживую с этим StackBlitz-стартером. Не теряйтесь, в будущем мы обязательно расскажем больше про интересные фичи, которые у нас есть!

Подробнее..

Как мы победили попапы в мессенджере Gem4Me

14.07.2020 10:11:05 | Автор: admin


Привет, Хабр!

Сегодня я, лид Web-разработки мессенджера Gem4Me, и мое альтер эго АйТи Синяк хотим поделиться своей историей борьбы с модальными окнами.

В каждом проекте, где я успел поработать, модальные окна были устроены максимально примитивно. Везде примерно одна и та же картина: один глобальный менеджер попапов на весь проект. И реализован он с помощью switch-case блока, который отвечает за то, какой именно сейчас попап отобразить. На мой взгляд, это никуда не годится.


Выглядит это обычно примерно следующим образом:


const { type, data } = this.props;switch (type) {  case ModalsConstants.type.AlertModal:    return <Alert key={type} data={data}/>;  case ModalsConstants.type.GroupMembersModal:    return <GroupMembers key={type} data={data}/>;  case ModalsConstants.type.ContactsModal:    return <ContactsView key={type} data={data}/>;  case ModalsConstants.type.UserSettingsModal:    return <UserSettingsView key={type} data={data}/>;  case ModalsConstants.type.UserProfileModal:    return <UserProfileModal key={type} data={data}/>;  ...  default:    return null;}

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

Недостатки глобального менеджера попапов


Их очень много, я выделил несколько основных.


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


Первая и основная, на мой взгляд, проблема это взаимодействие с навигацией в браузере. Я готов поспорить, если вы откроете попап на вашем текущем проекте и нажмете стрелку назад в браузере попап даже не закроется, а лишь изменится страница, которая под ним находится.


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


Шаринг ссылок на модальные окна


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


Эта штука неплохо работает в web-версии Твиттера. Вы входите и видите смешную картинку, она как раз открывается в попапе. Если вы скопируете URL-адрес и поделитесь им, то получатель увидит и у кого вы взяли картинку, и в рамках какого твита, и саму картинку все восстановится, как было, и это действительно удобно.


Обновление страницы


Третья ситуация аналогична второй. У всех проблемы с восстановлением попапа после перезагрузки страницы. Это решаемая проблема, мы можем сохранить стек попапов в LocalStorage или SessionStorage. Но кому нужны дополнительные заботы, если есть решения без этих хлопот?


Коммуникация с попапом


Четвертая сложность это общение между попапом, который лежит внутри попап-менеджера, и непосредственной страницей. К примеру, вы решили изменить банковскую карту, привязанную к аккаунту мобильного приложения. При нажатии отправки формы появляется диалог, который просит вас ввести пароль от аккаунта, и только после ввода пароля вся форма отправляется на сервер, где включены все данные как введенные на странице, так и пароль из попала.


Я такие попапы начал называть контекстными, поскольку они не имеют смысла существования без определенного контекста, в нашем случае страницы ввода банковских данных. А это значит, что попапу ввода пароля нужно общаться со страницей, но как сделать это удобно, если страница хранится в одном месте, а модальное окно где-то в глобальном менеджере попапов? В таком случае нужно передать либо какой-то callback в ваш стор, или хранить какие-то флаги, отображающие текущую информацию, в том же сторе. Опять сложности.


Отображение одновременно двух попапов


Switch-case конструкция позволяет отобразить лишь один попап одновременно. Но что, если вам нужно в первом попапе совершить какое-то действие, которое требует подтверждения вроде "Вы уверены, что хотите удалить пользователя из группы?". Такого рода подтверждения чаще всего также реализуются через попап, похожий на системный confirm-диалог. В итоге, если вы используете switch-case конструкцию, когда хотите удалить пользователя из группы, у вас "размонтируется" первый попап и будет виден только диалог с подтверждением, что и визуально выглядит крайне странно, и технически напряжно, ведь после согласия вам надо восстановить предыдущее состояние первого попапа. Снова приходится изворачиваться.


Конечно же, это не все сложности...


Есть и другие проблемы. К примеру, если использовать в качестве поддержки анимации закрытия попапа switch-case конструкцию, она мгновенно "вымаунтит" попап при изменении флага, поэтому надо что-то доизобретать, чтобы сначала произошла анимация закрытия, а уже после диалог "размаунтился". Или а что, если вам надо, чтобы в URL-адресе попап никак не отображался, а стрелка назад в браузере могла с ним взаимодействовать?


Обо всем этом мы сегодня и поговорим.


Короткий спойлер


Мое альтер-эго создало короткую видео-выжимку с решениями, которые будут описаны в этой статье: "Управление попапами с помощью React-router", "Как управлять двумя и более попапами одновременно с помощью react-router". Сама статья покрывает более широкий спектр проблем, но если вам проще с видео заходите, не стесняйтесь. А также я подготовил демку и GitHub, чтобы вы могли сами все потрогать и понять, как это работает на самом деле.


Gem4Me: начало


На проекте Gem4Me мы начинали борьбу примерно с той же исходной точки, а именно со switch-case конструкции. Партией было принято решение для начала переписать попап-галерею медиа-файлов.


Что есть галерея


Как и у любого другого мессенджера, у нас есть чат с нашими сообщеньками (открывается по URL /chats/:id). В этих сообщеньках, кроме текстовых, часто мелькают разные медиа-файлы: мемасики, гифки, видео и др. Для быстрого поиска мемасика мы можем открыть в отдельном попапе только медиа-файлы, сгрупированные по дате и динамически подгружаемые по скролу




Этот попап для нас является контекстным, т.к. он не имеет смысла без экрана чата. Поэтому мы решили использовать роутер для отображения этого попапа, а именно следующий URL /chats/:id/gallery. Таким образом, попап будет отображаться только если перед ним находится /chats/:id, а это гарантирует наличие открытого чата.


Имплементация галереии


У нас есть глобальный роутер, в котором есть чат:


<Route path="/chats/:id">  <Chat /></Route>

Далее внутри компонента чата мы можем вставить непосредственно сам попап:


...const Chat = ({ ... }) => {  return (    <>      ...      <Route path="/chat/:id/gallery">        <OverlayPopup isOpened onClose={onClose}>          <Gallery />        </OverlayPopup>      </Route>    </>  )}

Это и значит контекстный попап когда сам попап является частью той страницы, которой он принадлежит. React-router уже начиная с версии 4 позволяет делать вложенные роуты, что для нас очень удобно. Таким образом, когда срабатывает /chats/25565465, открывается определенный чат, а чтобы открыть попап, нам нужно лишь создать ссылку:


<Link to={`/chat/${id}/gallery`}>  Shared Media</Link>

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


Особенности переменной match в react-router


Хочу обратить внимание на интересный момент с переменной match. Она отвечает за совпадение атрибута path ближайшего родителя Route. К примеру, у нас текущий url-адрес /chats/25565465/gallery. Если мы сделаем console.log(match.url) именно внутри компонента чата, то результат будет /chats/25565465, т.к. path ближайшего родительского Route равняется path="/chats/:id". С другой стороны, если вы добавите лог console.log(match.url) внутри галереи при том же url-адресе, результат будет /chats/25565465/gallery, т.к. ближайший path является path="/chats/:id/gallery".


Данное свойство очень интересно и его можно использовать для построения относительных путей. К примеру, для генерации ссылки:


<Link to={`${match.url}/gallery`}>  Shared Media</Link>

Или для создания вложенных роутов, что улучшит переиспользуемость вложенного роута:


...const Chat = ({ ... }) => {  return (    <>      ...      <Route path={`${match.url}/gallery`}>        <OverlayPopup isOpened onClose={onClose}>          <Gallery />        </OverlayPopup>      </Route>    </>  )}

У такого подхода есть как преимущества, так и недостатки. Из недостатков теперь /:id/ больше не приходит как параметр, т.к. там вписана конкретная цифра. С другой стороны, у нас есть не только чаты, но и каналы, соответственно мы можем использовать при таком подходе этот компонент как для /chats/2544346/gallery, так и для /channels/3245353/gallery. Что также является для нас преимуществом, т.к. в данный момент чат и канал используют один и тот же компонент из-за своей схожести.


Но вообще react-router команда обещала в 6 версии библиотеки предоставить более удобные вложенные роуты. Что, возможно, поможет устранить все недостатки. А пока радуемся тому, что уже можно использовать.


Закрытие попапа


Мы научились открывать попап с помощью перехода по ссылке, но как его закрыть? Первая мысль использовать функцию history.goBack(), которая просто вернет Вас на предыдущий URL-адрес и попап закроется, среагировав на изменения в URL. Но у данного подхода есть неприятный кейс. Если вы скопировали ссылку на попап и открыли в новой вкладке или сбросили товарищу, то вы не сможете закрыть попап, оставшись на сайте, так как history.goBack привязывается именно к истории таба браузера, т.е. вместо закрытия попапа по нажатию на крестик вы вернетесь на предыдущий экран, к примеру на домашнюю страницу вашего браузера.


Это не значит, что нет вариантов, как использовать history.goBack для закрытия попапа. АйТи Синяк создал целое отдельное видео "Как закрыть попап используя history.goBack" посвященное разным стратегиям при закрытии попапа.


Стратегия, которую используем мы, самая простая для реализации. Обертываем крестик ссылкой:


<Link to={match.url.replace('/gallery', '')}>  <CrossIcon /></Link>

Да, это, конечно, упрощенная реализация, но идея максимально приближена к боевой. В текущей переменной match содержится /chats/23445345/gallery. Все, что нам нужно удалить gallery в конце URL-адреса, и такая реализация позволяет не заботиться о том, что сейчас открыто у пользователя чат или канал.


Промежуточные итоги галереи


Давайте подытожим, что у нас получилось:


  • Попап галереи открывается и закрывается в зависимости от URL-адреса, следовательно, стрелка назад в браузере, а также на андроид-смартфоне, действительно будет закрывать попап;
  • Решены проблемы с перезагрузкой страницы и шаринг ссылки, т.к. все восстановится по URL-адресу, без каких-либо дополнительных инструментов;
  • Из-за того, что попап находится непосредственно внутри компонента чата, мы можем в виде привычных нам props передавать абсолютно любые данные и колбеки, что избавляет от изобретения велосипедов;
  • Также мы обезопасили себя от ситуации, в которой компонент Chat по каким-либо причинам размонтирован, а следовательно, об отображении попапа галереи не может быть и речи;
  • В случае удаления чата или канала мы с компонентом сразу же удалим и попап, который лежит внутри папки компонента. Ведь часто бывает, что страницу удалили, а попап, принадлежащий этой странице, так и остался в менеджере.

Сон 2-го уровня


Итак, мы решили первую задачку, но тут же прилетела вторая. По нажатию на любую картинку в галерее должна открыться галерея с просмотром изображения на весь экран. Выглядит это примерно так:





URL, которому соответствует галерея /chats/:id/gallery/:fileId. А это значит, что у нас опять полное взаимодействие с браузерными контролами, и все так же восстановится после перезагрузки страницы.


Еще один интересный нюанс: несмотря на то, что галерея не прозрачна, под ней все еще находятся сам чат и попап галереи. Это удобно для нас, т.к. нам не нужно восстанавливать состояние того же попапа галереи. Только представьте ситуацию: вы долго скролили вниз, картинки подгружались, и тут вы случайно вы нажали на ненужную картинку, она открылась на весь экран, а после ее закрытия весь ваш прогресс исчез. В такой момент хочется перестать пользоваться таким мессенджером. Но эта история не про нас! :)


По вложенности, как вы уже догадались, попап изображения на весь экран находится прямо внутри попапа галереи. Это гарантирует их существование только в дуэте.


Глобальные попапы


С контекстными попапами, принадлежащими конкретным страницам или компонентам, идея, думаю, стала понятной. Но есть попапы, которые могут быть показаны на любой странице. Мы их называем глобальными. Ярким примером для многих проектов может служить попап Login. В нашем случае это, к примеру, попап личных настроек, вот так он выглядит:




В итоге мы не можем использовать pathname в URL-адресе, т.к. страница может быть любой. Поэтому мы решили использовать GET-параметры. К примеру, URL-адрес при открытии попапа выглядит следующим образом: /chats/34454356?popup=user-settings.


Как вы уже поняли, основная схема заключается в том, что GET-параметр popup будет принимать значение, какой именно попап сейчас отобразить. В данном случае это user-settings.


Имплементация Менеджера Попапов


Идея такого менеджера попапов очень похожа на классическую, только нужно следить за GET-параметром, а не за значением в вашем Redux сторе или где-то еще. Поэтому для начала напишем custom hook для извлечения значения из GET параметров по имени:


/* global URLSearchParams */import { useLocation } from 'react-router-dom';export default (name) => {    const { search } = useLocation();    const query = new URLSearchParams(search);    return query.get(name);};

Для этого используем custom hook от react-router useLocation. В итоге мы можем получить search, который выглядит примерно следующим образом: ?popup=user-settings. Чтобы не парсить руками строку, используем браузерное API URLSearchParams.


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


const popups = {    'user-settings': UserSettingsPopup,    'create-group' CreateGroup,    'bots-catalog', BotsCatalog,    ...} const PopupManager = () => {    const popupName = useGetParameter('popup');    const Component = popups[popupName];    if (!Component) {        return null;    }    return <Component />;};

Реализация ничем особо не отличается от обычного switch. Но более читабельна, на мой взгляд.


Анимация закрытия


И этот менеджер на самом деле можно еще улучшать. К примеру, нам нужно поддерживать анимацию закрытия попапа. Тогда стоит просто немного доработать:


const PopupManager = () => {    const { popupName, isOpened } = useGetPopupState();    const Component = popups[popupName];    if (!Component) {        return null;    }    return <Component isOpened={isOpened} />;};

Все, что нам нужно из одного параметра, отвечающего за отображение попапов, сделать два. Поэтому создадим свой hook, который будет возвращать 2 параметра: isOpened будет отвечать за то, отображается ли попап пользователю, а popupName будет отвечать, пора ли "размонтировать" попап. Т.е. по факту мы должны сохранить значение попапа в useState и изменять его с задержкой на время анимации закрытия попапа.


Реализацию этого хука с setTimeout уже оставлю на самостоятельные эксперименты, если совсем уж тяжко можете подсмотреть тут.


2 попапа одновременно


Можно возвращать и 2 попапа, у нас есть такой случай: в каталоге ботов, который на весь экран, можно открыть еще один попап с конкретным ботом. Немного поколдуем над менеджером:


const PopupManager = () => {  const { mountedPopups, popups } = useGetPopupsState();  return mountedPopups.map((mountedPopup) => {    const Component = mappedPopups[mountedPopup];    if (!Component) {      return null;    }    return (      <Component key={mountedPopup} isOpened={popups.includes(mountedPopup)} />    );  });};

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


Заключение


Конечно, это не все трюки с попапами. Есть еще попапы, которые хотелось бы контролировать с помощью роутера, но не хотелось бы, чтобы на них можно было сбросить ссылку товарищу, но при этом неплохо было бы, чтобы они восстанавливались при обновлении страницы. Да, это возможно, и react-router с этой задачей прекрасно справился! Есть и другие интересные эксперименты, если интересно могу написать еще одну статью о том, какие еще странные случае приходится обрабатывать.


А сейчас обращаюсь к тебе, читатель. Присоединяйся к борьбе! Прикрути хороший качественный попап на сайте, не пожалей на это свое время, уговори менеджера, что это действительно важно! Пользователи скажут тебе спасибо. Нам уже сказали ;-)

Подробнее..
Категории: Javascript , Reactjs , React-router , Popup , Modal dialog , Modal , Dialog

Опуститься до уровня руководителя?

26.12.2020 14:17:21 | Автор: admin

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

Красота в наших глазах. Но руководитель часто далек от технических деталей. Часто очень сложно объяснить всю программистскую кухню на языке диаграмм Ганта. Когда штудирование документации и вычитывание кода библиотеки выливается в метрику +10 строк кода. (За половину месяца.) Ну ведь правда, как-то не солидно 0.125 строк кода в день. (Это сколько символов в день? А в час?) Правда?


Мы правда должны опускаться до уровня руководителей?

И дело даже не в разных решаемых задачах. О том, что и мы программисты, и менеджеры, и маркетологи решают одну задачу создание успешного продукта/услуги. Каждый на своём месте движемся к реализации общей задачи. В роли лидеров или работников, как эксперты, или как испытатели, исследователи, тестировщики, технические писатели и т.д.

Но все же, что же с диалогом?

Если задаться вопросом: а нужно ли пасовать?

А может ли быть изящным то, чтобы понять какие преимущества получает пользователь при использовании этих десяти строк кода? Если попробовать продумать преимущества уменьшения загрузки серверов в версии кода этих 10-ти строк. Сложно назвать сумму прибыли? Возможно. Но если время ответа сервера уменьшиться на некоторое число миллисекунд. На полмесяца взгляд будет иной. Правда?


А если о диалоге?

А может быть изящным обсудить будущее приложения и то, какие улучшения возможны? Обсудить, где выиграет бизнес? Многогранная красота...

Подробнее..

Важность диалога между PM-ом и разработчиком

28.12.2020 10:22:36 | Автор: admin

Рассматривается кейс разработки, определяются некоторые проблемы, формулируется необходимость диалога.

Кейс разработки.
Разработчик завис над простой задачей. Занимался задачей две недели. По результатам двух недель работы внес в репозиторий изменений на 10-20 строк кода. В отчете по задаче множество технических деталей. В отчете выставил необходимость дополнительного времени на доработку задачи.

Если для вас кейс очевиден, то можно дальше и не читать.

Список проблем.

Перерасход бюджета. Разработчик потратил уйму времени на простую фичу и требует дополнительное время.

Разработчик не способен аргументировать необходимость больших временных затрат на простую фичу.

Возможность конфликта с разработчиком.


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

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

Возможность дальнейших прецедентов построения замков со стороны разработчика. Разработчик, возможно, будет игнорировать указания руководителя и самостоятельно выделять время на улучшение качества системы. За счёт основных фич, т.е. с уменьшением затрат на основную функциональность, которая приносит прибыль.

Разработчик не умеет перевести технические детали предполагаемого решения на язык преимуществ для пользователя.

Разработчик не может сформулировать почему низкое качество системы ведёт к ухудшению бизнес показателей продукта.

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

Разработчик не знаком со стратегией развития продукта. Поэтому предлагает решения, которые не нужны на текущем этапе развития продукта.


И так далее. (Прописываю только часть проблем.)

Решения.


Решения для каждой из проблем не предлагаю, потому что знаю, что у читателя достаточно опыта для понимания ситуации и определения подходов и стратегий реагирования, и достаточно профессионализма для поиска возможных решений.

Спасибо.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru