I am attempting to make changes to a document inside of an iframe from its parent. Yes, I know this is bad practice, but I've exhausted all of my other options for doing this in a sane way. Unfortunately I've run into a problem where the "contentDocument" property on the iframe is always null, even after a "load" event completes. What am I missing?
Code:
import {useCallback, useEffect, useRef} from "react";
export function MyIframeComponent(): JSX.Element {
const iframeFref = useRef<HTMLIFrameElement>(null);
const myUrl = "/url/to/be/framed";
const onLoadCallback = useCallback(() => {
if (!iframeFref.current) {
console.log("Current is null after load") // doesnt fail here
return;
}
console.log(iframeFref.current) // I'm a valid iframe with children!
console.log(iframeFref.current.contentWindow) // I'm a valid window!
console.log(iframeFref.current.contentEditable) // I'm ... "inherit"?
console.log(iframeFref.current.contentDocument) // I'm null!
if (iframeFref.current.contentDocument) {
console.log("Success! I can do stuff")
} else {
console.log("Failure! I'm mysterious")
}
}, []);
useEffect(() => {
if (iframeFref.current) {
iframeFref.current.addEventListener("load", onLoadCallback);
}
return () => {
if (iframeFref.current) {
iframeFref.current.removeEventListener("load", onLoadCallback);
}
}
}, [iframeFref, onLoadCallback]);
return <iframe ref={iframeFref} src={myUrl} />
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Related
I'm working on an open-source project and have encountered a bug. I'm not able to navigate the dropdown list items using the keyboard (arrow key/tab). I've written the keyboard-navigation logic, but not quite sure of how to implement it. Below is the code snippet.
.
.
.
const TopNavPopoverItem: FC<ComponentProps> = ({closePopover, description, iconSize, iconType, title, to}) => {
const history = useHistory();
const handleButtonClick = (): void => {
history.push(to);
closePopover();
};
const useKeyPress = function (targetKey: any) { // where/how am I supposed to use this function?
const [keyPressed, setKeyPressed] = useState(false);
function downHandler(key: any) {
if (key === targetKey) {
setKeyPressed(true);
}
}
const upHandler = (key: any) => {
if (key === targetKey) {
setKeyPressed(false);
}
};
React.useEffect(() => {
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
});
return keyPressed;
};
return (
<button className="TopNavPopoverItem" onClick={handleButtonClick}>
<Icon className="TopNavPopoverItem__icon" icon={iconType} size={iconSize} />
<div className="TopNavPopoverItem__right">
<span className="TopNavPopoverItem__title">{title}</span>
<span className="TopNavPopoverItem__description">{description}</span>
</div>
</button>
);
};
Any workaround or fixes?
Thanks in advance.
A custom hook should always be defined at the top level of your file. It cannot be inside of a component. The component uses the hook, but doesn't own the hook.
You have a hook which takes a key name as an argument and returns a boolean indicating whether or not that key is currently being pressed. It's the right idea, but it has some mistakes.
When you start adding better TypeScript types you'll see that the argument of your event listeners needs to be the event -- not the key. You can access the key as a property of the event.
(Note: Since we are attaching directly to the window, the event is a DOM KeyboardEvent rather than a React.KeyboardEvent synthetic event.)
Your useEffect hook should have some dependencies so that it doesn't run on every render. It depends on the targetKey. I'm writing my code in CodeSandbox where I get warnings about "exhaustive dependencies", so I'm also adding setKeyPressed as a dependency and moving the two handlers inside the useEffect.
I see that you have one handler as function and one as a const. FYI it really doesn't matter which you use in this case.
Our revised hook looks like this:
import { useState, useEffect } from "react";
export const useKeyPress = (targetKey: string) => {
const [keyPressed, setKeyPressed] = useState(false);
useEffect(
() => {
const downHandler = (event: KeyboardEvent) => {
if (event.key === targetKey) {
setKeyPressed(true);
}
};
const upHandler = (event: KeyboardEvent) => {
if (event.key === targetKey) {
setKeyPressed(false);
}
};
// attach the listeners to the window.
window.addEventListener("keydown", downHandler);
window.addEventListener("keyup", upHandler);
// remove the listeners when the component is unmounted.
return () => {
window.removeEventListener("keydown", downHandler);
window.removeEventListener("keyup", upHandler);
};
},
// re-run the effect if the targetKey changes.
[targetKey, setKeyPressed]
);
return keyPressed;
};
I don't know you intend to use this hook, but here's a dummy example. We show a red box on the screen while the spacebar is pressed, and show a message otherwise.
Make sure that the key name that you use when you call the hook is the correct key name. For the spacebar it is " ".
import { useKeyPress } from "./useKeyPress";
export default function App() {
const isPressedSpace = useKeyPress(" ");
return (
<div>
{isPressedSpace ? (
<div style={{ background: "red", width: 200, height: 200 }} />
) : (
<div>Press the Spacebar to show the box.</div>
)}
</div>
);
}
CodeSandbox Link
This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed last year.
React useState() doesn't update value of the variable if called just after setting value.
I read about useEffect(), but don't really know how this will be useful for this particular scenario.
Full code (please open the console tab to see the variable status)
UPDATE
// hook
const [ error, setError ] = useState<boolean>();
const handleSubmit = (e: any): void => {
e.preventDefault();
if (email.length < 4) {
setError(true);
}
if (password.length < 5) {
setError(true);
}
console.log(error); // <== still false even after setting it to true
if (!error) {
console.log("validation passed, creating token");
setToken();
} else {
console.log("errors");
}
};
Let's assume the user does not have valid credentials. The problem is here:
if (email.length < 4) { // <== this gets executed
setError(true);
}
if (password.length < 5) { // <== this gets executed
setError(true);
}
console.log(error); // <== still false even after setting it to true
if (!error) { // <== this check runs before setError(true) is complete. error is still false.
console.log("validation passed, creating token");
setToken();
} else {
console.log("errors");
}
You are using multiple if-checks that all run independently, instead of using a single one. Your code executes all if-checks. In one check, you call setError(true) when one of the conditions is passed, but setError() is asynchronous. The action does not complete before the next if-check is called, which is why it gives the appearance that your value was never saved.
You can do this more cleanly with a combination of if-else and useEffect instead: https://codesandbox.io/s/dazzling-pascal-78gqp
import * as React from "react";
const Login: React.FC = (props: any) => {
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const [error, setError] = React.useState(null);
const handleEmailChange = (e: any): void => {
const { value } = e.target;
setEmail(value);
};
const handlePasswordChange = (e: any): void => {
const { value } = e.target;
setPassword(value);
};
const handleSubmit = (e: any): void => {
e.preventDefault();
if (email.length < 4 || password.length < 5) {
setError(true);
} else {
setError(false);
}
};
const setToken = () => {
//token logic goes here
console.log("setting token");
};
React.useEffect(() => {
if (error === false) {
setToken();
}
}, [error]); // <== will run when error value is changed.
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="email#address.com"
onChange={handleEmailChange}
/>
<br />
<input
type="password"
placeholder="password"
onChange={handlePasswordChange}
/>
<br />
<input type="submit" value="submit" />
</form>
{error ? <h1>error true</h1> : <h1>error false</h1>}
</div>
);
};
export default Login;
Just like setState, useState is asynchronous and tends to batch updates together in an attempt to be more performant. You're on the right track with useEffect, which would allow you to effectively perform a callback after the state is updated.
Example from the docs:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Although it is also recommended that if you need the updated value as soon as an update to the state has been requested, you're likely better off with just a variable in the component.
More on using state synchronously
And if you're familiar with Redux's reducers, you could use useReducer as another alternative. From the docs:
useReducer is usually preferable to useState when you have complex
state logic that involves multiple sub-values or when the next state
depends on the previous one. useReducer also lets you optimize
performance for components that trigger deep updates because you can
pass dispatch down instead of callbacks.
As the title states, I have a variable which is a javascript object, i'm comparing it with another js object by stringifying them. The problem is that the variable is completely accessible without calling the keys, so these
if(JSON.stringify(response) == JSON.stringify(lastcmd))
if(JSON.stringify(response.id) == JSON.stringify(lastcmd))
work perfectly fine, but accessing lastcmd's id key will cause it to throw undefined.
if(JSON.stringify(response) == JSON.stringify(lastcmd.id))
full code link here
Edit: Here's the JSON
{ "id" : "001", "app": "msgbox", "contents": { "title": "Newpaste", "message": "I'm a edited paste!" } }
Edit2: Here's the code on the post
const { BrowserWindow, app, dialog, ClientRequest } = require("electron");
const axios = require("axios");
const url = require("url");
let win = null;
let lastcmd;
function grabCurrentInstructions(fetchurl) {
return axios
.get(fetchurl)
.then(response => {
// handle success
//console.log(response.data);
return response.data;
})
.catch(function(error) {
// handle error
console.log(error);
});
}
function boot() {
//console.log(process.type);
win = new BrowserWindow({
resizable: true,
show: false,
frame: false
});
win.loadURL(`file://${__dirname}/index.html`);
//Loop everything in here every 10 seconds
var requestLoop = setInterval(getLoop, 4000);
function getLoop() {
grabCurrentInstructions("https://pastebin.com/raw/i9cYsAt1").then(
response => {
//console.log(typeof lastcmd);
//console.log(typeof response);
if (JSON.stringify(response.app) == JSON.stringify(lastcmd.app)) {
console.log(lastcmd.app);
clearInterval(requestLoop);
requestLoop = setInterval(getLoop, 4000);
} else {
lastcmd = response;
switch (response.app) {
case "msgbox":
dialog.showMessageBox(response.contents);
//console.log(lastcmd);
clearInterval(requestLoop);
requestLoop = setInterval(getLoop, 1000);
}
}
}
);
}
}
app.on("ready", boot);
And here's the error:
(node:7036) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'id' of undefined
at grabCurrentInstructions.then.response (C:\Users\The Meme Machine\Desktop\nodejsprojects\electronrat\index.js:42:64)
at process._tickCallback (internal/process/next_tick.js:68:7)
Thanks to user str I saw that my lastcmd was undefined when I ran the comparison the first time, this would break it and thereby loop the same error over and over, by addding
grabCurrentInstructions("https://pastebin.com/raw/i9cYsAt1").then(
response => {
lastcmd = response;
}
);
below this line
win.loadURL(`file://${__dirname}/index.html`);
I made sure that the last command sent while the app was offline wouldn't be executed on launch and fixing my problem at the same time!
I am trying to run getResponse once when a web components finishes loading. However, when I try to run this, the debounce function just acts as an async delay and runs 4 times after 5000 ms.
static get properties() {
return {
procedure: {
type: String,
observer: 'debounce'
}
}
}
debounce() {
this._debouncer = Polymer.Debouncer.debounce(this._debouncer, Polymer.Async.timeOut.after(5000), () => {
this.getResponse();
});
}
getResponse() {
console.log('get resp');
}
What is necessary to get getResponse to run once upon the loading of the element?
Are you sure you want to use a debouncer for that? you could just use the connectedCallBack to get a one Time Event
class DemoElement extends HTMLElement {
constructor() {
super();
this.callStack = 'constructor->';
}
connectedCallback() {
this.callStack += 'connectedCallback';
console.log('rendered');
fetch(this.fakeAjax()).then((response) => {
// can't do real ajax request here so we fake it... normally you would do
// something like this.innerHTML = response.text();
// not that "rendered" get console logged before "fetch done"
this.innerHTML = `
<p>${this.callStack}</p>
<p>${response.statusText}</p>
`;
console.log('fetch done');
}).catch(function(err) {
console.log(err); // Error :(
});
}
fakeAjax() {
return window.URL.createObjectURL(new Blob(['empty']));
};
}
customElements.define('demo-element', DemoElement);
<demo-element></demo-element>
If you really need to use an observer you could also set a flag this.isLoaded in your connectedCallback() and check for that in your observer code.
I'm building a chrome app and I use Vue.js for the options page.
So I want to load settings from the chrome storage and put it into the vue data.
My problem is, that i can not access the vue compontens from inside the chrome storage callback. Every time i call it inside the callback, all vue elements are undefined.
Is there a way, to let the chrome storage cb function return a value, or give it an extra callback.
Here is my code
name: 'app',
data: function () {
return {
data []
}
},
methods: {
init: function() {
chrome.storage.sync.get('series', function (storageData) {
this.data = storageData //this is not possible, because this.data is undefined
})
});
}
},
created: function () {
this.init();
}
}
If using ES6 and transpiling (preferred approach). Note: arrow functions don't create a new context.
init: function() {
chrome.storage.sync.get('series', storageData => {
this.data = storageData
});
}
ES5 workaround:
init: function() {
var self = this;
chrome.storage.sync.get('series', function (storageData) {
self.data = storageData
});
}