Skip to content
This repository was archived by the owner on Jan 19, 2025. It is now read-only.

Commit ecf53fe

Browse files
committed
New post 🚀
1 parent 0f1f746 commit ecf53fe

File tree

1 file changed

+224
-3
lines changed

1 file changed

+224
-3
lines changed

_drafts/2019-10-31-pull-to-refresh-web.md

Lines changed: 224 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Inside it I added two other divs:
2626

2727
* one is used to show a loader/activity indicator that will start to rotate as soon as the user scroll to the maximum pull to refresh point (and as already explained above, at this point the reload of the content should have been started).
2828

29-
* the other one is used to show a message to the user that explaing to him/her what it is happening (this is a nice to have that I added because I liked it! :XXXXXX:)
29+
* the other one is used to show a message to the user that explaing to him/her what it is happening (this is a nice to have that I added because I liked it! :stuck_out_tongue_winking_eye:)
3030

3131
Below you can find the entire html code snippet.
3232

@@ -39,7 +39,7 @@ Below you can find the entire html code snippet.
3939
</div>
4040
```
4141

42-
Let's see what I did on the CSS side. The code reported below here is written in SASS (the XXXXX......), but you can easily transform it in plain CSS if you need. First of all there's a new CSS property used in the `html` rule: `overscroll-behavior-y`. This property let the developers change the browser behaviour when the user researches the edge of the page with a scroll gesture. This is a property supported by Chrome, Firefox and Opera (fuck you Safari!!! :XXXXX:). By setting it's value to `contains`, we can for example disable the native browser pull to refresh on Chrome and avoid the page bounce effect when the user starts to overflow the borders while dragging. Then I defined a property `pullable-content` that I used on the entire content of the page that I want to move in parallel with the pull to refresh. The next class is `pull-to-refresh` and contains all the styles needed to layout the pull to refresh in all its states. As you can see I defined all the animation I needed for this UI component here except for the translation applied while dragging that will be computed on the JavaScript side (because this are simple animation and [CSS is performant enough for this kind of animations](XXXXXXXXXXX)). Last but not least I defined 2 classes to reset the pull to refresh layout status when the pull to refresh is started or has reached the end and starts the refresh of the content (they will be applied, like other contained here, with JavaScript DOM API).
42+
Let's see what I did on the CSS side. The code reported below here is written in SASS (the XXXXX......), but you can easily transform it in plain CSS if you need. First of all there's a new CSS property used in the `html` rule: `overscroll-behavior-y`. This property let the developers change the browser behaviour when the user researches the edge of the page with a scroll gesture. This is a property supported by Chrome, Firefox and Opera (fuck you Safari!!! :XXXXX:). By setting it's value to `contains`, we can for example disable the native browser pull to refresh on Chrome and avoid the page bounce effect when the user starts to overflow the borders while dragging. Then I defined a property `pullable-content` that I used on the entire content of the page that I want to move in parallel with the pull to refresh. The next class is `pull-to-refresh` and contains all the styles needed to layout the pull to refresh in all its states. As you can see I defined all the animation I needed for this UI component here except for the translation applied while dragging that will be computed on the JavaScript side (because this are simple animation and [CSS is performant enough for this kind of animations](https://medium.com/outsystems-experts/how-to-achieve-60-fps-animations-with-css3-db7b98610108)). Last but not least I defined 2 classes to reset the pull to refresh layout status when the pull to refresh is started or has reached the end and starts the refresh of the content (they will be applied, like other contained here, with JavaScript DOM API).
4343

4444
```scss
4545
html {
@@ -107,8 +107,229 @@ html {
107107

108108
#### JavaScript
109109

110+
On the JavaScript side, I wrote the the pull to refresh widget as a standalone widget that export one single function `pullToRefresh()`. The first thing that this widget does is to check the browser support for service worker. Then it checks for some HTML component that are needed by the widget by using the `invariant` function. This HTML components are the loader, the loader message status and the content to be refreshed. The widget will throw an error if one of this HTML component is not present on the page where the it is instantiated.
111+
Then 3 new listener are attached to the 3 touches event on the entire document: `'touchstart'`, `'touchmove'` and `'touchend'`. In the `'touchstart'` ....
112+
113+
```javascript
114+
import { sendMessageToServiceWorker } from '../common/service-worker'
115+
import { addCssClass, removeCssClass } from '../common/css-class'
116+
import { getTrackingClientId } from '../common/tracking'
117+
118+
const pullToRefresh = (trackingCategory) => {
119+
if (!('serviceWorker' in navigator)) {
120+
return
121+
}
122+
123+
const pullToRefreshElement = document.querySelector('#pull-to-refresh')
124+
const pullToRefreshStatusElement = document.querySelector('#pull-to-refresh-status')
125+
const pullToRefreshLoaderElement = document.querySelector('#pull-to-refresh-loader')
126+
const pullableContent = document.querySelector('.pullable-content')
127+
128+
invariant(pullToRefreshElement instanceof HTMLElement)
129+
invariant(pullToRefreshStatusElement instanceof HTMLElement)
130+
invariant(pullToRefreshLoaderElement instanceof HTMLElement)
131+
invariant(pullableContent instanceof HTMLElement)
132+
133+
const pullToRefreshElementHeight = pullToRefreshElement.offsetHeight
134+
const pullToRefreshStatusRepository = createPullToRefreshStatusRepository()
135+
const decelerationFactor = 0.5
136+
let dragStartPoint = createTouchCoordinates(0, 0)
137+
138+
const dragUpdate = (dragMovement, pullToRefreshLoaderOpacity) => {
139+
pullToRefreshElement.style.transform = `translateY(${dragMovement}px)`
140+
pullableContent.style.transform = `translateY(${dragMovement}px)`
141+
pullToRefreshLoaderElement.style.opacity = `${pullToRefreshLoaderOpacity}`
142+
}
143+
144+
const isDraggingForPullToRefresh = (yMovement) => window.scrollY <= 0 && yMovement <= 0
145+
146+
const closePullToRefresh = () => {
147+
addCssClass(pullToRefreshElement, 'end-pull')
148+
addCssClass(pullableContent, 'end-pull')
149+
pullToRefreshElement.style.transform = ''
150+
pullableContent.style.transform = ''
151+
pullToRefreshLoaderElement.style.opacity = '0'
152+
}
153+
154+
const preparePullToRefreshToStart = () => {
155+
addCssClass(pullToRefreshElement, 'start-pull')
156+
removeCssClass(pullToRefreshElement, 'end-pull')
157+
addCssClass(pullableContent, 'start-pull')
158+
removeCssClass(pullableContent, 'end-pull')
159+
}
160+
161+
const showPullToRefresh = () => {
162+
addCssClass(pullToRefreshElement, 'visible-pull')
163+
removeCssClass(pullToRefreshElement, 'hidden-pull')
164+
}
165+
166+
const setRefreshingStatus = () => {
167+
pullToRefreshStatusElement.innerHTML = 'Refreshing'
168+
addCssClass(pullToRefreshLoaderElement, 'animate')
169+
}
170+
171+
const isPullToRefreshDragCompleted = (yAbsoluteMovement) => yAbsoluteMovement >= pullToRefreshElementHeight
172+
173+
const setRefreshStatusCompleted = () => {
174+
pullToRefreshStatusElement.innerHTML = 'Refresh completed'
175+
addCssClass(pullToRefreshElement, 'hidden-pull')
176+
removeCssClass(pullToRefreshElement, 'visible-pull')
177+
}
178+
179+
const resetPullToRefreshStatus = () => {
180+
pullToRefreshStatusElement.innerHTML = 'Pull down to refresh'
181+
removeCssClass(pullToRefreshLoaderElement, 'animate')
182+
}
183+
184+
document.addEventListener('touchstart', (event) => {
185+
dragStartPoint = getTouchesCoordinatesFrom(event)
186+
preparePullToRefreshToStart()
187+
}, { passive: false })
188+
189+
document.addEventListener('touchmove', (event) => {
190+
const dragCurrentPoint = getTouchesCoordinatesFrom(event)
191+
const yMovement = (dragStartPoint.y - dragCurrentPoint.y) * decelerationFactor
192+
const yAbsoluteMovement = Math.abs(yMovement)
193+
194+
if (isDraggingForPullToRefresh(yMovement) && !pullToRefreshStatusRepository.refreshStarted) {
195+
event.preventDefault()
196+
event.stopPropagation()
197+
showPullToRefresh()
198+
199+
if (isPullToRefreshDragCompleted(yAbsoluteMovement)) {
200+
pullToRefreshStatusRepository.startRefresh()
201+
dragUpdate(0, 1)
202+
setRefreshingStatus()
203+
sendMessageToServiceWorker({ message: 'refresh', url: window.location.href, clientId: getTrackingClientId(), trackingCategory }).then(() => {
204+
pullToRefreshStatusRepository.completeRefresh()
205+
setTimeout(() => {
206+
setRefreshStatusCompleted()
207+
closePullToRefresh()
208+
}, 1500)
209+
})
210+
} else {
211+
dragUpdate(yAbsoluteMovement - pullToRefreshElementHeight, yAbsoluteMovement / pullToRefreshElementHeight)
212+
}
213+
}
214+
}, { passive: false })
215+
216+
document.addEventListener('touchend', () => {
217+
if (!pullToRefreshStatusRepository.refreshStarted) {
218+
closePullToRefresh()
219+
}
220+
}, { passive: false })
221+
222+
pullToRefreshElement.addEventListener('transitionend', () => {
223+
if (pullToRefreshStatusRepository.refreshCompleted) {
224+
window.location.reload()
225+
} else {
226+
resetPullToRefreshStatus()
227+
}
228+
})
229+
}
230+
231+
const createTouchCoordinates = (x, y) => ({ x, y })
232+
233+
const createPullToRefreshStatusRepository = () => ({
234+
refreshStarted: false,
235+
refreshCompleted: false,
236+
startRefresh () {
237+
this.refreshStarted = true
238+
},
239+
completeRefresh () {
240+
this.refreshCompleted = true
241+
}
242+
})
243+
244+
const invariant = (statement) => {
245+
if (!statement) {
246+
throw new Error('Pull to refresh invariant failed')
247+
}
248+
}
249+
250+
const getTouchesCoordinatesFrom = (event) => {
251+
return createTouchCoordinates(
252+
event.targetTouches[0].screenX,
253+
event.targetTouches[0].screenY
254+
)
255+
}
256+
257+
export { tryToActivatePullToRefresh }
258+
```
259+
260+
Finally I instantiated the pull to refresh widget inside the blog main js file `index.blog.js` file. Below you can find the startup code for the pull to refresh widget inside a `load` event listener.
261+
262+
```javascript
263+
import { pullToRefresh } from './blog/pull-to-refresh'
264+
265+
//...other code...
266+
267+
window.addEventListener('load', () => {
268+
//...other code...
269+
pullToRefresh(trackingCategory)
270+
//...other code...
271+
})
272+
```
110273

111274
#### Service Worker
112275

276+
```javascript
277+
const sendMessageToServiceWorker = (message: any): Promise<any> => {
278+
return new Promise((resolve, reject) => {
279+
const messageChannel: MessageChannel = new MessageChannel()
280+
messageChannel.port1.onmessage = (event: MessageEvent) => {
281+
if (event.data) {
282+
if (event.data.error) {
283+
reject(event.data.error)
284+
} else {
285+
resolve(event.data)
286+
}
287+
}
288+
}
289+
if (navigator.serviceWorker && navigator.serviceWorker.controller) {
290+
navigator.serviceWorker.controller.postMessage(message, ([messageChannel.port2]: any))
291+
}
292+
})
293+
}
294+
```
295+
296+
```javascript
297+
//...other service worker code...
298+
299+
self.addEventListener('message', (event) => {
300+
const isARefresh = (event) => event.data.message === 'refresh'
301+
302+
const createDeleteOperationFor = (url, siteCache, requests) => siteCache
303+
.delete(requests
304+
.find((request) => request.url === url))
305+
306+
const createDeleteOperationsForImages = (siteCache, requests) => requests
307+
.filter((request) => request.url.endsWith('.jpg') && request.url.includes('posts'))
308+
.map((request) => siteCache.delete(request))
309+
310+
const sendRefreshCompletedMessageToClient = (event) => event.ports[0].postMessage({refreshCompleted: true})
311+
312+
if (isARefresh(event)) {
313+
caches.open(siteCacheName).then((siteCache) => {
314+
siteCache.keys().then((requests) => {
315+
const deleteRequestToBeRefreshed = createDeleteOperationFor(event.data.url, siteCache, requests)
316+
const deleteRequestsForImagesToBeRefreshed = createDeleteOperationsForImages(siteCache, requests)
317+
Promise.all([
318+
deleteRequestToBeRefreshed,
319+
...deleteRequestsForImagesToBeRefreshed,
320+
sendAnalyticsEvent(event.data.clientId, '{{ site.data.tracking.action.pull_to_refresh }}', event.data.trackingCategory, '{{ site.data.tracking.label.body }}')
321+
])
322+
.then(() => sendRefreshCompletedMessageToClient(event))
323+
.catch(() => sendRefreshCompletedMessageToClient(event))
324+
})
325+
})
326+
}
327+
})
328+
329+
//...other service worker code...
330+
```
331+
332+
#### Conclusion
333+
334+
As you can see, it is not too difficult to create a pull to refresh UX that almost matches the experience given by a mobile native app. Service Workers, modern CSS and HTML and vanilla JavaScript let you create beautiful native alike experience that can make you user fall in love with you web product before they install your app :heart: (or maybe they will just stick to your site because they hate mobile apps or because you hate mobile apps and you don't want to develop a new one :smiley:).
113335

114-
#### Conclusion

0 commit comments

Comments
 (0)