use std::rc::Rc;
use web_sys::{Event, FocusEvent};
use yew::services::reader::{File, FileData, ReaderService, ReaderTask};
use yew::services::Task;
use yew::{
html, Callback, ChangeData, Component, ComponentLink, Html, InputData, Properties, ShouldRender,
};
use yew_state::{GlobalHandle, SharedState, SharedStateComponent};
type ViewForm<T> = Rc<dyn Fn(FormHandle<T>) -> Html>;
pub struct FormHandle<'a, T>
where
T: Default + Clone + 'static,
{
handle: &'a GlobalHandle<T>,
link: &'a ComponentLink<Model<T>>,
}
impl<'a, T> FormHandle<'a, T>
where
T: Default + Clone + 'static,
{
pub fn state(&self) -> &T {
self.handle.state()
}
pub fn set<E: 'static>(&self, f: impl FnOnce(&mut T) + 'static) -> Callback<E> {
self.handle.reduce_callback_once(f)
}
pub fn set_with<E: 'static>(&self, f: impl FnOnce(&mut T, E) + 'static) -> Callback<E> {
self.handle.reduce_callback_once_with(f)
}
pub fn set_text(&self, f: impl FnOnce(&mut T, String) + 'static) -> Callback<InputData> {
self.handle
.reduce_callback_once_with(f)
.reform(|data: InputData| data.value)
}
pub fn set_select(&self, f: impl FnOnce(&mut T, String) + 'static) -> Callback<ChangeData> {
self.handle
.reduce_callback_once_with(f)
.reform(|data: ChangeData| {
if let ChangeData::Select(el) = data {
el.value()
} else {
panic!("Select element is required")
}
})
}
pub fn set_file(
&self,
f: impl FnOnce(&mut T, FileData) + Copy + 'static,
) -> Callback<ChangeData> {
let set_files = self.set_with(f);
self.link.callback(move |data| {
let mut result = Vec::new();
if let ChangeData::Files(files) = data {
let files = js_sys::try_iter(&files)
.unwrap()
.unwrap()
.into_iter()
.map(|v| File::from(v.unwrap()));
result.extend(files);
}
Msg::Files(result, set_files.clone())
})
}
}
#[derive(Properties, Clone)]
pub struct Props<T>
where
T: Default + Clone + 'static,
{
#[prop_or_default]
handle: GlobalHandle<T>,
#[prop_or_default]
pub on_submit: Callback<T>,
#[prop_or_default]
pub default: T,
#[prop_or_default]
pub auto_reset: bool,
pub view: ViewForm<T>,
}
impl<T> SharedState for Props<T>
where
T: Default + Clone + 'static,
{
type Handle = GlobalHandle<T>;
fn handle(&mut self) -> &mut Self::Handle {
&mut self.handle
}
}
pub enum Msg {
Files(Vec<File>, Callback<FileData>),
Submit(FocusEvent),
}
pub struct Model<T>
where
T: Default + Clone + 'static,
{
props: Props<T>,
cb_submit: Callback<FocusEvent>,
cb_reset: Callback<()>,
link: ComponentLink<Self>,
file_reader: ReaderService,
tasks: Vec<ReaderTask>,
}
impl<T> Component for Model<T>
where
T: Default + Clone + 'static,
{
type Message = Msg;
type Properties = Props<T>;
fn create(mut props: Self::Properties, link: ComponentLink<Self>) -> Self {
let cb_submit = link.callback(|e: FocusEvent| {
e.prevent_default();
Msg::Submit(e)
});
let default = props.default.clone();
let cb_reset = props
.handle()
.reduce_callback(move |state| *state = default.clone());
cb_reset.emit(());
Self {
props,
cb_submit,
cb_reset,
link,
tasks: Default::default(),
file_reader: Default::default(),
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::Submit(e) => {
self.props.on_submit.emit(self.props.handle.state().clone());
if self.props.auto_reset {
let reset_event = Event::new("reset").unwrap();
e.target()
.map(|target| target.dispatch_event(&reset_event).ok());
self.cb_reset.emit(());
}
false
}
Msg::Files(files, cb) => {
self.tasks.retain(Task::is_active);
for file in files.into_iter() {
let task = self
.file_reader
.read_file(file, cb.clone())
.expect("Error reading file");
self.tasks.push(task);
}
false
}
}
}
fn view(&self) -> Html {
let handle = FormHandle {
handle: &self.props.handle,
link: &self.link,
};
html! {
<form onreset = self.cb_reset.reform(|_| ()) onsubmit = self.cb_submit.clone()>
{ (self.props.view)(handle) }
</form>
}
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.props = props;
true
}
}
pub type Form<T> = SharedStateComponent<Model<T>>;
pub fn view_form<T: Default + Clone>(f: impl Fn(FormHandle<T>) -> Html + 'static) -> ViewForm<T> {
Rc::new(f)
}