react-bootstrap ModalTrigger doesn't hide when the parent element is unmounted - react-bootstrap

We encountered a strange behavior when using react-bootstrap's ModalTrigger with an array of items, in that it doesn't go away when the parent/owner item is unmounted. We suspect this has to do with React's virtual DOM and the diffing mechanism, and/or our own misuse of the ModalTrigger.
The setup is simple: a Content react component has a state that holds an array of item names. It also has an onClick(name) function that removes that name from the array via setState. In the render, it uses _.map to create a bunch of Item react components.
Each Item component displays its name and a ModalTrigger that holds a button labeled "delete me". Click on the button and it opens the modal; click "OK" in the modal and it executes the callback to the Content remove function.
When deleting the last item it works fine: the final Item component is unmounted, and with it, the ModalTrigger and its corresponding modal.
The problematic behavior we see is when deleting any item other than the last one. The item is removed but the modal stays open, whereas I would naively expect the modal to disappear since the parent ModalTrigger is gone. Not only that, but when clicking "ok" again, the next item on the list is removed, and so on until the modal happens to be associated with the last item, at which point clicking "ok" will finally hide it.
Our collective hunch is that this is caused by the overlayMixin's _overlayTarget being an anonymous element in the document, so that different ModalTriggers don't differentiate between them. Therefore, when a parent unmounts and React looks for the DOM diff, it sees the previous trigger's and says "hey, that could work".
This whole issue can easily be addressed by adding a hide() call in the Item's inner _onClick() function as is commented out in the code, and we finally arrive at my question:
Am I using ModalTrigger correctly, in that expecting it to go away when the parent is unmounting? This is kind of how I expect React to work in general, which means a bug in react-bootstrap.
Or should I be explicitly calling hide() because that's they way this component was designed?
Following is a piece of code that reproduces this.
Thanks!
var DeleteModal = React.createClass({
render:function() {
return (
<ReactBootstrap.Modal onRequestHide = {this.props.onRequestHide} title = "delete this?">
<div className="modal-body">
Are you sure?
</div>
<div className="modal-footer">
<button onClick={this.props.onOkClick}>ok</button>
<button onClick={this.props.onRequestHide}>cancel</button>
</div>
</ReactBootstrap.Modal>
);
}
});
var Item = React.createClass({
_onClick:function() {
//this.refs.theTrigger.hide();
this.props.onClick(this.props.name);
},
render:function() {
return (
<div>
<span>{this.props.name}</span>
<ModalTrigger modal={<DeleteModal onOkClick={this._onClick}/>} ref="theTrigger">
<button>delete me!</button>
</ModalTrigger>
</div>
);
}
});
var Content = React.createClass({
onClick:function(name) {
this.setState({items:_.reject(this.state.items, function(item) {return item === name;})});
},
getInitialState:function() {
return {items : ["first", "secondth", "thirdst"]};
},
render:function() {
return (
<div>
{_.map(this.state.items, function(item, i) {
return (
<Item name={item} onClick={this.onClick} key={i}/>
)}.bind(this)
)}
</div>
);
}
});
React.render(<Content/>, document.getElementById("mydiv"));

Turns out it was a misuse of React's "key" property. We gave the mapped objects integer keys, so when the render was called again, the same initial keys were given, which is why React thought it should reuse the same DOM element.
If instead we give it key={item} (where item is a simple string) it solves it in our case; however, this introduces a subtle bug whereby if there are 2 identical strings, React will display only one.
Trying to outsmart it by giving it key={item + i} introduces an even subtler bug, where duplicate items are displayed but are delete en mass, but in this case the bug is in the onClick method which would need to be modified to accept an index of some sort.
Therefore my take-away is that the keys must be a unique string, and callbacks should take these keys into consideration when performing any modifications.

Related

Making focus works inside a CK Editor 5 createUIElement

