Add files uploading support

This commit is contained in:
Denis Kolodin 2019-02-10 21:54:16 +03:00
parent b1eee76e31
commit 654716fe7e
7 changed files with 193 additions and 2 deletions

View File

@ -8,6 +8,13 @@
- Added `start_app` method. It's a shortcut to initialize a component and mount it to the body.
- Added handling of files of `input` element. There is new `ChangeData::Files` variant
of `onchange` handler.
- Added `ReaderService` to read data from `File` instances.
- New example `file_upload` that prints sizes of uploaded files.
### Bug fixes
## 0.5 - Released 2019-02-01

View File

@ -0,0 +1,8 @@
[package]
name = "file_upload"
version = "0.1.0"
authors = ["Denis Kolodin <deniskolodin@gmail.com>"]
edition = "2018"
[dependencies]
yew = { path = "../.." }

View File

@ -0,0 +1,71 @@
use yew::{html, ChangeData, Component, ComponentLink, Html, Renderable, ShouldRender};
use yew::services::reader::{File, FileData, ReaderService, ReaderTask};
pub struct Model {
link: ComponentLink<Model>,
reader: ReaderService,
tasks: Vec<ReaderTask>,
files: Vec<FileData>,
}
pub enum Msg {
Loaded(FileData),
Files(Vec<File>),
}
impl Component for Model {
type Message = Msg;
type Properties = ();
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
Model {
reader: ReaderService::new(),
link,
tasks: vec![],
files: vec![],
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::Loaded(file) => {
self.files.push(file);
}
Msg::Files(files) => {
for file in files.into_iter() {
let callback = self.link.send_back(Msg::Loaded);
let task = self.reader.read_file(file, callback);
self.tasks.push(task);
}
}
}
true
}
}
impl Renderable<Model> for Model {
fn view(&self) -> Html<Self> {
html! {
<div>
<input type="file", multiple=true, onchange=|value| {
let mut result = Vec::new();
if let ChangeData::Files(files) = value {
result.extend(files);
}
Msg::Files(result)
},/>
<ul>
{ for self.files.iter().map(|f| self.view_file(f)) }
</ul>
</div>
}
}
}
impl Model {
fn view_file(&self, file: &FileData) -> Html<Self> {
html! {
<li>{ format!("file: {}, size: {}", file.name, file.content.len()) }</li>
}
}
}

View File

@ -0,0 +1,3 @@
fn main() {
yew::start_app::<file_upload::Model>();
}

View File

@ -5,7 +5,7 @@
use std::rc::Rc;
use std::cell::RefCell;
use stdweb::web::{Element, EventListenerHandle, INode, Node};
use stdweb::web::{Element, EventListenerHandle, FileList, INode, Node};
use stdweb::web::html_element::SelectElement;
use virtual_dom::{Listener, VDiff, VNode};
use callback::Callback;
@ -361,12 +361,24 @@ impl_action! {
InputData { value }
}
onchange(event: ChangeEvent) -> ChangeData => |this: &Element, _| {
use stdweb::web::{FileList, IElement};
use stdweb::web::html_element::{InputElement, TextAreaElement, SelectElement};
use stdweb::unstable::TryInto;
match this.node_name().as_ref() {
"INPUT" => {
let input: InputElement = this.clone().try_into().unwrap();
ChangeData::Value(input.raw_value())
let is_file = input.get_attribute("type").map(|value| {
value.eq_ignore_ascii_case("file")
})
.unwrap_or(false);
if is_file {
let files: FileList = js!( return @{input}.files; )
.try_into()
.unwrap();
ChangeData::Files(files)
} else {
ChangeData::Value(input.raw_value())
}
}
"TEXTAREA" => {
let tae: TextAreaElement = this.clone().try_into().unwrap();
@ -407,6 +419,8 @@ pub enum ChangeData {
/// to collect your required data such as: `value`, `selected_index`, `selected_indices` or
/// `selected_values`. You can also iterate throught `selected_options` yourself.
Select(SelectElement),
/// Files
Files(FileList),
}
/// A bridging type for checking `href` attribute value.

View File

@ -7,6 +7,7 @@ pub mod console;
pub mod dialog;
pub mod fetch;
pub mod interval;
pub mod reader;
pub mod render;
pub mod storage;
pub mod timeout;
@ -16,6 +17,7 @@ pub use self::console::ConsoleService;
pub use self::dialog::DialogService;
pub use self::fetch::FetchService;
pub use self::interval::IntervalService;
pub use self::reader::ReaderService;
pub use self::render::RenderService;
pub use self::storage::StorageService;
pub use self::timeout::TimeoutService;

86
src/services/reader.rs Normal file
View File

@ -0,0 +1,86 @@
//! Service to load files using `FileReader`.
pub use stdweb::web::File;
use stdweb::web::{
IEventTarget,
FileReader,
FileReaderReadyState,
FileReaderResult,
TypedArray,
};
use stdweb::web::event::{
LoadEndEvent,
};
use callback::Callback;
use super::Task;
/// Struct that represents data of file.
#[derive(Clone, Debug)]
pub struct FileData {
/// Name of loaded file.
pub name: String,
/// Content of loaded file.
pub content: Vec<u8>,
}
/// A reader service attached to a user context.
#[derive(Default)]
pub struct ReaderService {}
impl ReaderService {
/// Creates a new service instance connected to `App` by provided `sender`.
pub fn new() -> Self {
Self {}
}
/// Reads all bytes from files and returns them with a callback.
pub fn read_file(&mut self, file: File, callback: Callback<FileData>) -> ReaderTask {
let file_reader = FileReader::new();
let reader = file_reader.clone();
let name = file.name();
file_reader.add_event_listener(move |event: LoadEndEvent| {
match reader.result() {
Some(FileReaderResult::String(_)) => {
unreachable!();
}
Some(FileReaderResult::ArrayBuffer(buffer)) => {
let array: TypedArray<u8> = buffer.into();
let data = FileData {
name: name.clone(),
content: array.to_vec(),
};
callback.emit(data);
}
None => { }
}
});
file_reader.read_as_array_buffer(&file).unwrap();
ReaderTask {
file_reader,
}
}
}
/// A handle to control reading.
#[must_use]
pub struct ReaderTask {
file_reader: FileReader,
}
impl Task for ReaderTask {
fn is_active(&self) -> bool {
self.file_reader.ready_state() == FileReaderReadyState::Loading
}
fn cancel(&mut self) {
self.file_reader.abort();
}
}
impl Drop for ReaderTask {
fn drop(&mut self) {
if self.is_active() {
self.cancel();
}
}
}