相关版本:React 16.4.2,Bootstrap 4.1.3,popper.js 1.14.4,Typescript 3.0.3
我在react
应用中使用了Bootstrap Popover功能。
如果页面的其余部分是静态的,则Popover效果很好。更改页面时(在浏览器级别),Popover可以非常快速,顺利地重新定位,因此在锚定的内容可见时,它仍然可见:
这一切都很好,因为按照以下答案,popper.js
显然正在观看window.scroll
和window.resize
事件:Bootstrap 4 - how does automatic Popover re-positioning work?
当我的react
应用程序开始显示/隐藏DOM元素时出现问题。因为popper.js
不了解react
,所以它不知道DOM发生了变化,因此也不知道Popover可能需要重新定位。
我知道在每个Popover锚点上调用popover("update")
都是可行的,因为我添加了这样的代码来间歇性地做到这一点:
window.setInterval(()=> $(this.selfRef).popover("update"), 100);
但是那令人讨厌又浪费,而且有点简陋。
有没有办法让react
告诉我何时更新DOM中的任何节点,因此我可以告诉popper.js
更新弹出窗口的位置?
请注意,引起DOM更改的react
组件不一定位于使用Popover的组件附近。它可能是层次结构中完全独立的部分,恰好在带有弹出框的组件之前显示-所以我不认为解决方案是componentWillReceiveProps()
或类似Popover组件上的方法,因为它是可能不是导致移动的组件。
请注意,我知道react-bootstrap
,reactstrap
或react-popper
之类的项目-但我不想使用它们。
编辑:看来MutationObserver可能是一种非反应性的方法。我只是想出了,因为React已经完成了所有协调工作,也许有一种方法可以让它在实际编辑DOM时通知我。
答案 0 :(得分:0)
“导致DOM更改的react组件不一定 位于使用Popover的组件附近。它可能是 层次结构中完全独立的部分中的某些东西”
如果更改DOM的组件和创建Popover的组件都在同一 parent 中,则可以在执行.popover('update')
的父级中共享一个方法。更改DOM的组件将需要触发此事件,但是不需要特别“意识到” Popover组件。 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>)
}
}
答案 1 :(得分:0)
这是我目前针对基于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>;
}
}