Issues with nodes returned by querySelectorAll - javascript

I am implementing an outside click hooks by class name
const useClickOutside = (className, f) => {
function handleClickOutside(event) {
if(event.which !== 1) return
const nodes = document.querySelectorAll(className)
console.log(nodes.length) // display the right length
console.log(nodes) // display the right elements
nodes.some((node) => { // falls
let outside = !node.contains(event.target)
if(outside) { f(); }
return outside
})
}
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
}
i call the hooks like that
useClickOutside(".foo",
() => {
// some code ...
},
);
i got this error TypeError: nodes.some is not a function even if just before the some function i got everything working on the nodes array !!
SOLUTION
thanks to #enapupe answer we can use also ES6
const nodes = [ ...document.querySelectorAll(className) ]

The Document method querySelectorAll() returns a static (not live) NodeList representing a list of the document's elements that match the specified group of selectors.
https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll
The NodeList type is not compatible with an array type, which has some.
You can do Array.from(selector) in order to use regular array prototypes on top of it.
const useClickOutside = (className, f) => {
function handleClickOutside(event) {
if (event.which !== 1) return
const nodes = Array.from(document.querySelectorAll(className))
nodes.some((node) => {
let outside = !node.contains(event.target)
if (outside) {
f()
}
return outside
})
}
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
}

Related

Replace all instances of a specific import via jscodeshift

okay so I have code that looks like this:
import { wait } from "#testing-library/react";
describe("MyTest", () => {
it("should wait", async () => {
await wait(() => {
console.log("Done");
});
});
});
I want to change that import member wait to be waitFor. I'm able to change it in the AST like so:
source
.find(j.ImportDeclaration)
.filter((path) => path.node.source.value === "#testing-library/react")
.find(j.ImportSpecifier)
.filter((path) => path.node.imported.name === "wait")
.replaceWith(j.importSpecifier(j.identifier("waitFor")))
.toSource()
However, the outputed code will look as follows:
import { waitFor } from "#testing-library/react";
describe("MyTest", () => {
it("should wait", async () => {
await wait(() => {
console.log("Done");
});
});
});
I'm looking for a way to change all subsequent usages of that import to match the new name
Is this possible with jscodeshift?
You can do it by visiting all the CallExpression nodes, filtering for those with the name you're targeting ("wait") and replacing them with new nodes.
To find the relevant nodes you can either loop through the collection returned by jscodeshift's find() method and add your logic there, or you can give find() a second argument. This should be a predicate used for filtering:
const isWaitExpression = (node) =>
node.callee && node.callee.name === "wait";
// I've used "root" where you used "source"
root
.find(j.CallExpression, isWaitExpression)
Then you can replace those nodes with replaceWith():
const replaceExpression = (path, j) =>
j.callExpression(j.identifier("waitFor"), path.node.arguments);
root
.find(j.CallExpression, isWaitExpression)
.replaceWith((path) => replaceExpression(path, j));
It can look a bit confusing, but to create a CallExpression node, you call the callExpression() method from the api. Note the camel-casing there.
All together, a transformer that renames "wait" from RTL to "waitFor" — both the named import declaration and every instance where it's called in the file — can be done like this:
const isWaitExpression = (node) => node.callee && node.callee.name === "wait";
const replaceExpression = (path, j) =>
j.callExpression(j.identifier("waitFor"), path.node.arguments);
export default function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
root
.find(j.ImportDeclaration)
.filter((path) => path.node.source.value === "#testing-library/react")
.find(j.ImportSpecifier)
.filter((path) => path.node.imported.name === "wait")
.replaceWith(j.importSpecifier(j.identifier("waitFor")));
root
.find(j.CallExpression, isWaitExpression)
.replaceWith((path) => replaceExpression(path, j));
return root.toSource();
}
And here's a link to it on AST explorer

useCallback with updated state object - React.js

