Disable and re-enable button on single action - javascript

I need to disable and re-enable a button during the async call. I am only able to disable it. If I add code to re-enable it is ignored. I acknowledge I may not be asking the right question.
I have a function with a button "Action":
<button className={`doFoo${buttonClasses[type]} ${type}`} onClick={onClick} disabled={isButtonDisabled}>
That is called by a React class "Actions":
<Action type="like" onClick={onLike} isButtonDisabled={isButtonDisabled} />
That is called by another React class "List":
<Actions onLike={this.handleLike} onDislike={this.handleDislike} isButtonDisabled={isButtonDisabled}/>
Also in that class is are the following functions:
...
thumbsUp() {
const {
...
} = this.props;
const {
...
} = this.state;
this.setState({ wasLiked: true, viewProfile: false }, () => {
setTimeout(doThumbsUp, ACTION_CONFIRMATION_ANIMATION_TIMEOUT);
});
function doThumbsUp() {
thumbsUp({
index: activeIndex,
id: profiles[activeIndex].id
});
}
},
handleLike() {
const { showThumbsUpConfirmation } = this.props;
if (showThumbsUpConfirmation) {
this.showThumbsUpConfirmationModal();
} else {
this.thumbsUp();
}
},
...
Here's what the source looks like:
export function thumbsUp({ id, ... }) {
return api.post(`${API.ENDPOINTS.FOO}/${id}/thumbs_up`, {
...
});
}
I can place this.setState(isButtonDisabled: true) at various places in this code and that value makes it to the view and disables the button. However I cannot figure out how to re-enable the button after the work has been done.

If I'm understanding you correctly you want the button to be disabled during async and after async be enabled? If that is the case, wherever you are calling the function that makes the api call, you just need to chain a .then(() => this.setState(isButtonDisabled: false) and that will update the state as soon as response has been received from api call. also if you aren't using es6 just make sure to set this to a variable above the api call to ensure its scoped properly for setState

Related

How to use async validator on submit with element plus in Vue 3?

Context description
I have a Vue 3 project and using element plus, I created a form with async validation. The code of validation function (Laravel precognition):
const validatePrecognitive = async (rule: InternalRuleItem, value: Value, callback: (error?: string | Error) => void) => {
return new Promise<void>((resolve, reject) => {
if (!rule.field) {
return;
}
precognitive.post('/events', computedFormData.value, {
validate: [rule.field],
onPrecognitionSuccess(response) {
if (response.status === 204) {
callback();
resolve();
}
},
onValidationError(response) {
callback(new Error(response.data.message));
reject();
},
});
});
};
and I define the rules like this:
const rules: FormRules = reactive({
} as Partial<Record<string, Arrayable<FormItemRule>>>);
// dynamically set the rules for each input, because of same value for all rules
Object.keys(getSubmitShape()).forEach(key => {
rules[key] = [{ asyncValidator: validatePrecognitiveDebounced, trigger: 'blur' }];
});
The getSubmitShape() funtion return Object with all form-item keys, that should be validated. So you can imagine it like:
const rules: FormRules = reactive({
rules['name'] = [{ asyncValidator: validatePrecognitive, trigger: 'blur' }];
rules['surname'] = [{ asyncValidator: validatePrecognitive, trigger: 'blur' }];
}
This works like a charm, when I write to inputs and switch between them. The errors would appear and dissappear correctly.
Submit
When the form is submitted, I am calling formEl.validate(...) function, which should validate all the inputs. It works, BUT:
for every rule item, one HTTP request is sent to server, because the validation method is called for each rule separately.
What I tried
I tried to implement debounce method, which would save the params from each previous function call and include that in next call, so in a last call, I would have all the inputs, rules and callbacks available. This works, when I try it while writing inputs to elements and switching them fast (triggering validation).
When on submit, it waits the debounce time for each rule. So the requests again go one by one with the delay of debounce time.
Any ideas, how I could make this in one request?
Thanks.

Leaflet geoman "Button with this name already exists" error when creating a new button in react

I am trying to create a custom button for arrows in the drawing tool of leaflet-geoman.
The idea was to work with the copyDrawControl function, and to use Line as a model to make Polylines with arrow tips.
I wrote a code mostly inspired from this demonstration https://codesandbox.io/s/394eq?file=/src/index.js and modified it for my goals. Here is the code :
import { useEffect } from "react";
import { useLeafletContext } from "#react-leaflet/core";
import "#geoman-io/leaflet-geoman-free";
import "#geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css";
const Geoman = () => {
const context = useLeafletContext();
useEffect(() => {
const leafletContainer = context.layerContainer || context.map;
leafletContainer.pm.setGlobalOptions({ pmIgnore: false });
//draw control options
leafletContainer.pm.addControls({
positions: {
draw: 'topleft',
edit: 'topright',
},
drawMarker: false,
rotateMode: false,
cutPolygon: false,
position: "bottomright"
});
//new button
leafletContainer.pm.Toolbar.copyDrawControl('Line', {
name: 'SoonToBeArrow',
block: 'draw',
title: 'Display text on hover button',
actions: [
// uses the default 'cancel' action
'cancel',
],
});
return () => {
leafletContainer.pm.removeControls();
leafletContainer.pm.setGlobalOptions({ pmIgnore: true });
};
}, [context]);
return null;
};
export default Geoman;
When trying to add the copyDrawControl, I faced a bug that would announce that "Button with this name already exists"
I suspect its because I add the button inside a useEffect that gets called several times, but it's also the only way to access leafletContainer, since it must be updated everytime the context changes.
I tried creating another useEffect that contains the same context and my new button, but it did not work.
Does anyone have any suggestion on how to solve this ?
Thnak you in advance
You only want to run this effect once, just after context becomes available. In order to do this, we can make a state variable to track whether or not you've already added the control:
const Geoman = () => {
const context = useLeafletContext();
const [added, setAdded] = useState(false);
useEffect(() => {
const leafletContainer = context.layerContainer || context.map;
// if the context is ready, and we've not yet added the control
if (leafletContainer && !added){
leafletContainer.pm.setGlobalOptions({ pmIgnore: false });
//draw control options
leafletContainer.pm.addControls({
// ...
});
//new button
leafletContainer.pm.Toolbar.copyDrawControl('Line', {
// ...
});
// register that we've already added the control
setAdded(true);
}
return () => {
leafletContainer.pm.removeControls();
leafletContainer.pm.setGlobalOptions({ pmIgnore: true });
};
}, [context]);
return null;
};
In this way, you effect will run whenever context changes - once context is ready, you add the control. You register that you've added the control, but then your if statement will make sure that further changes in context will not try to keep adding controls again and again.
BTW, a second option to using leaflet geoman with react leaflet is to use the official createControlComponent hook to create custom controls. This is not at all straightforward with leaflet-geoman, as createControlComponent requires you to feed it an instance of an L.Control that has all the required hooks and initializer methods. geoman does not have these - it is quite different in the way it initializes and adds to a map. However, you can create an L.Control from geoman methods, and then feed it to createControlComponent.
Create the L.Control:
/**
* Class abstraction wrapper around geoman, so that we can create an instance
* that is an extension of L.Control, so that react-leaflet can call all
* L.PM methods using the expected L.Control lifecycle event handlers
*/
const GeomanControl = L.Control.extend({
initialize(options: Props) {
L.PM.setOptIn(options.optIn ?? false);
L.setOptions(this, options);
},
addTo(map: L.Map) {
const { globalOptions, events } = this.options;
// This should never happen, but its better than crashing the page
if (!map.pm) return;
map.pm.addControls(toolbarOptions);
map.pm.setGlobalOptions({
pmIgnore: false,
...globalOptions,
});
// draw control options
map.pm.addControls({
// ...
});
// new button
map.pm.Toolbar.copyDrawControl('Line', {
// ...
});
Object.entries(events ?? {}).forEach(([eventName, handler]) => {
map.on(eventName, handler);
});
},
});
Then simply use createControlComponent
const createControl = (props) => {
return new GeomanControl(props);
};
export const Geoman = createControlComponent(createControl);
You can add quite a lot of logic into the addTo method, and base a lot of its behaviors off the props you feed to <Geoman />. This is another flexible way of adapting geoman for react-leaflet v4.

How to use ReactJS to give immediate feedback to user before a long operation?

NOTE: Marking Michael Cox's answer below correct, but you should read the discussion underneath the answer as well, which illustrates that componentDidMount is apparently called BEFORE browser render.
I have a ReactJS app, and when the user clicks a button, I'd like to change the button text to "Working..." so that the user can see that the app is working on their request. AFTER the updated button has been rendered, then and only then, do I want to actually start the operation.
My first attempt was to use componentDidUpdate. AFAIK, it shouldn't be called until AFTER rendering. But here is my attempt, and you can see the button never changes (open up console to help it take long enough). https://jsfiddle.net/joepinion/0s78e2oj/6/
class LongOperationButton extends React.Component {
constructor(props) {
super(props);
this.state = {
opInProgress: false,
};
}
render() {
let btnTxt = "Start Operation";
if(this.state.opInProgress) {
btnTxt = "Working...";
}
return(
<button
onClick={()=>this.startOp()}
>
{btnTxt}
</button>
);
}
startOp() {
if(!this.state.opInProgress) {
this.setState({opInProgress: true});
}
}
componentDidUpdate() {
if(this.state.opInProgress) {
this.props.op();
this.setState({opInProgress: false});
}
}
}
function takeALongTime() {
for(let i=1;i<=2000;i++) {
console.log(i);
}
}
ReactDOM.render(
<LongOperationButton op={takeALongTime} />,
document.getElementById('container')
);
Next attempt I tried using the callback of setState. Seems less correct because it bypasses componentShouldUpdate, but worth a shot: https://jsfiddle.net/joepinion/mj0e7gdk/14/
Same result, the button doesn't update.
What am I missing here???
laurent's comment is dead on. You're code is executing this.props.op, then immediately updating the state. TakeALongTime needs to signal when it's done.
function takeALongTime() {
return new Promise(resolve => {
setTimeout(resolve, 2000);
});
}
and componentDidUpdate needs to wait until op is done before setting the state.
componentDidUpdate() {
if(this.state.opInProgress) {
this.props.op().then(() => {
this.setState({opInProgress: false});
});
}
}
(This is going off of your first example.)
You could also do this with callbacks.
function takeALongTime(cb) {
setTimeout(cb, 1000);
}
componentDidUpdate() {
if(this.state.opInProgress) {
var parentThis = this;
this.props.op(function() {
parentThis.setState({opInProgress: false});
});
}
}

Framework 7 Vue how to stop Firebase from listening to changes when on different pages?

Suppose I have pageA where I listen for a firebase document changes
export default {
mounted() {
this.$f7ready(() => {
this.userChanges();
})
},
methods: {
userChanges() {
Firebase.database().ref('users/1').on('value', (resp) => {
console.log('use data has changed');
});
}
}
}
Then I go to pageB using this..$f7.views.current.router.navigate('/pageB/')
If on pageB I make changes to the /users/1 firebase route I see this ,message in the console: use data has changed, even though I'm on a different page.
Any way to avoid this behavior, maybe unload the page somehow?
I tried to stop the listener before navigating away from pageA using Firebase.off() but that seems to break a few other things.
Are you properly removing the listener for that specific database reference? You'll have to save the referenced path on a dedicated variable to do so:
let userRef
export default {
mounted() {
this.$f7ready(() => {
this.userChanges();
})
},
methods: {
userChanges() {
userRef = Firebase.database().ref('users/1')
userRef.on('value', (resp) => {
console.log('use data has changed');
});
},
detachListener() {
userRef.off('value')
}
}
}
That way you only detach the listener for that specific reference. Calling it on the parent, would remove all listeners as the documentation specifies:
You can remove a single listener by passing it as a parameter to
off(). Calling off() on the location with no arguments removes all
listeners at that location.
More on that topic here: https://firebase.google.com/docs/database/web/read-and-write#detach_listeners

how to add returned data to the existing template

I am using ember. I intercept one component's button click in controller. The click is to trigger a new report request. When a new report request is made, I want the newly made request to appear on the list of requests that I currently show. How do I make ember refresh the page without obvious flicker?
Here is my sendAction code:
actions: {
sendData: function () {
this.set('showLoading', true);
let data = {
startTime: date.normalizeTimestamp(this.get('startTimestamp')),
endTime: date.normalizeTimestamp(this.get('endTimestamp')),
type: constants.ENTERPRISE.REPORTING_PAYMENT_TYPE
};
api.ajaxPost(`${api.buildV3EnterpriseUrl('reports')}`, data).then(response => {
this.set('showLoading', false);
return response.report;
}).catch(error => {
this.set('showLoading', false);
if (error.status === constants.HTTP_STATUS.GATEWAY_TIMEOUT) {
this.notify.error(this.translate('reports.report_timedout'),
this.translate('reports.report_timedout_desc'));
} else {
this.send('error', error);
}
});
}
There are few think you should consider. Generaly you want to have variable that holds an array which you are render in template in loop. For example: you fetch your initial set of data in route and pass it on as model variable.
// route.js
model() { return []; }
// controller
actions: {
sendData() {
foo().then(payload => {
// important is to use pushObjects method.
// Plain push will work but wont update the template.
this.get('model').pushObjects(payload);
});
}
}
This will automatically update template and add additional items on the list.
Boilerplate for showLoading
You can easily refactor your code and use ember-concurency. Check their docs, afair there is example fitting your usecase.

Categories

Resources