Skip to content
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

Add file upload example and address issue #294 with solution. #868

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/advanced/dropfile-upload/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env"]
}
4 changes: 4 additions & 0 deletions examples/advanced/dropfile-upload/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
uploads
dist

5 changes: 5 additions & 0 deletions examples/advanced/dropfile-upload/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": false
}
16 changes: 16 additions & 0 deletions examples/advanced/dropfile-upload/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta name="description" content="Cycle.js example - File Input and Upload" />
<title>Cycle.js example - File Input and Upload</title>
</head>

<body>
<div id="main-container"></div>
<script src="./dist/main.js"></script>
</body>

</html>
35 changes: 35 additions & 0 deletions examples/advanced/dropfile-upload/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "example",
"version": "0.0.0",
"private": true,
"description": "file drag, drop, and upload example",
"main": "dist/main.js",
"author": "Robin Schulemann <rschulemann@gmail.com> (github.com/JuniperChicago)",
"license": "MIT",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"prebrowserify": "mkdirp dist",
"browserify": "browserify src/main.js -t babelify --outfile dist/main.js",
"start": "pnpm install && pnpm run browserify && echo 'OPEN index.html IN YOUR BROWSER'",
"server": "mkdirp uploads && node server/server"
},
"dependencies": {
"@cycle/dom": "^22.3.0",
"@cycle/http": "^15.1.0",
"@cycle/run": "^5.2.0",
"@cycle/state": "^1.1.0",
"xstream": "^11.7.0"
},
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/preset-env": "^7.2.3",
"babelify": "^10.0.0",
"browserify": "^16.2.3",
"express": "^4.16.4",
"mkdirp": "^0.5.1",
"multer": "^1.4.1",
"prettier": "^1.15.3",
"tsify": "^4.0.1",
"typescript": "^3.2.2"
}
}
38 changes: 38 additions & 0 deletions examples/advanced/dropfile-upload/server/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
const port = 3003;

// specify the folder
app.use(express.static(path.join(__dirname, 'uploads')));
// headers and content type
app.use(function(req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
);
next();
});

let storage = multer.diskStorage({
// destination
destination: function(req, file, cb) {
cb(null, './uploads');
},
filename: function(req, file, cb) {
cb(null, file.originalname);
},
});
let upload = multer({
storage: storage,
});

app.post('/upload', upload.array('files', 12), function(req, res) {
res.send(req.files);
});