I have a POST API call that I make on a button click. We have one large state object that gets sent as body for a POST call. This state object keeps getting updated based on different user interactions on the page.
function QuotePreview(props) {
const [quoteDetails, setQuoteDetails] = useState({});
const [loadingCreateQuote, setLoadingCreateQuote] = useState(false);
useEffect(() => {
if(apiResponse?.content?.quotePreview?.quoteDetails) {
setQuoteDetails(apiResponse?.content?.quotePreview?.quoteDetails);
}
}, [apiResponse]);
const onGridUpdate = (data) => {
let subTotal = data.reduce((subTotal, {extendedPrice}) => subTotal + extendedPrice, 0);
subTotal = Math.round((subTotal + Number.EPSILON) * 100) / 100
setQuoteDetails((previousQuoteDetails) => ({
...previousQuoteDetails,
subTotal: subTotal,
Currency: currencySymbol,
items: data,
}));
};
const createQuote = async () => {
try {
setLoadingCreateQuote(true);
const result = await usPost(componentProp.quickQuoteEndpoint, quoteDetails);
if (result.data?.content) {
/** TODO: next steps with quoteId & confirmationId */
console.log(result.data.content);
}
return result.data;
} catch( error ) {
return error;
} finally {
setLoadingCreateQuote(false);
}
};
const handleQuickQuote = useCallback(createQuote, [quoteDetails, loadingCreateQuote]);
const handleQuickQuoteWithoutDeals = (e) => {
e.preventDefault();
// remove deal if present
if (quoteDetails.hasOwnProperty("deal")) {
delete quoteDetails.deal;
}
handleQuickQuote();
}
const generalInfoChange = (generalInformation) =>{
setQuoteDetails((previousQuoteDetails) => (
{
...previousQuoteDetails,
tier: generalInformation.tier,
}
));
}
const endUserInfoChange = (endUserlInformation) =>{
setQuoteDetails((previousQuoteDetails) => (
{
...previousQuoteDetails,
endUser: endUserlInformation,
}
));
}
return (
<div className="cmp-quote-preview">
{/* child components [handleQuickQuote will be passed down] */}
</div>
);
}
when the handleQuickQuoteWithoutDeals function gets called, I am deleting a key from the object. But I would like to immediately call the API with the updated object. I am deleting the deal key directly here, but if I do it in an immutable way, the following API call is not considering the updated object but the previous one.
The only way I found around this was to introduce a new state and update it on click and then make use of the useEffect hook to track this state to make the API call when it changes. With this approach, it works in a weird way where it keeps calling the API on initial load as well and other weird behavior.
Is there a cleaner way to do this?
It's not clear how any children would call the handleQuickQuote callback, but if you are needing to close over in callback scope a "copy" of the quoteDetails details then I suggest the following small refactor to allow this parent component to use the raw createQuote function while children receive a memoized callback with the current quoteDetails enclosed.
Consume quoteDetails as an argument:
const createQuote = async (quoteDetails) => {
try {
setLoadingCreateQuote(true);
const result = await usPost(componentProp.quickQuoteEndpoint, quoteDetails);
if (result.data?.content) {
/** TODO: next steps with quoteId & confirmationId */
console.log(result.data.content);
}
return result.data;
} catch( error ) {
return error;
} finally {
setLoadingCreateQuote(false);
}
};
Memoize an "anonymous" callback that passes in the quoteDetails value:
const handleQuickQuote = useCallback(
() => createQuote(quoteDetails),
[quoteDetails]
);
Create a shallow copy of quoteDetails, delete the property, and call createQuote:
const handleQuickQuoteWithoutDeals = (e) => {
e.preventDefault();
const quoteDetailsCopy = { ...quoteDetails };
// remove deal if present
if (quoteDetailsCopy.hasOwnProperty("deal")) {
delete quoteDetailsCopy.deal;
}
createQuote(quoteDetailsCopy);
}

How to check if HTMLElement has already been generated?

I have a component from 3rd party which emits "onCellEdit" event and passes a cell element as parameter.
In my event handler I want to automatically select the whole text in the input element that is generated inside of this cell.
The problem I'm having is that when my handler is triggered the input element is not yet loaded.
(cellElement as HTMLTableCellElement).querySelector('input') returns nothing since the 3rd party component needs some time I guess.
My solution now looks like this:
selectTextOnEdit(cell: HTMLTableCellElement) {
const repeater = (element: HTMLTableCellElement) => {
const inputElement = element.querySelector('input');
if (inputElement) {
inputElement.select();
} else {
setTimeout(() => { repeater(element); }, 50);
}
};
repeater(cell);
}
this function then triggers the repeater function which goes around until the input element is found. I know I'm missing some kind of a check in case the input element is never generated.. but it's not important for this question.
I highly dislike this solution and I'm sure there are better ones.
Update:
After some research I found out about "MutationObserver".
Here is my new solution:
selectTextOnEdit(cell: HTMLTableCellElement) {
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.addedNodes && mutation.addedNodes.length > 0) {
const inputElement = cell.querySelector('input');
if (inputElement) {
inputElement.select();
observer.disconnect();
}
}
});
});
observer.observe(cell, {childList: true});
}
For these kind of scenarios, I like to use a utility function of waitUntil.
import { interval } from 'rxjs';
import { take } from 'rxjs/operators';
...
export const waitUntil = async (untilTruthy: Function): Promise<boolean> => {
while (!untilTruthy()) {
await interval(25).pipe(
take(1),
).toPromise();
}
return Promise.resolve(true);
}
Then in your function, it would be:
async selectTextOnEdit(cell: HTMLTableCellElement) {
await waitUntil(() => !!cell.querySelector('input'));
const inputElement = element.querySelector('input');
inputElement.select();
}
This is the same thing but slightly cleaner in my opinion. Why is it an issue that the input was never created, shouldn't always be created if the callback of selectTextOnEdit is called?
After some research I found out about "MutationObserver".
Here is my new solution:
selectTextOnEdit(cell: HTMLTableCellElement) {
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.addedNodes && mutation.addedNodes.length > 0) {
const inputElement = cell.querySelector('input');
if (inputElement) {
inputElement.select();
observer.disconnect();
}
}
});
});
observer.observe(cell, {childList: true});
}

