I am thinking about implementing context help in my application and i wonder if it is possible to implement it the way i have in mind:
Register global shortcut to Ext.Body() ex. ctrl+h
Shortcut handler will find the focused component and call its showHelp method
If component have no showHelp method it will move to its parent and call showHelp method.
I wonder if step 2 is possible?. Or is there a better way to do this?
Ok i dig into it. At first i do the following to implement context help:
Created help plugin and added it to each component which should provide context help. The plugin register click listener to each component.
The fired click event registers its source into static HelpManager that holds reference to last focused component
Then after pressing shortcut i get the last component from HelpManager and fire context help using its help config.
Code:
Ext.define('GSIP.core.help.GSIPHelp',{
alias:'plugin.help',
init: function(component) {
//var me = this;
component.on('afterrender',function(c) {
//WHY FOCUS EVENT IS NOT WORKING?? ONLY CLICK.
c.getEl().on('click',function() {
console.log('SHOUD REGISTER FOCUS');
GSIP.core.help.GSIPHelpMgr.registerFocus(component);
});
});
}
});
That solution had a serious flaw. If component has a parent and both of them got help plugin the click event is firing twice with parent as last.
During coding i found in docs Ext.FocusManager and that was it! Using it i am able to find focused component. Using simple function: if the component does not have help i scan through its parents to find one, if there is no parent i just show index, i was able to create context help.
Ext.define('GSIP.core.help.Help',{
mixins:{
document:'GSIP.core.utils.Document'
},
url:'/GSIP/resources/gsip/core/help/html/',
showHelp:function(comp) {
if (comp.help != undefined) {
this.showDocumentSrc(this.url + comp.help + '.html');
}else{
if (comp.ownerCt == undefined) {
this.showDocumentSrc(this.url + 'index.html');
}else{
this.showHelp(comp.ownerCt);
}
}
}
});
Related
I know i can use elementA.contains(elementB) to test if an element B is part of the Dom subtree of an element A.
But when using a portal to display B this wont work anymore as B is not anymore in the Dom Subtree.
Is there any clean way to test that B is in the React subtree of A ?
Edit:
codeSandbox example code https://codesandbox.io/s/nifty-surf-e1e8by?file=/src/App.js
Click the "open a dialog in portal" and then click the close button.
It will fire the portal click event. I know in this case we could have bound the event with onClick props on the portal markup but this is not the solution i'm looking for. What I'm looking for is a way to test in the event handler that evt.currentTarget.contains(evt.target) as part of its react subtree not dom subtree. Hope this is more clear.
To borrow the principles of React Testing Library, tests should resememble how your software is used, not how it is built.
Unless you are building a library, the question of whether a component is part of another component's subtree probably isn't relevant – that's an implementation detail (which may change at some point!).
In this case, it sounds like you want to ensure that a particular component is clickable.
You could add a check within your event listener to see if the click came from the component in question:
if (evt.currentTarget.querySelector(".dialog") === null) return;
https://codesandbox.io/s/late-water-3ddf5p?file=/src/App.js:877-941
Or simply target .dialog directly. From the user perspective, does it matter that it is inside the #portal element?
#Daniel Grant here's the crappy solution I came up with for now:
const getFiber = (component) => {
const fiberKey = Object.keys(component).filter(k=>!!k.match(/reactFiber/))[0]
return component[fiberKey]
}
const isInReactSubTreeOf = (maybeChildElementOrFiber, parentElement) => {
const childFiber = maybeChildElementOrFiber instanceof HTMLElement ? getFiber(maybeChildElementOrFiber) : maybeChildElementOrFiber
if (!childFiber.return) { // no more parent exit
return false
}
if(childFiber.return?.stateNode === parentElement ) { // testing parentElement
return true
}
// we continue to walk the tree from parent
return isInReactSubTreeOf(childFiber.return, parentElement)
}
Then we can do:
isInReactSubTreeOf(elementB, elementA)
This was interesting to find out but, this used unreliable apis so I was looking for a cleaner way to perform the same thing.
When I say unreliable, it's because it's clearly not intended to be used that way. Future implementation can break this hack without warning, as once again it's not intended to be used that way. This is internal api we should not mess with.
I've using "billboard.js": "^1.10.2", and react.js
I had searched billboard.js's documentation and found that onresize(), onresized() is attached on window and when I call
chart.destroy() then It removes every events being attached on window that being related with this library.
So I tested it without state update on onresize(), onresized() it successfully deleted all events, but when I did it with state update on onreszie(), onresized() events were still attached on window. As a result of this I think this issue happens not because of billboardjs, but reactjs.
Why is it? Any ideas?
//...
const [isResize, setIsResize] = useState(false);
const options = {
onresize(ctx) {
// "resize" keep prints even chart.destory() is called.
console.log("resize");
setIsResize(true);
},
onresized(ctx) {
setIsResize(false);
},
//...
<Chart
className="timelineChart"
options={options}
isResize={isResize}
ref={chartRef}
/>
//...
const options = {
onresize(ctx) {
// "resize" is no longer prints when chart.destory() is called.
console.log("resize");
},
onresized(ctx) {
},
//...
I believe you are attaching multiple event listeners. Each time the Page1 component re-renders, it attaches a new set of event listeners without cleaning up the old ones. What causes a re-render? State changes. That's why you are only seeing the issue once you add useState and setState.
You can verify this by checking the logs and noticing this not so helpful error:
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect
cleanup function.
in Page1 (at App.js:13)
You'll need to modify the code related to attaching/detaching event listeners to avoid this. I'm not familiar with Billboard so I can only tell you where the problem is, not the exact spot to fix it.
My gut says its the Chart.jsx here:
renderChart = () => {
console.log('render chart');
const { current } = this.chartRef;
if (current !== null) {
this.chartInstance = bb.generate({
...this.props.options,
bindto: this.chartRef.current
});
}
};
Updated with full solution
I was correct in that the Chart.jsx is where the problem lies.
Listeners should be attached when the DOM is created and removed with the DOM is destroyed. You were not wrong when you first thought to use the React Lifecycles for this chore, however I find the Callback References to be more useful, especially when some of the DOM may be destroyed or created during update cycles (you do not have this problem).
Callback References can be tricky, do not use functions that get recreated each render (trust me its a headache). Callback References are called for two reasons. First, the DOM has been created and to hand you a reference to the element. Second, the DOM has been destroyed, so time to clean up. If React senses a change in the Callback Reference (i.e. you give it a new one) it will tell the first Callback Reference to clean up, and the second Callback Reference to initialize (this is the headache I mentioned). You can avoid this by using an instance method.
// Bind to 'this' otherwise 'this' is lost
setChartRef = (ref) => {
// Remove listeners
if (!ref) {
console.log('no reference');
this.chartRef.current = null;
this.destroy();
}
// Add listeners
else {
console.log('new reference');
this.chartRef.current = ref;
this.createChart();
}
};
Next piece of the puzzle, you only want to call bb.generate one time. This was causing multiple listeners to be created. So I've simplified and renamed your renderChart to createChart
createChart = () => {
this.chartInstance = bb.generate({
...this.props.options,
bindto: this.chartRef.current
});
};
Finally, none of the lifecycle methods are necessary because Callback Reference tell us exactly when to create the chart and when to destroy it. You may be wondering what about resizing the chart? Seems like that is taken care of automatically? I could be missing something here, but in the event you need to update the chart, use this.chartInstance in an update lifecycle method.
My full modifications here:
onresize is actually a DOM event, it's not part of billboard or React.
https://developer.mozilla.org/en-US/docs/Web/API/Window/resize_event
You'll need to cancel your function somehow, like making it run conditionally or defining it in a component that will be unmounted.
I am using a library (https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu) that scrolls on use of the mousewheel, and I want to use this functionality when swiping left or right.
I am using hammerjs to replicate swipeleft and swiperight behavior, and this is working.
However, creating a WheelEvent does not seem to trigger the functionality dependent on the WheelEvent.
I am using componentDidUpdate for now as my react lifecycle method because for some reason this.containerRef.current is always null in componentDidMount, but once I figure out the reason behind that, I'll probably move it.
Anyway, here's my code:
componentDidUpdate() {
if(this.containerRef.current !== null) {
this.hammer = Hammer(this.containerRef.current)
this.hammer.on('swiperight', () => alert("swipe right"));
var wheelevent = new WheelEvent("wheel", {deltaX: 500, deltaY: 500});
this.hammer.on('swiperight', () => window.dispatchEvent(wheelevent));
}
}
Now I want to point out, the alert for swipe right DOES happen, so the behavior is definitely triggering, however my WheelEvent is not being caught by the scroll library.
How should I trigger a WheelEvent programmatically?
EDIT - I made a codepen about it:
https://codesandbox.io/s/react-horizontal-scrolling-menu-fi7tv
My hunch is that issue is related to Dragging being disabled and the event is canceled.
So you need to send the event down the chain a bit. I have updated the codesandbox below which works
https://codesandbox.io/s/react-horizontal-scrolling-menu-j46l8
The updated code part is below
var elem = document.getElementsByClassName("menu-wrapper")[0];
this.hammer.on("swiperight", () => elem.dispatchEvent(wheeleventRight));
this.hammer.on("swipeleft", () => elem.dispatchEvent(wheeleventLeft));
You may want to better the approach though in a more reactive fashion later. But this does show that once you sent the event in lower order elements the wheeling does work well
I'm running into a weird case that only seems to happen upon first loading a component on a heavily based component page (loading 30+ components).
#Component{
selector: <parent-component>
template: `<child-component [myObject]=myObject>
}
export class ParentComponent {
private myObject:DTOValue;
constructor(service:MyService){
service.getDTOValue().subscribe((dtoValue:DTOValue) => {
this.myObject = dtoValue;
});
}
}
#Component{
selector: <child-component>
template: `<div></div>
}
export class ChildComponent {
#Input set myObject(value:DTOValue) => {//do something};
constructor(){
}
}
In this code, the Parent is going to get a value to a child as an input. This value comes from a request at a later time, so when the child is first initialized, the input could be undefined. When the value does get returned from the request and is set on the variable myObject, I'd expect that the child component would receive an input event being triggered. However, due to the timing, it seems like this is not always the case, especially when I first load a page that contains a lot of files being loaded.
In the case that the child component doesn't receive the input, if I click else where on my page, it seems to now trigger the change detection and will get the input value.
The 2 possible solutions I can think of that would require some large code changes so I want to make sure I choose the right now before implement them.
Change the input to be an Subject, so that I push the input value which should ensure that a correct event is triggered(this seems like overkill).
Use the dynamic loader to load the component when the request as return with the correct value (also seems like overkill).
UPDATE:
Adding a plnker: http://plnkr.co/edit/1bUelmPFjwPDjUBDC4vb, you can see in here that the title seems to never get its data bindings applied.
Any suggestions would be appreciated.
Thanks!
If you can identify where the problem is and appropriate lifecycle hook where you could solve it, you can let Angular know using ChangeDetectorRef.
constructor(private _ref: ChangeDetectorRef)
method_where_changes_are_overlooked() {
do_something();
// tell angular to force change detection
this._ref.markForCheck();
}
I had a similar issue, only with router - it needed to do redirect when/if API server goes offline. I solved it by marking routerOnActivate() for check...
When you trigger change detection this way a "branch" of a component tree is marked for change detection, from this component to the app root. You can watch this talk by Victor Savkin about this subject...
Apologize, the issue ended up being my interaction with jQuery. When I triggered an event for a component to be loaded, inside of the jQuery code, it wouldn't trigger the life cycle. The fix was after the code was loaded to then call for a change detection.
I have a frontend javascript application built with require.js and backbone.js. Most parts of the application use the standard/recommended way of building application, including routing by using the Backbone Router object.
Now I want to add some more visual changes in one part of the application. Instead of clicking a link and the router render another view, I want some visual changes before that happens. Like GUI-effects happening when clicking the link, then when that effect is complete, the new view should render like before.
I guess one possible way to do this is by hooking a click event to the given link, cancel normal propagation (canceling the route catching in the backbone object), perform the visual stuff, and then manually call the router or render the view directly. Then I would need to have access to the router object from the view (to call the action method that normally catch the click), or I would need to render the view from within the click event added to the link, causing the render code to be duplicated (in the event function and in the view).
Is there a good and tidy way to do something like this, without making ugly spaghetti-code?
You can use the following code to catch all clicks on every link, and then do what you want :
$(document).on('click', 'a:not([data-bypass])', function (evt) {
var href = $(this).attr('href');
var protocol = this.protocol + '//';
if (href.slice(protocol.length) !== protocol) {
evt.preventDefault();
var rootLength = Backbone.history.root.length - ((Backbone.history.root.substring(Backbone.history.root.length - 1) === '/') ? 1 : 0);
// Here before calling the history.navigate that trigger your router
// routes, do your visual effects
Backbone.history.navigate(href.slice(rootLength), {
trigger: true
});
}
});