So I've a custom widget which renders a custom component.
conversion.for('editingDowncast').elementToElement({
model: 'modelName',
view: (modelElement, viewWriter) => {
const modelName = modelElement.getAttribute('modelName');
const modelNameView = viewWriter.createContainerElement('span', {
class: 'modelName',
'data-modelName': modelName,
});
const reactWrapper = viewWriter.createUIElement(
'span',
{
class: 'modelName__react-wrapper',
},
function (this, domDocument) {
const domElement = this.toDomElement(domDocument);
rendermodelName(modelName, domElement);
return domElement;
},
);
viewWriter.insert(
viewWriter.createPositionAt(modelNameView, 0),
reactWrapper,
);
return toWidgetEditable(modelNameView, viewWriter);
},
});
Where rendermodelName will give back a React component with a simple input box as
return (
<div>
<input type="text" />
</div>
);
https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/frameworks/react.html.
But the problem is, whenever I tried to add some content inside the input, the focus is lost from the field and automatically moved to the surrounding editor. What am I missing. Tried creating a focushandler and adding the modelNameView to it.
Should I go with the new createRawElement? My current CK5 is 20.0.0 So I don't want any breaking changes coming now.
EDIT:
I researched a little bit more. seems like createRawElement may not work here. I think this doesn't have a simple solution. I tried with allowContentOf: '$block' which also not letting me focus. But these values are explicitly for normal CK widget, not for a react component.
I had the same issue and solved it by adding this tag to the parent div that wraps my Vue component.
https://ckeditor.com/docs/ckeditor5/latest/framework/guides/deep-dive/ui/widget-internals.html#exclude-dom-events-from-default-handlers
Adding from CKE Docs:
Sometimes it can be useful to prevent processing of events by default handlers, for example using React component inside an UIElement in the widget where, by default, widget itself wants to control everything. To make it possible the only thing to do is to add a data-cke-ignore-events attribute to an element or to its ancestor and then all events triggered by any of children from that element will be ignored in default handlers.
Let’s see it in an short example:
<div data-cke-ignore-events="true">
<button>Click!</button>
</div>
In the above template events dispatched from the button, which is placed inside containing data-cke-ignore-events attribute, will be ignored by default event handlers.
I faced the similar issue.
CKEditor will takes all the events on React component which you hosted on Widget.
The work around is to stop propagation of events to CKEditor which are fired from your DOM element(domElement) where your React component hosted.
Here is the sample code:
https://github.com/ckeditor/ckeditor5-core/compare/proto/input-widget#diff-44ca1561ce575490eac0d660407d5144R239
You should stop all required events. Also you can't paste any content inside the input field of React component. That will also listened by clipboardInput event of CKEditor.

cypress.io how to remove items for 'n' times, not predictable, while re-rendering list itself

I've a unpredictable list of rows to delete
I simply want to click each .fa-times icon
The problem is that, after each click, the vue.js app re-render the remaining rows.
I also tried to use .each, but in this cas I got an error because element (the parent element, I think) has been detached from DOM; cypress.io suggest to use a guard to prevent this error but I've no idea of what does it mean
How to
- get a list of icons
- click on first
- survive at app rerender
- click on next
- survive at app rerender
... etch...
?
Before showing one possible solution, I'd like to preface with a recommendation that tests should be predictable. You should create a defined number of items every time so that you don't have to do hacks like these.
You can also read more on conditional testing, here: https://docs.cypress.io/guides/core-concepts/conditional-testing.html#Definition
That being said, maybe you have a valid use case (some fuzz testing perhaps?), so let's go.
What I'm doing in the following example is (1) set up a rendering/removing behavior that does what you describe happens in your app. The actual solution (2) is this: find out how many items you need to remove by querying the DOM and checking the length, and then enqueue that same number of cypress commands that query the DOM every time so that you get a fresh reference to an element.
Caveat: After each remove, I'm waiting for the element (its remove button to be precise) to not exist in DOM before continuing. If your app re-renders the rest of the items separately, after the target item is removed from DOM, you'll need to assert on something else --- such as that a different item (not the one being removed) is removed (detached) from DOM.
describe('test', () => {
it('test', () => {
// -------------------------------------------------------------------------
// (1) Mock rendering/removing logic, just for the purpose of this
// demonstration.
// -------------------------------------------------------------------------
cy.window().then( win => {
let items = ['one', 'two', 'three'];
win.remove = item => {
items = items.filter( _item => _item !== item );
setTimeout(() => {
render();
}, 100 )
};
function render () {
win.document.body.innerHTML = items.map( item => {
return `
<div class="item">
${item}
<button class="remove" onclick="remove('${item}')">Remove</button>
</div>
`;
}).join('');
}
render();
});
// -------------------------------------------------------------------------
// (2) The actual solution
// -------------------------------------------------------------------------
cy.get('.item').then( $elems => {
// using Lodash to invoke the callback N times
Cypress._.times($elems.length, () => {
cy.get('.item:first').find('.remove').click()
// ensure we wait for the element to be actually removed from DOM
// before continuing
.should('not.exist');
});
});
});
});