useContext not updating its value in callback

useConext value is not updating in my callback attached to wheel event. I tried to console but still printing static value. But outside the callback, it's printing updated value
const Home = () => {
//accessing my context
var [appState, dispatch] = useContext(CTX);
//printing updated value here (working perfect here)
console.log(appState);
//my callback on wheel event (also using debouce to queue burst of events)
var fn = debounce(e => {
//incrementing value ++1
dispatch({ type: 'INCREMENT_COMPONENT_COUNTER' });
//printing static value here (problem here)
console.log(appState);
}, 500);
//setting and removing listener on component mount and unmount
useEffect(() => {
window.addEventListener('wheel', fn);
return () => {
window.removeEventListener('wheel', fn);
};
}, []);
};
On mounting, the listener initialized with a function variable which encloses the first value of appStore in its lexical scope.
Refer to Closures.
To fix it, move it into useEffect scope.
const Home = () => {
const [appState, dispatch] = useContext(CTX);
useEffect(() => {
const fn = debounce(e => {
dispatch({ type: 'INCREMENT_COMPONENT_COUNTER' });
console.log(appState);
}, 500);
window.addEventListener('wheel', fn);
return () => {
window.removeEventListener('wheel', fn);
};
}, [appState]);
};
Friendly advice:
Use linter like eslint - It should have warned you of using appState inside useEffect
Don't use var - it's error-prone.
Your debance function is changing in every render, while the useEffect have the capture of only the first render, you can fix this with a useCallback:
const Home = () => {
// accessing my context
const [appState, dispatch] = useContext(CTX)
// printing updated value here (working perfect here)
console.log(appState)
// my callback on wheel event (also using debouce to queue burst of events)
const fn = useCallback(
() =>
debounce(e => {
// incrementing value ++1
dispatch({ type: 'INCREMENT_COMPONENT_COUNTER' })
// printing static value here (problem here)
console.log(appState)
}, 500),
[appState, dispatch],
)
// setting and removing listener on component mount and unmount
useEffect(() => {
window.addEventListener('wheel', fn)
return () => {
window.removeEventListener('wheel', fn)
}
}, [fn])
}

Can i set the state here? React-Native

In the getMatches function, I want to set the state, with the matches associated with the leagueID. Where I am running into problems is how to setState in the get Matches function.The fetch response gives me exactly what I want back. Its an array with matches associated with the LeagueID. That setState will run twice because I am using 2 leagueIds. Any help or advice would be greatly appreciated.
export default class MatchView extends Component {
state = {
matches: null,
leagueName: null
}
componentWillMount = () => {
this.getLeagueNames();
}//end componentwwillMOunt
// componentDidMount = () => {
// console.log("league_array", this.state.leagueName)
// }
getLeagueNames = () => {
let leagueArray = [];
fetch(`https://apifootball.com/api/?action=get_leagues&APIkey=42f53c25607596901bc6726d6d83c3ebf7376068ff89181d25a1bba477149480`)
.then(res => res.json())
.then(leagues => {
leagues.map(function(league, id){
leagueArray = [...leagueArray, league]
})//end .map
this.setState({
leagueName: leagueArray
});
})
}// end getLeagueNames
getMatches = (leagueID) => {
let LeagGamesArray = [];
console.log('working')
fetch(`https://apifootball.com/api/?action=get_events&from=2016-10-30&to=2016-11-01&league_id=${leagueID}&APIkey=42f53c25607596901bc6726d6d83c3ebf7376068ff89181d25a1bba477149480`)
.then(res => res.json())
.then(fixtures => {
LeagGamesArray = [...LeagGamesArray, fixtures]
** this.setState({
matches: LeagGamesArray
}) ** Setting the state here dosent work, just keeps running the code over and over again.**strong text**
})
}
//console.log(this.state.matches)
//console.log(this.state.matches)
//console.log(this.state.matches)
renderItem = () => {
if(this.state.leagueName != null){
return this.state.leagueName.map(function(league){
let leagueID = league.league_id
let fix = this.getMatches(leagueID)
//console.log(fix)
return (
<LeagueName key={league.country_id} league={league}/>
)
}.bind(this))//end maps..We .bind(this) b/c we want to change the meaning of this. We first use it in the .map function, but we also want to use this to call the getMacthes function.
} else {
return null;
}
}//end renderItem
render(){
return (
<ScrollView>
{this.renderItem()}
</ScrollView>
)
}
}
strong text
Your renderItem method calls getMatches(leagueID), which in turn fires-off your fetch which results in setState and a subsequent render... and repeat.
You should call getMatches from elsewhere, like componentWillMount. Or by some user interaction (or even timer) that you use to generate a refresh.

Categories

Resources