let server = app.listen(port, function() {
console.log('Listening on port %s...', port);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export function httpRequest(stateStream) {
return stateStream
.filter(state => state.status === 'starting upload' && state.size > 0)
.map(function request(state) {
const data = new FormData();
for (let i = 0; i < state.files.length; i++) {
data.append('files', state.files[i]);
}
return {
url: 'http://localhost:3003/upload',
method: 'POST',
category: 'fileuploads',
send: data,
progress: true,
};
});
}

export function httpResponses(httpSource) {
const selected$ = httpSource.select('fileuploads').flatten();

const progress$ = selected$.filter(
resp => resp.loaded && resp.direction === 'upload'
);

const response$ = selected$.filter(resp => resp.status);

return {
progress$,
response$,
};
}
19 changes: 19 additions & 0 deletions examples/advanced/dropfile-upload/src/DropareaUploader/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import view from './view';
import intent from './intent';
import model from './model';
import {httpRequest} from './httpHelpers';

export default function dropareaUploader(sources) {
const {DOM, HTTP, state} = sources;

const actions = intent(DOM);
const reducer$ = model(actions, HTTP);
const request$ = httpRequest(state.stream);
const vtree$ = view(state.stream);
return {
DOM: vtree$,
HTTP: request$,
state: reducer$,
preventDefault: actions.preventDefault$,
};
}
37 changes: 37 additions & 0 deletions examples/advanced/dropfile-upload/src/DropareaUploader/intent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import xs from 'xstream';
import dropRepeats from 'xstream/extra/dropRepeats';
export default function intent(DOM) {
const dragoverEvent$ = DOM.select('#droparea').events('dragover');

const dragleaveEvent$ = DOM.select('#droparea').events('dragleave');

const dragenterEvent$ = DOM.select('#droparea').events('dragenter');

const dropFilesEvent$ = DOM.select('#droparea').events('drop');

const inputFileEvent$ = DOM.select('#form-file-input').events('change');

const startUploadEvent$ = DOM.select('#upload-button').events('click');

const dragoverMessage$ = dragoverEvent$.mapTo('dragover');
const dragleaveMessage$ = dragleaveEvent$.mapTo('dragleave');

const draggingEventMessages$ = xs
.merge(dragleaveMessage$, dragoverMessage$)
.compose(dropRepeats());

const preventDefault$ = xs.merge(
dragoverEvent$,
dragleaveEvent$,
dragenterEvent$,
dropFilesEvent$
);

return {
draggingEventMessages$,
dropFilesEvent$,
inputFileEvent$,
startUploadEvent$,
preventDefault$,
};
}
132 changes: 132 additions & 0 deletions examples/advanced/dropfile-upload/src/DropareaUploader/model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import xs from 'xstream';

import {httpResponses} from './httpHelpers';

const defaultState = {
dragging: false,
status: 'click below or drag&drop files!',
size: 0,
loaded: 0,
files: [],
};

export default function model(actions, http) {
const responseStreams = httpResponses(http);

const initReducer$ = xs.of(function initReducer(prevState) {
if (typeof prevState === 'undefined') {
return defaultState;
} else {
return prevState;
}
});

const droppedFiles$ = actions.dropFilesEvent$.map(function(ev) {
const {dataTransfer} = ev;
const filelist = dataTransfer ? dataTransfer.files : null;
return fileListTransformer(filelist);
});

const formFiles$ = actions.inputFileEvent$.map(function({target}) {
const {files} = target;
const filelist = target ? files : null;
return fileListTransformer(filelist);
});

const filesAdded$ = xs.merge(droppedFiles$, formFiles$);

const filesAddedReducer$ = filesAdded$.map(
({size, files}) =>
function filesAddedReducer(prevState) {
const {files: prevFiles, size: prevSize, dragging} = prevState;

return {
...prevState,
files: files.concat(prevFiles),
size: prevSize + size,
loaded: 0,
status: 'loaded',
dragging: false,
};
}
);

const draggingReducer$ = actions.draggingEventMessages$.map(
message =>
function draggingReducer(prevState) {
return {
...prevState,
dragging: message === 'dragover' ? true : false,
};
}
);

const startUploadReducer$ = actions.startUploadEvent$.map(
() =>
function startUploadReducer(prevState) {
const {files, status} = prevState;
return {
...prevState,
status: files.length > 0 ? 'starting upload' : status,
};
}
);

const httpProgressReducer$ = responseStreams.progress$.map(
response =>
function httpProgressReducer(prevState) {
const {loaded} = response;
const {size} = prevState;
return {
...prevState,
loaded,
status: `uploading ${unitHandler(loaded)} / ${unitHandler(size)}`,
};
}
);

const httpResponseReducer$ = responseStreams.response$.map(
response =>
function httpProgressReducer(prevState) {
return {
...prevState,
status: `${response.status} upload completed of ${unitHandler(
prevState.loaded
)}`,
};
}
);

return xs.merge(
initReducer$,
draggingReducer$,
filesAddedReducer$,
startUploadReducer$,
httpResponseReducer$,
httpProgressReducer$
);
}

function fileListTransformer(fileList) {
const files = [];
let size = 0;

if (fileList) {
for (let i = 0; i < fileList.length; i++) {
size += fileList[i].size;
files.push(fileList[i]);
}
}
return {
size,
files,
};
}

export function unitHandler(value) {
const unit = value > 1000000 ? 'MB' : 'KB';

return unit === 'MB'
? `${(value / 1000000).toFixed(2)} MB`
: `${(value / 1000).toFixed(2)} KB`;
}
Loading