Listen to events from parent component in child and execute child’s method in vue without hub

There seems to be a lot of discussion around this topic such as Stackoverflow answer using hub, Stackoverflow answer using refs, so I really like to ask experts to provide for once a clear concise answer to this question. If the answer is also just not possible please state that!
Here is the scenario:
There are two components, a parent and a child
<Parent> // There is a button here that can be clicked to emit an event using 'this.$emit()'
<Child></Child> // The child listens and once hears the event, it does something
</Parent>
What to be achieved?
Clicking the button in the Parent emits a certain event, the child will be constantly listening and once it hears the event it executes an action, such as calling a method of its own.
What is out there about this so far?
Using a hub, in Vue Hub it is clearly stated this is for Non
Parent-Child Communication, so what is the point in using it for a
parent-child communication?
Using Refs, which is given as an end solution when it is not possible to use props and events. So why it is not possible with events at first place?
My own thought
It seems to me the firing of an event and listening to it is only possible from child to parent, basically one way communication. The parent is able to emit an event but child component(s) are not able to capture the event. Why? I tried this and didn’t work:
In the parent component I have (triggered by clicking a button in the parent component):
methods: {
generateCharts: function () {
this.$emit('generate-charts')
console.log('charts generated')
}
In the child component I have:
mounted () {
this.parent.$on('generate-charts', function () {
console.log('event captured') // Here nothing gets logged to the console
})
}
Update
Just came across this answer Vue $emit.
Apparently this is not possible at all with Vue.
At first instance it seems it is a deficiency because I have been in several situations where I needed to fire an event from parent and listen to it in the child.
I can imagine there must be a reason why this is not possible with Vue, it is probably a design consideration, Vue experts explaining why this is the case, and what is the better design approach to solve in general a scenario to pass events from parent to child, would be very appreciated.
The answer is to use props and react to changes to those props. It is a little confusing to get used to at first because it seems like a lot of code to to do something simple but as your application gets more complex, the one way data flow enforced by the use of props really helps with debugging and reasoning about what the code is trying to accomplish.
For instance, a modal. Your parent sets showChildModal = true with a button click, this value is passed to the child as a prop. The child is watching for changes to the prop, when it sees it set to true it opens the modal. Finally, when the modal is closed, the child $emit('close') which the parent is watching for, and when it sees that it sets showChildModal = false
Vue.component('child', {
template: '#child',
props: ['showModal'],
name: 'child',
watch: {
showModal: function(show) {
if (show) {
window.setTimeout(() => {
if (window.confirm("Hi, I'm the child modal")) {
this.$emit('close');
} else {
this.$emit('close');
}
}, 100)
}
}
}
})
var vm = new Vue({
el: '#el',
data() {
return {
showChildModal: false
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="el">
Parent
<p>
<button #click="showChildModal = true">Click to show child modal</button>
</p>
<hr>
<p>
<child :show-modal="showChildModal" v-on:close="showChildModal = false"> </child>
</p>
</div>
<script type="x-template" id="child">
<div>
Child
<p>
modal open? {{ showModal }}
</p>
</div>
</script>

Angular2 - DOM events lost on re-render when using Redux and uni-directional data flow

I wasn't sure of the best way to word the title, but hopefully the description will clarify.
I have an Angular2 component that uses Redux for state management.
That component uses *ngFor to render an array of small inputs with buttons like this. The "state" is something like this...
// glossing over how I'd get this from the Redux store,
// but assume we have an Immutable.js List of values, like this...
let items = Immutable.fromJS([{value: foo}, {value: bar}, /*...etc*/ })
And the template renders that like so...
<input *ngFor="let item of items, let i = index"
type="text"
value="item.get('value')"
(blur)="onBlur($event, i)">
<button (click)="onClick($event)">Add New Input</button>
When an input is focused and edited, then focus is moved away, the onBlur($event) callback is called, a redux action (ie: "UPDATE_VALUE") is dispatched with the new value.
onBlur($event, index) {
let newValue = $event.target.value;
this.ngRedux.dispatch({
type: "UPDATE_VALUE",
value: {index, newValue}
});
}
And the reducer updates the value (using Immutable.js):
case "UPDATE_VALUE": {
let index = getIndex(action.value.index); // <-- just a helper function to get the index of the current value.
return state.updateIn(["values", index], value => action.value.newValue);
}
The state is updated, so the component is re-rendered with the updated value.
When the button next to the input is clicked, the onClick($event) callback is fired which dispatches a different redux action (ie: "ADD_VALUE"), updates the state, and the component is re-rendered with a new blank input & button.
The problem comes up when the input is focused (editing) and the user clicks the button. The user intended to click the button, but because they happened to be focused on the field, it doesn't behave as expected. The (blur) event is fired first, so the input onBlur callback is fired, redux action dispatched, state updated, component re-rendered. BUT, the button (click) is lost, so the new input isn't created.
Question:
Is there a way to keep track of the click event and trigger the second action after the re-render? Or, is there a way to somehow chain the redux actions so they happen in sequence before the re-render?
Side-note - I've tried changing the (click) event to use (mousedown) which is triggered before the (blur) but that caused Angular (in devMode) to complain that the #inputs to the components were changed after being checked (the state changed during the change detection cycle). I didn't dig into that too deeply yet though. I'm hoping to find a solution using click and blur as is.
Thanks!

drop-area element not hiding with DND module and FineUploaderBasic

I've got everything working well except the drop area element doesn't hide after moving back off the page.
For example, if I drag something to the page, the drop area element shows, but then if I decide not to drop, it stays visible instead of hiding. It looks like in the demos on the site, it should hide again.
I've been studying this page: http://docs.fineuploader.com/branch/master/integrating/options/fineuploader.html#draganddrop-option-properties
There doesn't seem to be a callback for when there is no longer a file about to be dropped in the browser window. My code looks just like the examples, but has my own element id and class names, both of which work.
Any ideas?
Update - here is some code:
<div id="file-upload-well" class="well text-center">
<div id="file-upload-drop-area" style="display:none;">Drop files here</div>
Upload Files
</div>
and here is the DND js:
var dragAndDropModule = new qq.DragAndDrop({
dropZoneElements: [document.getElementById('file-upload-drop-area')],
classes: {
dropActive: "dropActive"
},
callbacks: {
processingDroppedFiles: function () {
$('#file-upload-drop-area').hide();
},
processingDroppedFilesComplete: function(files) {
uploader.addFiles(files);
}
}
});
The issue was setting the hideDropZonesBeforeEnter is set to false by default and needed to be set to true.
Both the variable name and the help documentation make it seem it will only show the drop zone when a file is directly over it, but instead it hides and shows the dropdown when a file is over a compliant browser.

Resources