Реагировать - Как уведомить Поппера о перемещении моего поповера всякий раз, когда реакция обновляет любой элемент DOM
Соответствующие версии: React 16.4.2, Bootstrap 4.1.3, popper.js 1.14.4, Typescript 3.0.3
Я использую функциональность Bootstrap Popover в моем react
приложение.
Popover работает хорошо, если остальная часть страницы статична. Когда страница изменяется (на уровне браузера), Popover перемещается очень быстро и плавно, поэтому он остается видимым, в то время как контент, к которому он привязан, виден:
- при прокрутке, если он сталкивается с краями окон
- если экран поворачивается на телефоне
- если окно изменено
Это все работает хорошо, потому что popper.js
видимо смотрит window.scroll
а также window.resize
события, согласно этому ответу: Bootstrap 4 - как работает автоматическое изменение положения Popover?
Проблема возникает, когда мой react
приложение начинает показывать / скрывать элементы DOM. Так как popper.js
не знает о react
, он не знает, что DOM изменился, поэтому он не знает, что Popovers, возможно, нужно переместить.
Я знаю звоню popover("update")
на каждом якоре Popover работает, потому что я добавил такой код, чтобы делать это периодически:
window.setInterval(()=> $(this.selfRef).popover("update"), 100);
Но это отвратительно, расточительно и немного дерзко.
Есть ли способ иметь react
скажите мне, когда он обновляет любой узел в DOM, чтобы я мог потом сказать popper.js
обновить положение поповерс?
Обратите внимание, что react
Компонент, который вызывает изменение DOM, не обязательно находится рядом с компонентом, который использует Popover. Это может быть что-то в совершенно отдельной части иерархии, которое отображается перед компонентом с поповером - так что я не думаю, что решение componentWillReceiveProps()
или подобные методы в компоненте Popover, потому что, вероятно, это не тот компонент, который вызывает движение.
Обратите внимание, что я в курсе таких проектов, как react-bootstrap
, reactstrap
или же react-popper
- но я не хочу их использовать.
РЕДАКТИРОВАТЬ: кажется, что MutationObserver может быть нереагировать способ сделать это. Я только что подумал, так как React уже выполняет всю эту работу по согласованию, возможно, есть способ заставить его уведомить меня, когда он действительно редактирует DOM.
2 ответа
Это моя нынешняя попытка MutationObserver
основанное решение.
UserApp
это компонент, расположенный в верхней части иерархии приложения. Popover
класс (более) используется в разных местах в моем приложении для кучу вещей.
Возможность бесконечной рекурсии, вызванной стрельбой popover("update")
из MutationObserver
событие заставляет меня опасаться использовать это решение в долгосрочной перспективе. Кажется, что сейчас он выполняет свою работу, но это одна из вещей, которых однонаправленное связывание призвано избегать.
С другой стороны, это работает, даже если у вас есть нереагирующие компоненты в вашем приложении (как, например, Bootstrap). navbar
).
export class UserApp extends React.Component<any, AppState> {
public domChangeObservers = $.Callbacks();
public mutationObserver = new MutationObserver(
(mutations: MutationRecord[])=>{
// premature optimisation?
// I figure I don't care about each individual change, if the browser
// batched em up, just fire on the last one.
// But is this a good idea given we have to inspect the mutation in order
// to avoid recursive loops?
this.domChangeObservers.fire(mutations[mutations.length-1]);
}
);
constructor(props: any) {
super(props);
this.mutationObserver.observe(document.documentElement, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
}
componentWillUnmount(){
this.mutationObserver.disconnect();
}
...
}
const DefaultTrigger = "click";
export interface PopoverProps{
popoverTitle: string | Element | Function;
popoverContent: string | Element | Function;
/** Set to "focus" to get "dismiss on next click anywhere" behaviour */
popoverTrigger?: string;
/** Leaving it empty means that the popover gets created
* as a child of the anchor (whatever you use as the child of the popover).
* Setting this to "body" means the popover gets created out on the body
* of the document.
* "body" can help with stuff like when the popover ends up
* being clipped or "under" other components (because of stuff like
* `overflow:hidden`).
*/
container?: string;
allowDefaultClickHandling?: boolean;
ignoreDomChanges?: boolean;
id?: string;
}
export class Popover
extends PureComponent<PopoverProps, object> {
// ! to hack around TS 2.6 "strictPropertyInitialization"
// figure out the right way... one day
selfRef!: HTMLSpanElement;
onDomChange = (mutation:MutationRecord)=>{
/*
- popover("update") causes DOM changes which fire this handler again,
so we need to guard against infinite recursion of DOM change events.
- popover("update") is async, so we can't just use an "if not currently
handling a mutation" flag, because the order of events ends up being:
onDomChange() -> flag=true -> popover("update") -> flag=false ->
popper.js changes DOM -> onDomChange() called again -> repeat forever
- Can't just detect *this* popover. If DOM event occurs because popovers
overlay each other they will recurse alternately - i.e. pop1 update
call makes DOM changes for pop2, pop2 update makes changes for pop1,
repeat forever.
*/
if( Popover.isPopoverNode(mutation) ){
return;
}
/*
- tell popper.js to reposition the popover
- probably not necessary if popover is not showing, but I duuno how to tell
*/
$(this.selfRef).popover("update");
};
private static isPopoverNode(mutation: MutationRecord){
/*
Had a good attempt that used the structure of the mutation target to
see if it's parent element was defined as `data-toggle="popover"`; but
that fails when you set the `container` prop to some other element -
especially, "body", see the comments on the Props .
*/
if( mutation.target.nodeType != 1 ){
return false;
}
// Is Element
let element = mutation.target as Element;
/*
Is the mutation target a popover element?
As defined by its use of the Bootstrap "popover" class.
This is dodgy, it relies on Bootstrap always creating a container
element that has the "popover" class assigned.
BS could change their classname, or they could
change how they structure their popover, or some other
random widget could use the name.
Actually, this can be controlled by overriding the popover template,
which I will do... later.
*/
let isPopoverNode = element.classList.contains("popover");
// very helpful when debugging - easy to tell if recursion is happening
// by looking at the log
// console.log("target", isPopoverNode, mutation, mutation.target );
return isPopoverNode;
}
componentDidMount(): void{
// the popover() method is a "JQuery plugin" thing,
// that's how Bootstrap does its stuff
$(this.selfRef).popover({
container: this.props.container || this.selfRef,
placement: "auto",
title: this.props.popoverTitle,
content: this.props.popoverContent,
trigger: this.props.popoverTrigger || DefaultTrigger,
});
if( !this.props.ignoreDomChanges ){
UserApp.instance.domChangeObservers.add(this.onDomChange);
}
}
componentWillUnmount(): void {
if( !this.props.ignoreDomChanges ){
UserApp.instance.domChangeObservers.remove(this.onDomChange);
}
// - without this, if this component or any parent is unmounted,
// popper.js doesn't know that and the popover content just becomes
// orphaned
$(this.selfRef).popover("dispose");
}
stopClick = (e: SyntheticEvent<any>) =>{
if( !this.props.allowDefaultClickHandling ){
// without this, if the child element is an <a> or similar, clicking it
// to show/dismiss the popup will scroll the content
e.preventDefault();
e.stopPropagation();
}
};
render(){
let popoverTrigger = this.props.popoverTrigger || DefaultTrigger;
// tabIndex is necessary when using "trigger=focus" to get
// "dismiss on next click" behaviour.
let tabIndex = popoverTrigger.indexOf("focus")>=0?0:undefined;
return <span id={this.props.id}
tabIndex={tabIndex}
ref={(ref)=>{if(ref) this.selfRef = ref}}
data-toggle="popover"
onClick={this.stopClick}
>{this.props.children}</span>;
}
}
"Реагирующий Компонент, который вызывает изменение DOM, не обязательно находится рядом с Компонентом, который использует Popover. Это может быть что-то в совершенно отдельной части иерархии"
Если и Компонент, который изменяет DOM, и Компонент, который создает Popover, находятся в одном и том же родительском элементе, вы можете совместно использовать метод в родительском элементе, который выполняет .popover('update')
, Компонент, который изменяет DOM, должен был бы вызвать это событие, но он не должен быть специально "осведомлен" о Popover Component. Компонент Popover не должен знать об изменении компонента DOM.
class ChangeDom extends React.Component {
constructor(props) {
super(props);
this.changeDom = this.changeDom.bind(this);
}
changeDom () {
this.props.domChanged();
}
render() {
return (
<div>
<button className="ml-2 btn btn-primary" onClick={this.changeDom}>Change Dom
</button>
</div>)
}
}
class Pop extends React.Component {
constructor(props) {
super(props);
this.togglePopover = this.togglePopover.bind(this);
}
togglePopover() {
$('[data-toggle="popover"]').popover('toggle');
}
render() {
return (
<div class="position-relative">
<button className="mt-4 btn btn-primary" onClick={this.togglePopover} data-toggle="popover"
</button>
</div>)
}
}
class Parent extends React.Component {
domChanged(){
$('[data-toggle="popover"]').popover("update");
}
render() {
return (
<div>
<ChangeDom domChanged={this.domChanged} />
<Pop />
</div>)
}
}