Popup + View + Page: как организовать структуру

В статье я расскажу как правильно готовить Popup (а также Panel, Sheet и Popover) с View и Page. Не все моменты описаны в документации и мне приходилось много тестировать, чтобы понять, как оно работает на самом деле. Надеюсь, мой опыт вам пригодится. Мы будем рассматривать Popup именно как компонент роутера.

Самая простая структура шаблона имеет вид:

<div class=”popup”>
	<div class=”page”>
		…
	</div>
</div>

Мы можем подписаться на события Popup, например:

on : {
   popupOpen() {
      …
   }
}

Также можно подписаться на события компонента, например:

mounted() {
   …
}

Но вот на события Page мы не сможем подписаться, вот так работать не будет:

on : {
  pageInit() {
    …
  }
}

Навигация внутри Popup работать не будет: смена Page невозможна, ведь над Page нет View.

Компоненты с классом *-init создаваться не будут, но вы можете создать их самостоятельно поместив код их создания, к примеру, в popupOpen.

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

Второй вариант структуры шаблона, более сложный:

<div class=”popup”>
   <div class=”view view-init”>
      <div class=”page”>
       …
      </div>
   </div>
</div>

Тут мы обернули Page во View с классом view-init для автоматического создания View.

Отличия от предыдущего рассмотренного случая:

Теперь вы можете подписываться на события Page, например:

on : {
   pageInit() {
	…
   }
}

Но тут есть крайне важная деталь, допустим наш Popup-компонент:

<div class=”popup”>
   <div class="view view-init">
      <div class=”page”>
         <a href=”/second”>second</a>
      </div>
   </div>
</div>
...
on : {
   pageInit() {
      console.log(‘first’);
   }
}

Страница second, на которую мы переходим имеет такую структуру:

<div class=”page” data-name=”second”>
	...
</div>
...
on : {
   pageInit() {
      console.log(‘second’);
   }
}

После открытия Popup вы увидите в консоли ‘first’, затем, перейдя на страницу second, вы увидите в консоли:

first
second

Получается, что сработало событие pageInit для первой страницы, но почему, ведь страница не создавалась, конечно, заново? Причина в том, что событие pageInit вешается на весь <div class=”popup”>...</div>, а вызывается оно в <div class=”page”>...</div> и только благодаря “всплытию” мы его и ловим. Когда мы загружаем вторую страницу, то оно всплывает из <div class=”page” data-name=”second”>...</div> в итоге в <div class=”popup”>...</div> и срабатывает наше изначальное pageInit.

Чтобы этого избежать, можно написать так:

<div class=”popup”>
   <div class=”view view-init”>
      <div class=”page” @page:init=”onPageInit”>
         <a href=”/second”>second</a>
      </div>
   </div>
</div>
…
methods : {
   onPageInit() {
      …
   }
}

Теперь мы ловим событие непосредственно на Page. Можно исправить такое поведение также по-другому: сделать “правильную” и “полноценную” навигацию внутри View (см. далее).

Следующее отличие: теперь вы можете осуществлять навигацию внутри Popup, вернее внутри View.

Но и тут есть ограничение, допустим в Popup изначально был page1, затем вы перешли на page2. В этом случае не будет никаких проблем, вы сможете вернуться назад, т.к. page1 осталась в DOM. Но вот если вы перешли еще глубже, на page3, то при возврате на page2 и попытке вернуться еще дальше, на page1, у вас ничего не получится: page1 уже не существует в принципе. Здесь мы не будем говорить о steckPages и keepAlive. Чтобы решить данную проблему, нужно использовать другую структуру, об этом также читайте далее.

Стоит обратить внимание, что c помощью класса view-init мы создали View, но вот после закрытия Popup оно не уничтожается. Для этого в событии popupClose нужно вызывать метод destroy() у данного View, но лучше все же использовать другую структуру.

Ну и последнее отличие: теперь компоненты с классами *-init будут автоматически создаваться.

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

Третий способ, самый правильный:

Изначально мы имеем такой шаблон:

<div class=”popup”>
   <div class=”view”></div>
</div>

Обратите внимание, что <div class=”view”></div> ничего в себе не содержит, а также не имеет класса view-init.

Действие первое: создадим View в popupOpen и загрузим в него нужную страницу

on : {
   popupOpen() {
      const view = this.$app.views.create(this.$el.find('.view'), {
         name : 'popup', 
      });

      view.router.navigate(this.$route.query.url, {
         animate: false,
         context : {
            ...
         }
      });
   },
}

Имя для Popup мы задаем для того, чтобы именно его потом уничтожить. С помощью navigate мы загружаем необходимую начальную страницу, адрес которое берем из query. Почему не использовать нужный url при создании View? Так можно, но тогда мы не сможем передать context, если он нужен.

Далее в событии popupClose мы уничтожаем данный View:

on : {
   popupClose() {
     this.$app.views['popup'].destroy();
   },
}

Теперь мы избавились от двух недостатков предыдущего способа: мы получили правильную (почти) обработку событий Page и полноценную навигацию.

Так что же все-таки опять не так с событиями у Page? То, что при закрытии Popup у любой Page (их может быть несколько в DOM) события уничтожения, к примеру, pageBoforeRemove вызываться не будут. Почему? Да потому, что view.destroy() не вызовет этих событий. Также события Page как компонента, например, beforeDestroy также не будут вызываться!

Как же поступить, если нам нужно при удалении страницы обязательно что-то сделать? Для этого идеально подойдут события.

В data у экземпляра Framework7, т.е. там, где мы создаем App:

events : new Framework7.Events()

Теперь в popupClose выпустим событие о том, что Popup закрывается:

this.$root.events.emit(‘popup-close’, {});

На любой страницы, которая может быть открыта в данном Popup, нам нужно подписаться на события

on : {
   pageInit() {
      this.$root.events.once(‘popup-close’, () => {
		…
      });
   }
}

и отписаться

on : {
   pageBoforeRemove() {
      this.$root.events.off(‘popup-close’, () => {
         …
      });
   }
}

pageBoforerRemove будет по прежнему вызываться при навигации внутри View, который внутри Popup. Также здесь используется once, ведь событие будет вызываться только единожды.

На этом статья закончена.

1 Like
on : {
   popupOpen() {
      const view = this.$app.views.create(this.$el.find('.view'), {
         name : 'popup', 
      });

      view.router.navigate(this.$route.query.url, {
         animate: false,
         context : {
            ...
         }
      });
   },
}

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

on : {
   popupOpen() {
      const view = this.$app.views.create(this.$el.find('.view'), {
         name : 'popup', 
         url: this.$route.query.url
      });
   },
}