Maybe I'm not understanding well enough how nuxt works, but apparently my b-modals (bootstrap vue) are being declared twice whenever I use async fetch on a page. I'll explain how my code is structured (can't upload all of the code because of company policies):
I have a default layout in the layouts folder.
<template>
<div>
<wrapper-navbar />
<nuxt class="body" />
<wrapper-footer />
</div>
</template>
Very simple stuff. The wrappers are declared in my components folder. The wrapper-navbar is a normal bootstrap navbar with modifications, and there I declare a b-modal with the unique id #login-modal which gets called whenever a user wants to login pressing a button with the attribute v-b-modal.login-modal. Everything works as expected, the user clicks the login button and a modal appears. The problem comes when in a page (in the pages folder) I call async fetch(). In these cases, when the user presses the login button, the login modal appears twice. I noticed that in these pages the methods created() and mounted() get called twice too, so I think that somehow the modal is getting declared twice.
// Inside pages/index.vue for example
async fetch() {
try {
this.data = await getSomeAsyncData();
} catch (ex) {
console.log(ex);
}
}
This only happens when I have an await inside it. If I only print something or just avoid using fetch, the modal just appears once. What am I doing wrong? How can I avoid this? Thanks for your feedback.
Related
I have been working with React for YEARS and have yet to encounter this phenomenon. If you can crack it, you'll be my hero forever 😂
The Setup
I have a shared component. It is a wrapper for some buttons that appear at the bottom of several different pages (most use cases are steps in a form. The last of those buttons ALWAYS throws a modal to cancel setup.
I am using the shared component in ALL the following use cases:
✔ The first page of a multi-step form. Works fine, throws modal.
✔ The second page of a multi-step form. Works fine, throws modal.
❌ The third page of a multi-step form. Fails. See description below
❌ The fourth page of a multi-step form. Fails
✔ The review page after the form is completed. Works fine again.
The component itself contains just a button that triggers a useState() setter locally, which toggles the modal open/closed.
The Problem
On the pages where it doesn't work, the component throws all the form values on the page up into the URL as a query string.
see image - green underlined portion is added on modal trigger
This triggers a page reload and of course things break.
Expected Result
I should be able to click "cancel" button and the modal pops. Nothing more, nothing less.
The (relevant) code
const IntakeLossButtons = ({
// PRIMARY BUTTON PROPS
buttonText,
onButtonClick,
buttonDisabled,
buttonSpinning,
buttonAriaLabel,
// CANCEL LINK AT BOTTOM
idToCancel,
saveAction,
onCancel
}) => {
const [showCancelModal, setShowCancelModal] = useState(false)
return (
<div className={styles.buttonsWrapper}>
<SpinnerButton
type="primary"
text={buttonText}
disabled={buttonDisabled}
spinning={buttonSpinning}
onClick={onButtonClick}
ariaLabel={buttonAriaLabel}
/>
<button
className="btnLink"
id="cancel-id"
tabIndex="0"
aria-label={translations.cancelBtnAria ||"Opens a modal to cancel your claim"}
color="secondary"
onClick={()=>setShowCancelModal(true)}
>
Cancel Setup
</button>
<CancelModal
isOpen={showCancelModal}
idToCancel={idToCancel}
onCancel={onCancel}
onSaveForLaterAction={saveAction}
onCloseModal={()=>setShowCancelModal(false)}
/>
</div>
)
}
export default IntakeLossButtons;
WTF?
What would cause this type of behavior? I have tried googling and reading what might cause this. I have logged everything I can log and I still don't know what is causing such a weird behavior.
Any ideas?
It sounds like maybe you forgot to do:
e.preventDefault();
inside the onSubmit handler of your form. In fact, it looks like you don't even have a <form> at all, just a <button> (one with no type attribute, which means it will default to being a submit button).
You need to wrap the button in a <form> with such a submit handler. If you don't do that, when you "submit the form" (ie. click the button) it will use all the form's inputs to generate a new URL, and then it will send you to do that URL.
This is the "default" behavior for forms in browsers. If you go to www.google.com, for instance, and search for "cute puppies", you'll see the URL changes to have ?q=cute+puppies, because Google relies on not preventing this default behavior.
Let's say I have the following component
function ClickButton(props) {
const history = useHistory();
const onClick = () => {
history.push('/next-page');
};
return <button onClick={onClick}>Go to next page</button>;
}
More accessible version would be to use Link from react-router as the following
function ClickButton(props) {
return (
<Link to="/next-page">
<span>Go to next page</span>
</Link>
);
}
But what if redirection depends on some http call success, like
function ClickButton(props) {
const history = useHistory();
const onClick = async () => {
try {
await axios.get('https://google.com')
history.push('/next-page');
} catch(err) {
setAlertMessage('Google can't be reached - check your internet connection');
}
};
return <button onClick={onClick}>Go to next page</button>;
}
What is equivalent (and is there some semantically meaningful way) of using Link (or something else that e.g. screen readers would treat as link-alike) here, in case of async handler of onClick event?
I'd probably opt to use the Link component since semantically it makes the most sense to. You can use button element, but then you would need to add all the appropriate accessibility attributes, i.e. role="link" and correct click and keypress event handlers, etc...
You can add an onClick handler to the Link and do the same check. The key is preventing initially the default link behavior.
const ClickButton = props => {
const history = useHistory();
const onClick = async e => {
e.preventDefault();
try {
await axios.get("https://google.com"); // NOTE *
history.push("/next-page");
} catch (err) {
alert("Google can't be reached - check your internet connection");
}
};
return (
<Link to="/next-page" onClick={onClick}>
<span>Go to next page</span>
</Link>
);
};
* NOTE: this GET request seems to just fail in codesandbox.
The simplest way to do this is with a hyperlink. (<link>) as #Drew Reese pointed out.
There is nothing wrong with intercepting the hyperlink action via JavaScript and reporting back if there is an error.
To do this you would just use a standard e.preventDefault(), make your AJAX call and redirect if it works. If it fails then create an alert that explains that the call failed.
Why use an anchor / hyperlink?
Semantics - accessibility is all about expected behaviour. If I change page then a hyperlink makes a lot more sense than a button. It also means I understand that the page will change from the element in case your description does not make sense.
JS Fallback - it could be your SPA does not function at all without JS. However if it does work in some limited fashion without JS then using a hyperlink means that if a user has JS disabled (around 2% of screen reader users don't use JS) or an error causes your JS to fail then navigation is still possible with a hyperlink.
SEO - I dare to swear on Stack Overflow and mention the forbidden term! Although search engines are very good at understanding JS they still aren't perfect. They understand hyperlinks perfectly. If SEO matters use a hyperlink.
Styling - hyperlinks have :visited and :active whereas buttons do not.
How to handle async loading.
When you click on the element you need to let the screen reader know an action is being performed.
The easiest way to do this is to have a visually hidden div located on the page that has the aria-live="polite" attribute. (You can use the same div for all actions).
Then when a function is called that contains an async action you set the contents of that div to "loading" or "waiting" (whatever is appropriate).
This will then get announced in the screen reader (you could argue aria-live="assertive" is better but I will leave that to you).
If the action fails then set your alert message (but make sure that your alert has the appropriate WAI-ARIA roles and behaviour. I would suggest role="alert" but without seeing your application that is a best guess. It could equally be role="alertdialog" if you want a confirmation from the user.
Final thought
When navigation occurs in a AJAX powered SPA, don't forget to set the focus to something on the new page. I normally recommend adding a <h1> with tabindex="-1" (so you can focus it) and setting the focus on that (as your <h1> should obviously explain what the page is about).
In my routes I have set-up a slug for a particular route like so:
Main route: /members
Sub route: /members/:slug
Now when I go to www.website.com/members/some-slug
I will try to detect whether there's a slug or not with:
if (this.props.match.params.slug) {
// If have slug, open a modal window with data that corresponds to that slug
this.showModal(this.props.match.params.slug);
}
What's happening is that showModal will change the state of a Modal component to true (thus reveal it) and also trigger an API call to get the details pertaining to the slug parameter that's passed (e.g. the slug sarah-smith is used for a GET request as a param to get Sarah Smith's details).
So all of these are fine so far. However, the issue is the re-rendering of the page when I either:
Open the modal
Close the modal
We have transitions for each element on the page so if you visit a route, the elements will have a subtle fade-in transition.
When a modal is clicked what I do is (member.name being the slug):
<Link
to={`/member/${member.name}`}
</Link>
Which causes a re-routing hence you can see those small transitions in the background while the modal is fading into view (as if I am opening /member/slug for the first time).
Same with the modal close button, if I click on it, what I do is invoke:
closeModal() {
this.props.history.push("/member");
}
Which re-renders the entire page /member page and not just close the modal. I can't just use this.setState({ showModal: false }); since that wouldn't change the route back to /member.
Is there a way to resolve this? Or another way of doing it so that the page doesn't re-render while changing the URL?
You can wrap your modal in route like this.
Modal
Then this.props.history.push("/path");
I think React-Router docs already have such examples. You can check following
https://reacttraining.com/react-router/web/example/modal-gallery.
There is npm module available as well https://github.com/davidmfoley/react-router-modal
In my small Electron app I have a couple of buttons to allow the user to browse for folders to use for the processing the app does. The call to open the dialog to do this passes the ID of the main browser window so that the dialog will be modal and this initially appeared to work. The buttons in the app main screen appear to be disabled. However, if the disabled buttons are clicked, when the dialog is closed those clicks are executed. This true of all the buttons in the main screen. If I click on the disabled "Cancel" button while the dialog is showing the app closes when the dialog is closed.
Seems to me that one should not be able to switch back to the parent of a modal dialog and "store" clicks.
The dialog.showOpenDialog call is made in the renderer process, is this possibly the issue?
Sid
In the renderer process you need to use the browser window reference, not the ID. You can get the reference in the renderer process by using: remote.getCurrentWindow(). You can make the call as follows. The example is specifically for opening multiple files, should be configured as needed of course:
const { remote } = window.require('electron')
const dialog = remote.dialog
dialog.showOpenDialog(
remote.getCurrentWindow(),
{ properties: ['openFile', 'multiSelections'] },
(filePaths?) => {
// do your thing
}
)
Not sure what was going on previously, I now cannot reproduce the problem, so I am going to mark this for closure.
Sorry for the noise,
Sid
You don't need electron/remote to achieve this, in fact, electron/remote is deprecated. The key is to provide the browser window's reference to the showOpenDialog API (in the main process). This is a general API pattern in window managers: in order to have a modal window, you have to specify a parent window. Code sample:
const ipcApi = {
'select-file': async () => {
// It's important to pass the window handler in showOpenDialog
// in order to make the dialog modal
const browserWindow = BrowserWindow.getFocusedWindow()
if (browserWindow) {
return dialog.showOpenDialog(browserWindow, { properties: ['openFile'] })
}
return
},
I hve the standard LinkedIn JSAPI script in my index.html:
<script type="text/javascript" src="http://platform.linkedin.com/in.js"></script>
I am using Angular.js and have this code in my sign-in page:
<button class="btn btn-primary" ng-click="FBlogin()">Connect with Facebook</button>
<script type="in/Login" data-onAuth="getLinkedInProfile"></script>
<script type="text/javascript">
// upon getting in.js above, apply to register window scope
function getLinkedInProfile() {
IN.API.Profile("me").fields(["id", "firstName", "lastName", "email-address", "picture-urls::(original)", "industry", "languages", "educations", "three-current-positions", "three-past-positions"])
.result(function(me) {
angular.element(document.getElementById('login_window')).scope().linkedInRegistration(me) // calls scope function
})
.error(function(error) {
console.log('>> error on LinkedInLoad', error)
})
}
</script>
On certain random chances, the button just does not appear at all. I've somewhat noticed that if I removed all the fields() options, then the button will reliably show up, but I'm not if this is the reason. Any suggestions?
I'm no angular expert, but I can tell you that once the user authenticates for the first time, anytime he is logged in into LinkedIn.com, he is automatically logged in into your website and the button disappears.
This only happens with the JavaScript API, and I believe that's what is happening on your side.
I noticed today that the linkedIn login button disappears when state changes. If the button is in state A and state is changed to B and then back to A, the button is no longer displayed.
I solved it by using a directive to place the button instead of hardcoding it into the view. The solution I used is in an answer to a similar question here at stackoverflow:
LinkedIn share button only appearing on initial load in AngularJS app
Hope this helps others coming by :-)