-
Notifications
You must be signed in to change notification settings - Fork 22
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
Refine microcosm-http. and microcosm-dom Add file-uploader example #509
Changes from all commits
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 @@ | ||
public/uploads/* |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
"name": "examples-file-uploads", | ||
"version": "0.0.0", | ||
"private": true, | ||
"license": "MIT", | ||
"scripts": { | ||
"start": "webpack-dev-server --mode=development" | ||
}, | ||
"dependencies": { | ||
"babel-loader": "^7.1.2", | ||
"body-parser": "^1.18.2", | ||
"html-webpack-plugin": "^3.0.6", | ||
"multer": "^1.3.0", | ||
"react": "^16.3.2", | ||
"react-dom": "^16.3.2", | ||
"webpack": "^4.6.0", | ||
"webpack-dev-server": "^3.1.3" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import http from 'microcosm-http' | ||
|
||
export const uploadFile = http.prepare({ method: 'POST', url: '/files' }) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import React from 'react' | ||
import DOM from 'react-dom' | ||
import { FileUploader } from './views/file-uploader' | ||
|
||
DOM.render(<FileUploader />, document.getElementById('app')) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import React from 'react' | ||
import { uploadFile } from '../actions/files' | ||
import { Subject } from 'microcosm' | ||
import { Presenter, ActionForm } from 'microcosm-dom' | ||
import { Progress } from './progress' | ||
|
||
function asFormData(form) { | ||
return { data: new FormData(form) } | ||
} | ||
|
||
export class FileUploader extends Presenter { | ||
state = { | ||
status: 'inactive', | ||
file: undefined | ||
} | ||
|
||
queue = new Subject() | ||
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. is 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'm using it waaay more than Observable. It's a way to get the observable API externally, like this, but inside out:
Can be:
The advantage of an observable is that the behavior inside is lazy based on if anything subscribes to it. Subjects are more active. 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's awesome. Not sure if 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. It is. It comes from Observables literature, but I agree that it's vague. I'd be happy to figure out another name. Working more on scheduler stuff, I have some times wondered about calling this 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. Issue filed: |
||
|
||
render() { | ||
let { status, file } = this.state | ||
|
||
return ( | ||
<ActionForm | ||
action={uploadFile} | ||
serializer={asFormData} | ||
queue={this.queue} | ||
onSend={this.trackProgress} | ||
> | ||
<Progress status={status} file={file} onCancel={this.queue.clear} /> | ||
|
||
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. New here: you can pass a New here: |
||
<label htmlFor="files">Upload a file</label> | ||
<input id="file" multiple name="files" type="file" /> | ||
|
||
<footer> | ||
<button>Upload</button> | ||
</footer> | ||
</ActionForm> | ||
) | ||
} | ||
|
||
trackProgress = action => { | ||
action.every(iteration => | ||
this.setState({ status: iteration.status, file: iteration.payload }) | ||
) | ||
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. New here: |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import React from 'react' | ||
|
||
function Loading({ file, onCancel }) { | ||
return ( | ||
<div> | ||
<p>Your files are uploading...</p> | ||
<progress value={file.progress} /> | ||
<button type="button" onClick={onCancel}> | ||
Cancel | ||
</button> | ||
</div> | ||
) | ||
} | ||
|
||
export function Progress({ status, file, onCancel }) { | ||
switch (status) { | ||
case 'next': | ||
return <Loading file={file} onCancel={onCancel} /> | ||
case 'error': | ||
return <p className="msg error">{file.message}</p> | ||
case 'complete': | ||
return <p className="msg success">Files sent!</p> | ||
case 'cancel': | ||
return <p className="msg error">File upload cancelled</p> | ||
default: | ||
return null | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
const HTMLWebpackPlugin = require('html-webpack-plugin') | ||
const path = require('path') | ||
const multer = require('multer') | ||
const bodyParser = require('body-parser') | ||
|
||
module.exports = { | ||
plugins: [ | ||
new HTMLWebpackPlugin({ | ||
template: 'public/index.html' | ||
}) | ||
], | ||
resolve: { | ||
alias: { | ||
microcosm: path.resolve(__dirname, '../../microcosm/src/'), | ||
'microcosm-http': path.resolve( | ||
__dirname, | ||
'../../microcosm-http/src/http.js' | ||
), | ||
'microcosm-dom': path.resolve(__dirname, '../../microcosm-dom/src/react') | ||
} | ||
}, | ||
module: { | ||
rules: [ | ||
{ | ||
test: /\.js/, | ||
loader: 'babel-loader', | ||
exclude: /node_modules/ | ||
} | ||
] | ||
}, | ||
devServer: { | ||
before: app => { | ||
const upload = multer({ dest: `${__dirname}/public/uploads` }) | ||
|
||
app.use(bodyParser.json()) | ||
app.use(bodyParser.urlencoded({ extended: true })) | ||
|
||
app.post('/files', upload.array('files', 100), function(req, res, next) { | ||
res.json(req.body) | ||
}) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,7 @@ export function generateActionButton(createElement, Component) { | |
constructor() { | ||
super(...arguments) | ||
|
||
this.queue = new Subject('action-button') | ||
this.queue = this.props.queue || new Subject() | ||
this._onClick = this._onClick.bind(this) | ||
} | ||
|
||
|
@@ -15,7 +15,7 @@ export function generateActionButton(createElement, Component) { | |
} | ||
|
||
componentWillUnmount() { | ||
this.queue.cancel() | ||
this.queue.complete() | ||
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 want to cancel. That would cancel any outstanding actions, which might not be what the user wants. If that is the desired behavior, you can now do that yourself. |
||
} | ||
|
||
render() { | ||
|
@@ -24,47 +24,45 @@ export function generateActionButton(createElement, Component) { | |
delete props.tag | ||
delete props.action | ||
delete props.value | ||
delete props.onStart | ||
delete props.onSend | ||
delete props.onNext | ||
delete props.onComplete | ||
delete props.onChange | ||
delete props.onError | ||
delete props.onCancel | ||
delete props.send | ||
delete props.prepare | ||
|
||
if (this.props.tag === 'button' && props.type == null) { | ||
props.type = 'button' | ||
} | ||
|
||
return createElement(this.props.tag, props) | ||
} | ||
|
||
click() { | ||
let { action, prepare, value } = this.props | ||
|
||
let params = prepare(value) | ||
let result = this.send(action, params) | ||
let result = this.send(this.props.action, this._parameterize()) | ||
let action = Subject.hash(result) | ||
|
||
if (result && 'subscribe' in result) { | ||
this._onChange('start', result) | ||
this.props.onSend(action) | ||
|
||
result.subscribe({ | ||
error: this._onChange.bind(this, 'error', result), | ||
next: this._onChange.bind(this, 'next', result), | ||
complete: this._onChange.bind(this, 'complete', result), | ||
cancel: this._onChange.bind(this, 'cancel', result) | ||
}) | ||
let tracker = action.every(this._onChange, this) | ||
|
||
this.queue.subscribe(result) | ||
} | ||
this.queue.subscribe({ | ||
error: tracker.unsubscribe, | ||
complete: tracker.unsubscribe, | ||
cancel: action.cancel | ||
}) | ||
|
||
return result | ||
return action | ||
} | ||
|
||
// Private --------------------------------------------------- // | ||
|
||
_onChange(status, result) { | ||
this.props[toCallbackName(status)](result.payload, result.meta) | ||
_parameterize() { | ||
let { value, prepare } = this.props | ||
|
||
return prepare(value) | ||
} | ||
|
||
_onChange(action) { | ||
this.props[toCallbackName(action.status)](action) | ||
} | ||
|
||
_onClick(event) { | ||
|
@@ -80,12 +78,14 @@ export function generateActionButton(createElement, Component) { | |
ActionButton.defaultProps = { | ||
action: 'no-action', | ||
onClick: identity, | ||
onStart: identity, | ||
onSend: identity, | ||
onNext: identity, | ||
onComplete: identity, | ||
onError: identity, | ||
onChange: identity, | ||
onCancel: identity, | ||
prepare: identity, | ||
queue: null, | ||
send: null, | ||
tag: 'button', | ||
value: null | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,7 @@ export function generateActionForm(createElement, Component) { | |
constructor() { | ||
super(...arguments) | ||
|
||
this.queue = new Subject('action-form') | ||
this.queue = this.props.queue || new Subject() | ||
this._onSubmit = this._onSubmit.bind(this) | ||
} | ||
|
||
|
@@ -16,7 +16,7 @@ export function generateActionForm(createElement, Component) { | |
} | ||
|
||
componentWillUnmount() { | ||
this.queue.cancel() | ||
this.queue.complete() | ||
} | ||
|
||
render() { | ||
|
@@ -29,7 +29,7 @@ export function generateActionForm(createElement, Component) { | |
delete props.action | ||
delete props.prepare | ||
delete props.serializer | ||
delete props.onStart | ||
delete props.onSend | ||
delete props.onNext | ||
delete props.onComplete | ||
delete props.onNext | ||
|
@@ -43,21 +43,19 @@ export function generateActionForm(createElement, Component) { | |
|
||
submit(event) { | ||
let result = this.send(this.props.action, this._parameterize()) | ||
let action = Subject.hash(result) | ||
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. Wrapping the result in
That means you can pass anything into the So you don't really need a repo. Just a send method: let send = () => Promise.resolve(true)
let callback = (subject) => {}
<ActionButton send={send} onSend={callback}/> |
||
|
||
if (result && 'subscribe' in result) { | ||
this._onChange('start', result) | ||
this.props.onSend(action) | ||
|
||
result.subscribe({ | ||
error: this._onChange.bind(this, 'error', result), | ||
next: this._onChange.bind(this, 'next', result), | ||
complete: this._onChange.bind(this, 'complete', result), | ||
cancel: this._onChange.bind(this, 'cancel', result) | ||
}) | ||
let tracker = action.every(this._onChange, this) | ||
|
||
this.queue.subscribe(result) | ||
} | ||
this.queue.subscribe({ | ||
error: tracker.unsubscribe, | ||
complete: tracker.unsubscribe, | ||
cancel: action.cancel | ||
}) | ||
|
||
return result | ||
return action | ||
} | ||
|
||
// Private --------------------------------------------------- // | ||
|
@@ -70,8 +68,8 @@ export function generateActionForm(createElement, Component) { | |
: prepare(serializer(this._form)) | ||
} | ||
|
||
_onChange(status, result) { | ||
this.props[toCallbackName(status)](result.payload, result.meta) | ||
_onChange(action) { | ||
this.props[toCallbackName(action.status)](action) | ||
} | ||
|
||
_onSubmit(event) { | ||
|
@@ -87,12 +85,14 @@ export function generateActionForm(createElement, Component) { | |
ActionForm.defaultProps = { | ||
action: 'no-action', | ||
onSubmit: identity, | ||
onStart: identity, | ||
onSend: identity, | ||
onNext: identity, | ||
onComplete: identity, | ||
onChange: identity, | ||
onError: identity, | ||
onCancel: identity, | ||
prepare: identity, | ||
queue: null, | ||
send: null, | ||
tag: 'form', | ||
serializer: form => serialize(form, { hash: true, empty: true }) | ||
|
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's
FormData
?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.
I think a part of XMLHttpRequest 2.0:
https://developer.mozilla.org/en-US/docs/Web/API/FormData
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.
Ahh, is it naive to think that this would be a reasonable default for the
serializer
ActionForm property?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.
Actually I was just typing this up. I'd be okay with dropping
form-serialize
and just making this the default. I wonder what turning FormData into JSON would look like.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.
Issue filed:
#510