Old component retains state on back button - ruby-on-rails

I have a link in a modal that goes to a new page, and I can't seem to reset the state of the component with the modal when directing to that page.
The component with the modal seems to be keeping its state after directing to the new page, because when I hit the back button, it automatically opens the modal.
The modal is either opened or closed based on the state of modalIsOpen.
So I have my simplified Listings component:
import React from 'react'
import ListingModalContent from '../ListingModalContent'
import Modal from '../Modal'
export default class Listings extends React.Component {
constructor(props) {
super(props)
this.state = {
modalIsOpen: false,
modalContent: null
}
}
modalClick = (e, listing) => {
e.preventDefault()
this.setState({
modalContent: <ListingModalContent listing={listing}/>
}, () => {
this.setState({modalIsOpen: true})
})
}
modalClose = () => {
this.setState({modalIsOpen: false})
}
componentWillMount() {
this.setState({modalIsOpen: false})
console.log('mounting...')
console.log(this.state.modalIsOpen)
}
componentWillUnmount() {
console.log('unmounting...')
this.setState({
modalIsOpen: false
}, () => {
console.log('got here...')
console.log(this.state.modalIsOpen)
})
console.log(this.state.modalIsOpen)
}
render() {
const listings = this.props.listings.map(listing => (<div className="listing">
<a href="#" onClick={e => this.modalClick(e, listing)}>More Details</a>
</div>))
return (<div id="listings">
<section className="listings">
{listings}
<Modal visible={this.state.modalIsOpen} onClose={this.modalClose}>
{this.state.modalContent}
</Modal>
</section>
</div>)
}
}
And my ListingsModalContent component:
import React from 'react'
export default class ListingModalContent extends React.Component {
constructor(props) {
super(props)
}
render() {
const {listing} = this.props
return (<div className="listing-modal">
<div className="details">
<h2 className="address">{listing.address}</h2>
<p className="description">{listing.description}</p>
</div>
<div className="btn-container">
<a href={`/listings/${listing.slug}`} onClick={this.props.modalClose}>View Full Listing</a>
</div>
</div>)
}
}
The console output is...
// after initially mounting:
mounting...
false
// after clicking the listing link:
unmounting...
true
// after hitting the back button:
mounting...
false
I'm pretty sure I need to fix this by using componentWillUnmount to set the state of modalIsOpen to false before the component unmounts, but it never seems to finish setting the state before unmounting.
I'm using react on rails, which seems to use some hybrid routing rails/react routing system, but I'm not too familiar with it, and don't want to go down that rabbit hole at the moment if I don't have to.
So my question is, if this is expected behavior of the react component lifecycle, is there a way I can ensure the state of modalIsOpen is reset before unmounting? Or is there a way I can make sure my state is reset to its initial state when going back? Or is this more likely a consequence of the routing system I'm using?

This is strange, unexpected bahaviour in react and for sure is not caused by react (as #azium stated) but some 'things around', probably react_on_rails issue (or 'feature'). Report a bug/create an issue on github.
As you see in log state has proper value on mounting and there is no reason to render modal. 'Normal' react would work as expected.
There is no sense to set state on unmount - instance of component will be destroyed, its state, too.
HINTS
You shouldn't store modal content in state. It's possible, it works for simple cases, it can be used a kind of cache for parts of content, but you can have issues when conditional rerendering needed (using prop/state changes).
After setting state this.setState({modalIsOpen: true, modalContent:listing}) in click handler you can use conditional rendering (in render):
{this.state.modalIsOpen && <ListingModalContent listing={this.state.modalContent}/>}
To be true even this.setState({modalIsOpen: true}) can be removed (by save only listing idx in state, '-1' for closing) but then code can be less readable (storing additional pointer is cheap).

Related

Update task attributes in flex for assigned task

I am updating a task property through a request to my backend for an assigned task, this request is made on a custom component, on a button click during the call.
I can see in my twilio console that the update went fine and the new value is shown there, but the task prop on my flex components are not being updated, maintaining the same attributes since the assignment.
Is there a way to "refresh" the task in flex? I would need this updated attribute in order to perform a conditional rendering on my custom component.
Thanks in advance to anyone that helps me out.
import React from 'react';
import { withTaskContext } from '#twilio/flex-ui';
class IsRecording extends React.Component {
constructor(props) {
super(props);
}
render() {
// this.task = this.props.tasks
return (
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#E50000">
<path
d="M16 16c0 1.104-.896 2-2 2h-12c-1.104 0-2-.896-2-2v-8c0-1.104.896-2 2-2h12c1.104 0 2 .896 2 2v8zm8-10l-6 4.223v3.554l6 4.223v-12z"/>
<animate
attributeType="XML"
attributeName="fill"
values="#800;#f00;#800;#800"
dur="1.5s"
repeatCount="indefinite"/>
</svg>
</div>
)
}
};
export default withTaskContext(IsRecording);
Twilio developer evangelist here.
If you have access to the task object within Flex you do not need to do so via the back-end. Instead, you can call on task.setAttributes and that will update the task attributes directly and cause it to update the state everywhere within Flex.
However, the difficulty here is that a supervisor will not have a live view on tasks, so you need to do a little more work to have the supervisor listen for changes on the tasks. I had a play around and got this working. It uses the Sync liveQuery interface to subscribe to updates to workers' tasks.
We create the live query subscription on componentDidMount. The first argument when creating the live query is "tr-task" and this refers to all tasks in the system. The second argument is the query, in this case we are querying for all tasks for this worker.
Once we get the query we can load the current items and set them in the state. We then listen for additions, updates and removals and update the state accordingly.
In the render function we calculate whether any of the tasks have an isRecording attribute and display the icon accordingly.
Here's the <IsRecording> component:
import React from "react";
import { Manager } from "#twilio/flex-ui";
class IsRecording extends React.Component {
constructor(props) {
super(props);
this.state = {
tasks: [],
};
}
componentDidMount() {
Manager.getInstance()
.insightsClient.liveQuery(
"tr-task",
`data.worker_sid == "${this.props.worker.sid}"`
)
.then((query) => {
this.liveQuery = query;
const items = query.getItems();
this.setState({ tasks: Object.values(items) });
query.on("itemAdded", (item) => {
this.setState((prevState) => ({
tasks: [...prevState.tasks, item.value],
}));
});
query.on("itemUpdated", (item) => {
this.setState((prevState) => ({
tasks: [
...prevState.tasks.filter((task) => task.sid !== item.value.sid),
item.value,
],
}));
});
query.on("itemRemoved", (item) => {
this.setState((prevState) => ({
tasks: prevState.tasks.filter(
(task) => task.sid !== item.previousItemData.sid
),
}));
});
})
.catch((err) =>
console.debug(`Error when subscribing to live updates for Tasks`, err)
);
}
componentWillUnmount() {
if (this.liveQuery) {
this.liveQuery.removeAllListeners();
this.liveQuery.close();
}
}
render() {
if (this.state.tasks.some((task) => task.attributes.isRecording)) {
return (
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#E50000">
<path d="M16 16c0 1.104-.896 2-2 2h-12c-1.104 0-2-.896-2-2v-8c0-1.104.896-2 2-2h12c1.104 0 2 .896 2 2v8zm8-10l-6 4.223v3.554l6 4.223v-12z" />
<animate attributeType="XML" attributeName="fill" values="#800;#f00;#800;#800" dur="1.5s" repeatCount="indefinite" />
</svg>
</div>
);
} else {
return null;
}
}
}
export default IsRecording;
You attach it to the WorkersDataTable like this:
flex.WorkersDataTable.Content.add(
<ColumnDefinition
key="is-recording"
header={"Recording"}
content={(items) => {
return (
<IsRecording
key={`is-recording-${items.worker.sid}`}
worker={items.worker}
/>
);
}}
/>
);
I'm accomplishing this through reading from the redux store. Below has been modified so I don't reveal proprietary knowledge.
Create a custom listener for the Twilio reservationCreated event
manager.workerClient.on("reservationCreated", this.acceptTask);
The below fetches the updated task. If we used task directly it would be the stale object unless things have changed.
async acceptTask(reservation) {
const updatedTask = this.manager.store.getState().flex.worker.tasks.get(reservation.task.sid);
// consume it
// get the updated attributes with updatedTask.attributes
}
Hope that works for you / helps.

React Bootstrap OverlayTrigger with trigger="focus" bug work around

In iOS safari, OverlayTrigger with trigger="focus" isn't able to dismiss when tapping outside. Here is my code:
<OverlayTrigger
trigger="focus"
placement="right"
overlay={ <Popover id="popoverID" title="Popover Title">
What a popover...
</Popover> } >
<a bsStyle="default" className="btn btn-default btn-circle" role="Button" tabIndex={18}>
<div className="btn-circle-text">?</div>
</a>
</OverlayTrigger>
I know that this is a known bug for Bootstrap cuz this doesn't even work on their own website in iOS, but does anyone know any method to go around it? It would be the best if it is something that doesn't require jQuery, but jQuery solution is welcome. Thanks.
OK, since no one else gives me a work around, I worked on this problem with my co-worker together for 3 days, and we came up with this heavy solution:
THE PROBLEM:
With trigger="focus", Bootstrap Popover/Tooltip can be dismissed when CLICKING outside the Popover/Tooltip, but not TOUCHING. Android browsers apparently changes touches to clicks automatically, so things are fine on Android. But iOS safari and browsers that is based on iOS safari (iOS chrome, iOS firefox, etc...) don't do that.
THE FIX:
We found out that in React Bootstrap, the Overlay component actually lets you customize when to show the Popover/Tooltip, so we built this component InfoOverlay based on Overlay. And to handle clicking outside the component, we need to add event listeners for both the Popover/Tooltip and window to handle both 'mousedown' and 'touchstart'. Also, this method would make the Popover have its smallest width all the time because of the padding-right of the component is initially 0px, and we make based on the width of some parent component so that it is responsive based on the parent component. And the code looks like this:
import React, { Component, PropTypes as PT } from 'react';
import {Popover, Overlay} from 'react-bootstrap';
export default class InfoOverlay extends Component {
static propTypes = {
PopoverId: PT.string,
PopoverTitle: PT.string,
PopoverContent: PT.node,
// You need to add this prop and pass it some numbers
// if you need to customize the arrowOffsetTop, it's sketchy...
arrowOffsetTop: PT.number,
// This is to be able to select the parent component
componentId: PT.string
}
constructor(props) {
super(props);
this.state = {
showPopover: false,
popoverClicked: false
};
}
componentDidMount() {
// Here are the event listeners and an algorithm
// so that clicking popover would not dismiss itself
const popover = document.getElementById('popoverTrigger');
if (popover) {
popover.addEventListener('mousedown', () => {
this.setState({
popoverClicked: true
});
});
popover.addEventListener('touchstart', () => {
this.setState({
popoverClicked: true
});
});
}
window.addEventListener('mousedown', () => {
if (!this.state.popoverClicked) {
this.setState({
showPopover: false
});
} else {
this.setState({
popoverClicked: false
});
}
});
window.addEventListener('touchstart', () => {
if (!this.state.popoverClicked) {
this.setState({
showPopover: false
});
} else {
this.setState({
popoverClicked: false
});
}
});
// this is to resize padding-right when window resizes
window.onresize = ()=>{
this.setState({});
};
}
// This function sets the style and more importantly, padding-right
getStyle() {
if (document.getElementById(this.props.componentId) && document.getElementById('popoverTrigger')) {
const offsetRight = document.getElementById(this.props.componentId).offsetWidth - document.getElementById('popoverTrigger').offsetLeft - 15;
return (
{display: 'inline-block', position: 'absolute', 'paddingRight': offsetRight + 'px'}
);
}
return (
{display: 'inline-block', position: 'absolute'}
);
}
overlayOnClick() {
this.setState({
showPopover: !(this.state.showPopover)
});
}
render() {
const customPopover = (props) => {
return (
{/* The reason why Popover is wrapped by another
invisible Popover is so that we can customize
the arrowOffsetTop, it's sketchy... */}
<div id="customPopover">
<Popover style={{'visibility': 'hidden', 'width': '100%'}}>
<Popover {...props} arrowOffsetTop={props.arrowOffsetTop + 30} id={this.props.PopoverId} title={this.props.PopoverTitle} style={{'marginLeft': '25px', 'marginTop': '-25px', 'visibility': 'visible'}}>
{this.props.PopoverContent}
</Popover>
</Popover>
</div>
);
};
return (
<div id="popoverTrigger" style={this.getStyle()}>
<a bsStyle="default" className="btn btn-default btn-circle" onClick={this.overlayOnClick.bind(this)} role="Button" tabIndex={13}>
<div id="info-button" className="btn-circle-text">?</div>
</a>
<Overlay
show={this.state.showPopover}
placement="right"
onHide={()=>{this.setState({showPopover: false});}}
container={this}>
{customPopover(this.props)}
</Overlay>
</div>
);
}
}
In the end, this is a heavy work around because it is a big amount of code for a fix, and you can probably feel your site is slowed down by a tiny bit because of the 4 event listeners. And the best solution is just tell Bootstrap to fix this problem...

Knockout Binding Not Working with jQueryUI Dialogue

My viewModel has an array called 'Items'. I want to display the contents of 'Items' using a foreach binding. Everything works fine when I use regular HTML. But does not work with a dialogue box which I created using jQueryUI.
HTML:
<div id="skus0">
<div id="skus1">
<ul data-bind="foreach: Items">
<li data-bind="text:Name"></li>
</ul>
</div>
<input type="button" id="openQryItems" class="btn btn-info" value="Open" data-bind="click:openQueryItems" />
</div>
JavaScript:
// my view model
var viewModel = {
Items: [{Name:'Soap'},{Name:'Toothpaste'}]
};
// JS to configure dialogue
$("#skus1").dialog({
autoOpen: false,
width: 500,
modal: true,
buttons: {
"OK": function () {
$(this).dialog("close");
},
"Cancel": function () {
$(this).dialog("close");
}
}
});
// for mapping my model using ko.mapping plugin
var zub = zub || {};
zub.initModel = function (model) {
zub.cycleCountModel = ko.mapping.fromJS(model);
zub.cycleCountModel.openQueryItems = function () {
$("#skus1").dialog("open");
}
ko.applyBindings(zub.cycleCountModel, $("#skus0")[0]);
}
zub.initModel(viewModel);
I have created a fiddle here my fiddle
$.fn.dialog removes the element from its place in the DOM and places it in a new container; this is how it can create a floating window. The problem with this happening is that it breaks data binding, since the dialog DOM is no-longer nested within the top-level data-bound DOM.
Moving the dialog initialization to after ko.applyBindings will enable dialog to yank stuff out of the DOM after the list is populated. Of course, this means that after that point, future changes will still not be reflected, which may be important if you're wanting the opened dialog to change automatically.
If you are wanting the dialog contents to be fully dynamic, you could create a binding handler; we did this in our project. Here's a rough outline of how we did this:
ko.bindingHandlers.dialog = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingCtx) {
var bindingValues = valueAccessor();
var hasAppliedBindings = false;
var elem = $(element);
var options = {
id: ko.utils.unwrapObservable(bindingValues.id),
title: ko.utils.unwrapObservable(bindingValues.title),
// etc...
onOpen: function () {
if (!hasAppliedBindings) {
hasAppliedBindings = true;
var childCtx = bindingCtx.createChildContext(viewModel);
ko.applyBindingsToDescendants(childCtx, element);
}
}
};
elem.dialog(options);
}
return { controlsDescendantBindings: true };
}
...which we used like this:
<div data-bind="dialog: { title: 'some title', id: 'foo', ... }">
<!-- dialog contents -->
</div>
What return { controlsDescendantBindings: true } does is makes sure that outer bindings do not affect anything using the dialog binding handler. Then we create our own Knockout binding "island" after it is pulled out of the DOM, based on the original view model.
Although in our project we also used hybrid jQuery+Knockout, I would highly recommend you avoid this whenever possible. There were so many hacks we had to employ to sustain this type of application. The very best thing you should do is prefer Knockout binding handlers (and I think it has a "component" concept now which I haven't played with) over DOM manipulations to avoid buggy UI management.

React Native: Can you force refreshing the App or rerendering the page onPress of a button?

I have an app with SignInPage component and Main App component.
If the user is signed in, the App will render MainApp. Otherwise, SignInPage will be rendered.
On press of Sign In button from SignInPage, I would like to refresh or re render the page so that it redirects to the MainApp component.
Are there any ways to do this ? Or any workaround for such routing ?
So there's two ways that I can think of doing this. One would be to have the app always try to load the sign-in page at the start of the app. Then in the sign-in component, do something like:
componentDidMount: function() {
if (currentUserStore.getUser()) {
return this.props.navigator.push({
title: 'Home',
component: MainApp
});
}
}
So what this does is check to see if you have a user logged in. If you do, then it navigates to the MainApp scene, otherwise it does nothing and renders the sign-in normally.
Another way to do it is using state. So you can make a Home component like:
var Home = React.createClass({
getInitialState: function() {
return {
loggedIn: false
};
},
componentDidMount: function() {
if (currentUserStore.getUser()) {
return this.setState({
loggedIn: true
});
}
},
render: function() {
if this.state.loggedIn
<MainApp ... />
else
<SignIn ... />
}
});
Then in your SignIn part of the component, make sure to setState to true when the user logs in successfully.

Ember.js + JQuery-UI Tooltip - Tooltip does not reflect the model / controller changes

Context
I have a small Ember app, which, amongst other things, displays a number of connected users and, when hovering an element of the page, their names as a list.
All in all, it works quite well. The applications pulls data from a REST endpoint every two minutes, as the backend didn't allow for pushing data.
The contents of the tooltip are computed in the Controller, with a function that basically concatenate strings in various ways according to the context. Then it's bound to a data attribute of the <img> the tooltip is created on. When the View is ready and didInsertElement is fired, the tooltip is generated (if needs be) based on this data-bindattr value.
Question
When new data is pulled from the backend, everything is updated accordingly, except the tooltip content. (When browsing the page's DOM, the data-bindattr value is updated too.)
What could cause the tooltip to not refresh? Is it a case of JQuery-UI not calculating it again?
Some code
Refreshing code in the app's controller:
Monitor.ApplicationController = Ember.ArrayController.extend({
itemController: 'process',
sortProperties: ['name'],
sortAscending: true,
intervalId: undefined,
startRefreshing: function() {
var self = this;
if (self.get('intervalId')) {
return;
}
self.set( 'intervalId', setInterval(function() {
self.store.find('process');
}, 120000 ));
}
});
View: Process.hbs
<div {{bind-attr class=":inline inactive:inactive"}}>
<img {{bind-attr src=icon}} {{bind-attr data-caption=contentText}} class="caption" />
<div class="counter">{{nbUsers}}</div>
</div>
View: ProcessView
Monitor.ProcessView = Ember.View.extend({
// (...) Various stuff.
didInsertElement: function() {
this.updateTooltip();
},
updateTooltip: function() {
console.log('Inside updateTooltip!');
if (!this.$()) {return;}
if (this.get('controller').get('inactive')) {
this.$().tooltip({items: '.caption', disabled: true});
return;
}
this.$().tooltip({
items: '.caption',
tooltipClass: 'tooltip',
content: function() {
return $(this).data('caption');
},
position: {
my: 'left+15px center',
at: 'right center',
collision: 'flip'
},
show: false,
hide: false
});
}.observes('controller.inactive', 'controller.contentText')
});
Controller: ProcessController
Monitor.ProcessController = Ember.ObjectController.extend({
contentText: function() {
var tooltipContent = '';
this.get('containers').forEach(function(container) {
// Do a lot of things to tooltipContent involving:
// container.get('name')
// container.get('text')
// container.get('size')
// container.get('nbUsers')
// The data-bindattr value refreshes correctly so I cut this out for readability.
return tooltipContent;
}.property('name', 'containers.#each')
});
Edit 1:
Replaced 'containers.#each' by 'contentText' in the observer and added logging.
Here's what I think is happening:
Your tooltip library isn't observing the data-caption attribute. Meaning, when you update the attribute, you have to explicitly tell the library to update the tooltip as well. So although your attribute is updating just fine, the tooltip library isn't actually watching for those updates.
This can be remedied by calling updateTooltip, which you do, in didInsertElement. However, didInsertElement only fires once, when the element is first inserted. It's not called when the content changes.
Those two things combined are, I think, causing your problem. I think that all you need to do is have updateTooltip also observe the controller.contextText property. Then it should be called when the text updates.
So it turns out my codes declares and initialize a tooltip, but once it's done, you can't change the content the same way. Plus it adds unneeded computing anyway.
Thanks to #GJK's answer and that question, I found out what was happening. Turns out you need to set the content of the tooltip to refresh it, not recreate it.
Here is the working code for Ember integration:
Monitor.ProcessView = Ember.View.extend({
// Other stuff
didInsertElement: function() {
this.initTooltip();
},
initTooltip: function() {
if (!this.$()) {return;}
if (this.get('controller').get('inactive')) {
this.$().tooltip({items: '.caption', disabled: true});
return;
}
this.$().tooltip({
items: '.caption',
tooltipClass: 'tooltip',
content: function() {
return $(this).data('caption');
},
position: {
my: 'left+15px center',
at: 'right center',
collision: 'flip'
},
show: false,
hide: false
});
},
updateTooltip: function() {
if (!this.$()) {return;}
if (this.get('controller').get('inactive')) {
this.$().tooltip({items: '.caption', disabled: true});
return;
}
content = this.get('controller').get('contentText');
this.$().tooltip("option", "content", content);
}.observes('controller.contentText')
});
As an added bonus, you can avoid using the data attribute as a buffer now, although I'm not sure why.

Resources