-
Notifications
You must be signed in to change notification settings - Fork 86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
File uploader example #16
Changes from 7 commits
38adda8
3b3abaf
13b888e
49a54a3
0a9e6fb
74973cf
971edb8
6e99ca5
0790061
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
uploads/* |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
# File Uploader | ||
|
||
This example is a browser-based file-upload component, i.e. it renders a list | ||
of chosen files, shows their upload progress, and allows users to cancel | ||
uploads. | ||
|
||
Similar to the [zip-code-futures][zip] example, it demonstrates a way to do | ||
asynchronous side effects in a controlled way. The top-level update function | ||
(in `main.js`) was copied from that example. It features a convention for making | ||
synchronous and asynchronous updates together which is very similar to Elm's | ||
`effects`. You can read more about this in the [Elm architecture tutorial][elm]. | ||
|
||
It also features routing of nested updates, both synchronous (which 'bubble up' | ||
the component tree in the familiar way, based on user events) and asynchronous | ||
(which are constructed via future chains of actions to 'dive down' the component | ||
tree, based on server responses). | ||
|
||
The uploader wraps XMLHttpRequest in a [ramda-fantasy][rf] Future, which | ||
repeatedly resolves on XHR progress events. | ||
|
||
|
||
## How to build it | ||
|
||
Install the dependencies. | ||
|
||
``` | ||
npm install | ||
``` | ||
|
||
Then build the code with | ||
|
||
``` | ||
npm run build | ||
``` | ||
|
||
Run the tests | ||
|
||
``` | ||
npm run test | ||
``` | ||
|
||
|
||
## How to use it | ||
|
||
Create an uploads folder, if it doesn't already exist: | ||
|
||
``` | ||
mkdir -p uploads | ||
``` | ||
|
||
Start the test server | ||
|
||
``` | ||
node server.js 8080 | ||
``` | ||
|
||
Then open your browser to `http://localhost:8080/index.html`, and choose some | ||
files to upload. | ||
|
||
|
||
## Other notes | ||
|
||
- The progress bar is rendered as SVG using @yelouafi 's [builder][svg] (not yet | ||
part of snabbdom, but it works great!) | ||
|
||
- The example is intended to be a step towards a realistic stand-alone | ||
file uploader, i.e. a 'widget' or reusable component. So `main` and `app` | ||
give a picture of how you would integrate it into a real app. Comments | ||
welcome on the app interface to the 'widget' (the `list` and `uploader`), | ||
I am sure it could be improved. | ||
|
||
- 'Structural' styling is done via javacript, while app-specific styles can be | ||
applied via CSS. | ||
|
||
- The XHR `abort` method is not handled in a pure way. This seems impossible, | ||
at least using the Future interface. So there is a bit of a 'wormhole' | ||
exposing this method back to the app and into the model, where it can be | ||
hooked up to a click handler. Other suggestions welcome. | ||
|
||
- The tests include a simple dummy uploader for running unit tests independent | ||
of XHR and the browser, an example of how easy it is to test using this | ||
architecture. | ||
|
||
|
||
|
||
[zip]: https://github.com/paldepind/functional-frontend-architecture/tree/master/examples/zip-codes-future | ||
[elm]: https://github.com/evancz/elm-architecture-tutorial#example-5-random-gif-viewer | ||
[rf]: https://github.com/ramda/ramda-fantasy | ||
[svg]: https://github.com/paldepind/snabbdom/issues/4 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | ||
<title>File Uploader</title> | ||
<script type="text/javascript" src="js/build.js"></script> | ||
<style> | ||
|
||
body { | ||
box-sizing: border-box; | ||
font-size: 12pt; | ||
font-family: Verdana, Arial, Helvetica, sans-serif; | ||
margin: 1%; | ||
} | ||
|
||
.upload { | ||
padding: 0.5rem; | ||
margin: 0.5rem; | ||
border: 1px solid lightgrey; | ||
border-radius: 3px; | ||
} | ||
|
||
.upload.uploaded { | ||
background-color: aliceblue; | ||
} | ||
|
||
.upload.error { | ||
color: red; | ||
} | ||
|
||
.upload.abort { | ||
color: lightgrey; | ||
} | ||
|
||
.upload > .status { | ||
text-transform: uppercase; | ||
font-size: 0.7em; | ||
transform: translateY(30%); | ||
} | ||
|
||
.upload > .progress rect.bar { | ||
fill: steelblue; | ||
} | ||
|
||
.upload > .progress line.end { | ||
stroke: lightgrey; | ||
stroke-width: 1; | ||
} | ||
|
||
</style> | ||
</head> | ||
<body> | ||
<h1>File Uploader example</h1> | ||
<div id="container"></div> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
|
||
const Type = require('union-type'); | ||
const T = require('ramda/src/T') | ||
, assoc = require('ramda/src/assoc') | ||
, curry = require('ramda/src/curry') | ||
, compose = require('ramda/src/compose') | ||
, map = require('ramda/src/map') | ||
, invoker = require('ramda/src/invoker') | ||
; | ||
const h = require('snabbdom/h'); | ||
|
||
const uploadList = require('./list'); | ||
const uploader = require('./uploader'); | ||
|
||
|
||
// action | ||
|
||
const listUpdate = (listAction,model) => { | ||
const [state, tasks] = uploadList.update(listAction, model.uploads); | ||
return [ assoc('uploads', state, model), | ||
tasks.map( map(Action.Route) ) | ||
]; | ||
} | ||
|
||
const Action = Type({ | ||
Create: [T, T], | ||
Route: [uploadList.Action] | ||
}); | ||
|
||
const update = Action.caseOn({ | ||
Create: (up,files,model) => { | ||
return listUpdate( uploadList.Action.Create(up,files), model ); | ||
}, | ||
|
||
Route: listUpdate | ||
}); | ||
|
||
|
||
// model | ||
|
||
const init = () => { return { uploads: uploadList.init() }; } | ||
|
||
// view | ||
|
||
const view = curry( ({url, headers, action$}, model) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That was intended to be in case you need to set headers on the XHR, but it doesn't need to be parameterized at the top level of the app, for same reason |
||
|
||
const up = uploader.upload(headers, url); | ||
|
||
const form = ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe making There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, I was not consistent about putting functions in the highest scope they can be, that's my preference too. |
||
h('form', {on: {submit: preventDefault} }, [ | ||
h('input', | ||
{ props: {type: 'file', multiple: true}, | ||
on: { | ||
change: compose(action$, Action.Create(up), getTarget('files')) | ||
} | ||
} | ||
) | ||
] | ||
) | ||
); | ||
|
||
return ( | ||
h('div.uploading', {}, [ | ||
form, | ||
uploadList.view(model.uploads) | ||
]) | ||
); | ||
}); | ||
|
||
const getTarget = curry( (key,e) => e.target[key] ); | ||
const preventDefault = invoker(0, 'preventDefault'); | ||
|
||
|
||
module.exports = { init, update, Action, view } | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about dropping the
{ return }
: