I have an editor on a page and save button. I want save button only to appear if there are changes (since last save). So, when you save, i set this.setState({hasChanges: false}) and in onChange function i set hasChanges to true.
And that's all working fine. The issue is that editor will fire onChange event when editor gain focus (but content is not changed yet). And, that is expected behaviour according to their documentation
The event listeners within the component will observe focus changes
and propagate them through onChange as expected, so state and DOM will
remain correctly in sync.
Is there some way to know if content has changed inside onChange method, or only selection is changed?
I posted this answer to your question on github earlier today, but I'll add it here as well for future reference:
You're right in that onChange is called every time the editorState changes. And since the editorState holds both the selectionState and the contentState, it will change both when there's a content edit, and when the selection/focus changes.
If you want to know if the change that triggered the onChange method was in the contentState or in the selectionState, you could compare them with their old values:
function onChange(newState) {
const currentContentState = this.state.editorState.getCurrentContent()
const newContentState = newState.getCurrentContent()
if (currentContentState !== newContentState) {
// There was a change in the content
} else {
// The change was triggered by a change in focus/selection
}
}
Now, if you know that the change was in the contentState, you can get some more info by calling newState.getLastChangeType(). It should return any of these values (unless you, or some plugin you've added, have created new change types).
However, sometimes just the conditional if (currentContentState !== newContentState) not work and not detect the change for cases like when you to modify the content state using Entity.mergeData together with forceSelection because this change is stored inside entity and is not exposed in contentState.
So you could do something like that additionaly.
this.currentEntityMap = null;
function onChange(newEditorState) {
const currentContentState = editorState.getCurrentContent();
const newContentState = newEditorState.getCurrentContent();
const newContentStateRaw = convertToRaw(newContentState);
const newEntityMap = newContentStateRaw ? newContentStateRaw.entityMap : null;
//so if you have the two entity maps, you can to compare it, here an example function.
const isEntityMapChanged = this.isEntityMapChanged(newEntityMap);
this.currentEntityMap = newEntityMap;
}
isEntityMapChanged(newEntityMap) {
let isChanged = false;
if (this.currentEntityMap) {
loop1:
for (const index of Object.keys(newEntityMap)) {
if (this.currentEntityMap[index] && newEntityMap[index].type === this.currentEntityMap[index].type) {
loop2:
for (const prop of Object.keys(newEntityMap[index].data)) {
if (newEntityMap[index].data[prop] !== this.currentEntityMap[index].data[prop]) {
isChanged = true;
break loop1;
}
}
} else {
isChanged = true;
break loop1;
}
}
}
return isChanged;
}
then to used the flag isEntityMapChanged and the first conditional for to do the save.
you should use code follow:
const onChange = newState => {
if (
// when content change or line style change will change the state
!newState.getCurrentContent().equals(editorState.getCurrentContent()) ||
!newState
.getCurrentInlineStyle()
.equals(editorState.getCurrentInlineStyle())
) {
setEditorState(newState);
}
};
if you just use content equal ,then line style change will not on effect.
Related
I am very new to react. I am currently creating a game and trying to detect if the current turn has changed. I am wondering if there is a simple way to do this.
let hasTurnChanged = props.turn % 2 == 1;
function chooseBestPrice() {
// start by seeing if prices fluctuate every turn
let currBestPrice = props.price;
console.log(hasTurnChanged);
if(hasTurnChanged){
currBestPrice = fluctuatePrice(props.price);
}
return currBestPrice;
}
When I click a button called Turn the prices are suppose to change.
Assuming you're trying to detect a prop come from parent component, useEffect could help with this.
All we need to do is put the prop into the dependencies array of useEffect.
const ChildComponent = (props) => {
useEffect(() => {
// call the function here
}, [props.price])
// ...other code
}
See the official document for more information.
I am trying to create a code to fill a form in which the options of some selectors depend on what is selected in a previous selector. I am using the jQuery library to create the events.
I would like to be able to make the event be able to observe the variables even when the code that creates the event has already finished.
Below is an example of what I need to do.
function preValForm() {
App.get('params').then((params) => {
let devices = []
params.devices.forEach(i => { // Here is the params Object { devices: [[...]] , stock: [[...]] }
if (i[0] == "Mobile" && devices.indexOf(i[1]) == -1) { // Not insert duplicates to the array
devices.push(i[1]) // Insert value to array if isn't duplicate
}
})
$('#listDevices').html(devices.map(d => {
return `<option value="${d}">${d}</option>`
}).join('')) // Insert option values to a select element in DOM
// Here is the problem ...
$('#listDevices').on('change', (ev) => { // Create the event when selector #listDevices changes
let stock = [] // Declare variable stock
params.stock.forEach(s => { // ! params is not defined **! -- This is the error**
if (s[1] == ev.target.value && stock.indexOf(s[1]) == -1) {
stock.push(s[1])
}
})
$('#stockOptions').html(stock.map(k => {
return `<option value="${k}">${k}</option>`
}).join(''))
})
})
}
I have seen that this can be done, for example with the SweetAlert2 library you see this behavior where you define an event called preConfirm and when executed the variables are still visible for that event. I would like to be able to do something similar to that behavior, that when I make a change in the selection the event is executed and that event recognizes my variable 'params'.
Thanks
I had a class component named <BasicForm> that I used to build forms with. It handles validation and all the form state. It provides all the necessary functions (onChange, onSubmit, etc) to the inputs (rendered as children of BasicForm) via React context.
It works just as intended. The problem is that now that I'm converting it to use React Hooks, I'm having doubts when trying to replicate the following behavior that I did when it was a class:
class BasicForm extends React.Component {
...other code...
touchAllInputsValidateAndSubmit() {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
let inputs = {};
for (let inputName in this.state.inputs) {
inputs = Object.assign(inputs, {[inputName]:{...this.state.inputs[inputName]}});
}
// TOUCH ALL INPUTS
for (let inputName in inputs) {
inputs[inputName].touched = true;
}
// UPDATE STATE AND CALL VALIDATION
this.setState({
inputs
}, () => this.validateAllFields()); // <---- SECOND CALLBACK ARGUMENT
}
... more code ...
}
When the user clicks the submit button, BasicForm should 'touch' all inputs and only then call validateAllFields(), because validation errors will only show if an input has been touched. So if the user hasn't touched any, BasicForm needs to make sure to 'touch' every input before calling the validateAllFields() function.
And when I was using classes, the way I did this, was by using the second callback argument on the setState() function as you can see from the code above. And that made sure that validateAllField() only got called after the state update (the one that touches all fields).
But when I try to use that second callback parameter with state hooks useState(), I get this error:
const [inputs, setInputs] = useState({});
... some other code ...
setInputs(auxInputs, () => console.log('Inputs updated!'));
Warning: State updates from the useState() and useReducer() Hooks
don't support the second callback argument. To execute a side effect
after rendering, declare it in the component body with useEffect().
So, according to the error message above, I'm trying to do this with the useEffect() hook. But this makes me a little bit confused, because as far as I know, useEffect() is not based on state updates, but in render execution. It executes after every render. And I know React can queue some state updates before re-rendering, so I feel like I don't have full control of exactly when my useEffect() hook will be executed as I did have when I was using classes and the setState() second callback argument.
What I got so far is (it seems to be working):
function BasicForm(props) {
const [inputs, setInputs] = useState({});
const [submitted, setSubmitted] = useState(false);
... other code ...
function touchAllInputsValidateAndSubmit() {
const shouldSubmit = true;
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
let auxInputs = {};
for (let inputName in inputs) {
auxInputs = Object.assign(auxInputs, {[inputName]:{...inputs[inputName]}});
}
// TOUCH ALL INPUTS
for (let inputName in auxInputs) {
auxInputs[inputName].touched = true;
}
// UPDATE STATE
setInputs(auxInputs);
setSubmitted(true);
}
// EFFECT HOOK TO CALL VALIDATE ALL WHEN SUBMITTED = 'TRUE'
useEffect(() => {
if (submitted) {
validateAllFields();
}
setSubmitted(false);
});
... some more code ...
}
I'm using the useEffect() hook to call the validateAllFields() function. And since useEffect() is executed on every render I needed a way to know when to call validateAllFields() since I don't want it on every render. Thus, I created the submitted state variable so I can know when I need that effect.
Is this a good solution? What other possible solutions you might think of? It just feels really weird.
Imagine that validateAllFields() is a function that CANNOT be called twice under no circunstances. How do I know that on the next render my submitted state will be already 'false' 100% sure?
Can I rely on React performing every queued state update before the next render? Is this guaranteed?
I encountered something like this recently (SO question here), and it seems like what you've come up with is a decent approach.
You can add an arg to useEffect() that should do what you want:
e.g.
useEffect(() => { ... }, [submitted])
to watch for changes in submitted.
Another approach could be to modify hooks to use a callback, something like:
import React, { useState, useCallback } from 'react';
const useStateful = initial => {
const [value, setValue] = useState(initial);
return {
value,
setValue
};
};
const useSetState = initialValue => {
const { value, setValue } = useStateful(initialValue);
return {
setState: useCallback(v => {
return setValue(oldValue => ({
...oldValue,
...(typeof v === 'function' ? v(oldValue) : v)
}));
}, []),
state: value
};
};
In this way you can emulate the behavior of the 'classic' setState().
I have tried to solve it using the useEffect() hook but it didn't quite solve my problem. It kind of worked, but I ended up finding it a little too complicated for a simple task like that and I also wasn't feeling sure enough about how many times my function was being executed, and if it was being executed after the state change of not.
The docs on useEffect() mention some use cases for the effect hook and none of them are the use that I was trying to do.
useEffect API reference
Using the effect hook
I got rid of the useEffect() hook completely and made use of the functional form of the setState((prevState) => {...}) function that assures that you'll get a current version of your state when you use it like that. So the code sequence became the following:
// ==========================================================================
// FUNCTION TO HANDLE ON SUBMIT
// ==========================================================================
function onSubmit(event){
event.preventDefault();
touchAllInputsValidateAndSubmit();
return;
}
// ==========================================================================
// FUNCTION TO TOUCH ALL INPUTS WHEN BEGIN SUBMITING
// ==========================================================================
function touchAllInputsValidateAndSubmit() {
let auxInputs = {};
const shouldSubmit = true;
setInputs((prevState) => {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
for (let inputName in prevState) {
auxInputs = Object.assign(auxInputs, {[inputName]:{...prevState[inputName]}});
}
// TOUCH ALL INPUTS
for (let inputName in auxInputs) {
auxInputs[inputName].touched = true;
}
return({
...auxInputs
});
});
validateAllFields(shouldSubmit);
}
// ==========================================================================
// FUNCTION TO VALIDATE ALL INPUT FIELDS
// ==========================================================================
function validateAllFields(shouldSubmit = false) {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
let auxInputs = {};
setInputs((prevState) => {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
for (let inputName in prevState) {
auxInputs =
Object.assign(auxInputs, {[inputName]:{...prevState[inputName]}});
}
// ... all the validation code goes here
return auxInputs; // RETURNS THE UPDATED STATE
}); // END OF SETINPUTS
if (shouldSubmit) {
checkValidationAndSubmit();
}
}
See from the validationAllFields() declaration that I'm performing all my code for that function inside a call of setInputs( (prevState) => {...}) and that makes sure that I'll be working on an updated current version of my inputs state, i.e: I'm sure that all inputs have been touched by the touchAllInputsValidateAndSubmit() because I'm inside the setInputs() with the functional argument form.
// ==========================================================================
// FUNCTION TO CHECK VALIDATION BEFORE CALLING SUBMITACTION
// ==========================================================================
function checkValidationAndSubmit() {
let valid = true;
// THIS IS JUST TO MAKE SURE IT GETS THE MOST RECENT STATE VERSION
setInputs((prevState) => {
for (let inputName in prevState) {
if (inputs[inputName].valid === false) {
valid = false;
}
}
if (valid) {
props.submitAction(prevState);
}
return prevState;
});
}
See that I use that same pattern of the setState() with functional argument call inside the checkValidationAndSubmit() function. In there, I also need to make sure that I'm get the current, validated state before I can submit.
This is working without issues so far.
I am having trouble checking the active state of an element attribute. I tried below but it returned false even though the element has the attribute in an active state - (.c-banner.active is present)
ngAfterViewInit() {
const bannerElm = document.getElementById("banner");
const isActive = bannerElm && bannerElm.getAttribute("class")
.indexOf("c-banner.active") !== -1; // returned false
}
Why you not use classList and contains?
classList.contains();
ngAfterViewInit() {
const bannerElm = document.getElementById("banner");
const isActive = bannerElm && bannerElm.classList.contains('c-banner') && bannerElm.classList.contains('active');
}
You can use classList.contains method to check if the element has the active class.
ngAfterViewInit() {
setTimeout(() => {
const isActive = bannerElm &&
bannerElm.classList.contains('c-banner') &&
bannerElm.classList.contains('active');
}, 1000);
}
[UPDATED] wrap it within setTimeout() and it worked! In case anyone else is stuck with the component initialization orders issue that I had previously.
Aside from what's the best way of actually accessing the value of the class attribute, judging by your comments the issue appears to be of asynchronous nature.
To avoid using a hackish setTimeout solution, I would recommend to apply a mutation observer and react to changes of the attribute accordingly.
Here's one way on how to do it.
PS: Edited the answer to make it more suitable for what you're trying to achieve. In the end this won't make a huge difference other than that in case the banner state is changed before the debounce time runs out, the subject emits right away and you potentially save some time waiting as compared to using setTimeout
bannerIsActive$ = new BehaviorSubject<boolean>(false);
ngAfterViewInit() {
const banner = document.getElementById('banner');
const observer = new MutationObserver((mutations: MutationRecord[]) => {
const mutation = mutations[0];
const classList: DOMTokenList = mutation.target['classList'];
this.bannerIsActive$.next(mutation.attributeName === 'class' && classList.contains('active'));
});
observer.observe(banner, {
attributes: true
});
this.bannerIsActive$.pipe(
debounce(isActive => timer(isActive ? 0 : 1000)),
take(1)
).subscribe(isActive => {
// do something with the banner state
});
}
you should work with ViewChild rather then Access your DOM directly.
both will work, but this is the strict way to do it.
#ViewChild('banner') input;
ngAfterViewInit() {
var classAttributes= this.input.nativeElement.getAttribute('class');
var result=classAttributes&&classAttributes('c-banner')
}
I am using ag-Grid onCellEditingStopped event handler to get the changed value of a grid cell.
onCellEditingStopped: function(event) {
// event.value present the current cell value
console.log('cellEditingStopped');
}
But it does not provide the previous value (the value before the change happens). Is there anyway to get the previous value ?
My current solution:
I am using onCellEditingStarted event to store the current cell value in a separate variable and use that variable inside the onCellEditingStopped event handler function. But it is not a clear solution.
Thanks
you can use value Setter function for that column as below.
valueSetter: function (params) {
console.log(params.oldValue);
console.log(params.newValue);
if (params.oldValue !== params.newValue) {
//params.data["comments"] = params.newValue;
return true;
}
else {
return false;
}
}
I'm using onCellEditingStopped in my Angular application, with agGrid v24.1... and it does have an oldValue and a newValue value in there.
onCellEditingStopped = (_params) => {
if (_params.newValue != _params.oldValue) {
// Do something...
}
}