Skip to content

Instantly share code, notes, and snippets.

@ahmadawais
Created March 18, 2019 13:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ahmadawais/8e52ee4660ce11319cbcbe91e9d73348 to your computer and use it in GitHub Desktop.
Save ahmadawais/8e52ee4660ce11319cbcbe91e9d73348 to your computer and use it in GitHub Desktop.
Offline Form
<div>
<form action="" method="POST" id="example-form" class="form" autocomplete="off">
<fieldset class="form__fieldset">
<div class="form__field">
<label for="name" class="form__label">Name</label>
<input
type="text"
id="name"
class="form__input"
name="name"
required
>
</div>
<div class="form__field">
<label for="email" class="form__label">Email</label>
<input
type="email"
id="email"
class="form__input"
name="email"
required
>
</div>
<div class="form__field">
<label for="shirtsize" class="form__label">T-Shirt Size</label>
<select
id="shirtsize"
class="form__input"
name="shirtsize"
>
<option value="S">Small</option>
<option value="M">Medium</option>
<option value="L">Large</option>
<option value="XL">XLarge</option>
</select>
</div>
<div class="form__actions">
<button type="submit" class="btn btn--primary" id="submit">Submit Form</button>
<div id="feedback" class="form__feedback" aria-live="assertive" role="alert"></div>
</div>
</fieldset>
</form>
<p class="more">Read <a href="https://mxb.at/blog/offline-forms/">more about this here</a>.</p>
</div>
const SELECTORS = {
form: '.form',
feedbackArea: '.form__feedback',
}
class OfflineForm {
constructor(element) {
this.form = element;
this.id = element.id;
this.action = element.action;
this.data = {};
this.feedbackArea = this.form.querySelector(SELECTORS.feedbackArea);
this.form.addEventListener('submit', e => this.handleSubmit(e));
window.addEventListener('online', () => this.checkStorage());
window.addEventListener('load', () => this.checkStorage());
}
handleSubmit(e) {
// check network status on form submit
e.preventDefault();
this.getFormData();
if (!navigator.onLine) {
// user is offline, store data locally
const stored = this.storeData();
let message = '<strong>You appear to be offline right now. </strong>';
if (stored) {
message += 'Your data was saved and will be submitted once you come back online.';
}
this.resetFeedback();
this.feedbackArea.innerHTML = message;
} else {
// user is online, send data to server
this.sendData();
}
}
storeData() {
// save data in localStorage
if (typeof Storage !== 'undefined') {
const entry = {
time: new Date().getTime(),
data: this.data,
}
localStorage.setItem(this.id, JSON.stringify(entry));
return true;
}
return false;
}
sendData() {
// send ajax call to server
axios.post(this.action, this.data)
.then((response) => {
this.handleResponse(response);
})
.catch((error) => {
console.warn(error);
});
}
handleResponse(response) {
// handle server response
this.resetFeedback();
if (response.status === 200) {
// on success
localStorage.removeItem(this.id);
this.form.reset();
this.feedbackArea.classList.add(`success`);
this.feedbackArea.textContent = '👍 Successfully sent. Thank you!';
} else {
// failure
this.feedbackArea.textContent = '🔥 Invalid form submission. Oh noez!';
}
}
checkStorage() {
// check if we have saved data in localStorage
if (typeof Storage !== 'undefined') {
const item = localStorage.getItem(this.id);
const entry = item && JSON.parse(item);
if (entry) {
// discard submissions older than one day
const now = new Date().getTime();
const day = 24 * 60 * 60 * 1000;
if (now - day > entry.time) {
localStorage.removeItem(this.id);
return;
}
// we have saved form data, try to submit it
this.data = entry.data;
this.sendData();
}
}
}
getFormData() {
// simple parser, get form data as object
let field;
let i;
const data = {};
if (typeof this.form === 'object' && this.form.nodeName === 'FORM') {
const len = this.form.elements.length;
for (i = 0; i < len; i += 1) {
field = this.form.elements[i];
if (field.name &&
!field.disabled &&
field.type !== 'file' &&
field.type !== 'reset' &&
field.type !== 'submit'
) {
data[field.name] = field.value || '';
}
}
}
this.data = data;
}
resetFeedback() {
this.feedbackArea.classList.remove(`success`);
this.feedbackArea.innerHTML = '';
}
}
// init
Array.from(document.querySelectorAll(SELECTORS.form)).forEach((form) => {
new OfflineForm(form);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.2/axios.min.js"></script>
$blue: #31708F;
$green: #00A86B;
body {
display:flex;
justify-content:center;
align-items:center;
min-height:100vh;
background-color:paleturquoise;
line-height:1.25;
}
.form {
width:320px;
padding:2rem;
background-color:#FFF;
border:0;
border-radius:.375rem;
box-shadow: 0 15px 45px -5px rgba(10, 16, 34, .15);
&__fieldset {
border:0;
padding:0;
}
&__field {
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:1rem;
min-height:2.375rem;
}
&__label {
flex:0 1 33.33%;
}
&__input {
display:block;
flex: 1;
padding:.5rem;
}
&__actions {
padding-top:.5rem;
}
&__feedback {
margin-top:1rem;
padding:1rem;
border:2px solid currentColor;
border-radius:.25rem;
color:$blue;
background-color:rgba($blue, .1);
&:empty {
display:none;
}
&.success {
color:$green;
background-color:rgba($green, .1);
}
}
}
.btn {
display: inline-block;
text-align: center;
white-space: nowrap;
vertical-align: middle;
user-select: none;
transition:all .2s ease-in-out;
font-size: .875rem;
padding: 1rem 1.5rem;
border: 2px solid rgba(0,0,0,.15);
border-radius: .25rem;
cursor:pointer;
&--primary {
display:block;
width:100%;
font-weight:700;
background-color:salmon;
color:#FFF;
font-size:.875rem;
text-transform:uppercase;
letter-spacing:2px;
&:hover, &:focus {
background-color: darken(salmon, 10%);
}
}
}
.more {
text-align:center;
padding-top:.5rem;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment