1use std::rc::Rc;
2
3use web_sys::{Event, FocusEvent};
4use yew::services::reader::{File, FileData, ReaderService, ReaderTask};
5use yew::services::Task;
6use yew::{
7 html, Callback, ChangeData, Component, ComponentLink, Html, InputData, Properties, ShouldRender,
8};
9use yew_state::{GlobalHandle, SharedState, SharedStateComponent};
10
11type ViewForm<T> = Rc<dyn Fn(FormHandle<T>) -> Html>;
12
13pub struct FormHandle<'a, T>
14where
15 T: Default + Clone + 'static,
16{
17 handle: &'a GlobalHandle<T>,
18 link: &'a ComponentLink<Model<T>>,
19}
20
21impl<'a, T> FormHandle<'a, T>
22where
23 T: Default + Clone + 'static,
24{
25 pub fn state(&self) -> &T {
27 self.handle.state()
28 }
29
30 pub fn set<E: 'static>(&self, f: impl FnOnce(&mut T) + 'static) -> Callback<E> {
32 self.handle.reduce_callback_once(f)
33 }
34
35 pub fn set_with<E: 'static>(&self, f: impl FnOnce(&mut T, E) + 'static) -> Callback<E> {
37 self.handle.reduce_callback_once_with(f)
38 }
39
40 pub fn set_text(&self, f: impl FnOnce(&mut T, String) + 'static) -> Callback<InputData> {
42 self.handle
43 .reduce_callback_once_with(f)
44 .reform(|data: InputData| data.value)
45 }
46
47 pub fn set_select(&self, f: impl FnOnce(&mut T, String) + 'static) -> Callback<ChangeData> {
53 self.handle
54 .reduce_callback_once_with(f)
55 .reform(|data: ChangeData| {
56 if let ChangeData::Select(el) = data {
57 el.value()
58 } else {
59 panic!("Select element is required")
60 }
61 })
62 }
63
64 pub fn set_file(
66 &self,
67 f: impl FnOnce(&mut T, FileData) + Copy + 'static,
68 ) -> Callback<ChangeData> {
69 let set_files = self.set_with(f);
70 self.link.callback(move |data| {
71 let mut result = Vec::new();
72 if let ChangeData::Files(files) = data {
73 let files = js_sys::try_iter(&files)
74 .unwrap()
75 .unwrap()
76 .into_iter()
77 .map(|v| File::from(v.unwrap()));
78 result.extend(files);
79 }
80 Msg::Files(result, set_files.clone())
81 })
82 }
83}
84
85#[derive(Properties, Clone)]
86pub struct Props<T>
87where
88 T: Default + Clone + 'static,
89{
90 #[prop_or_default]
91 handle: GlobalHandle<T>,
92 #[prop_or_default]
93 pub on_submit: Callback<T>,
94 #[prop_or_default]
95 pub default: T,
96 #[prop_or_default]
97 pub auto_reset: bool,
98 pub view: ViewForm<T>,
99 }
102
103impl<T> SharedState for Props<T>
104where
105 T: Default + Clone + 'static,
106{
107 type Handle = GlobalHandle<T>;
108
109 fn handle(&mut self) -> &mut Self::Handle {
110 &mut self.handle
111 }
112}
113
114pub enum Msg {
115 Files(Vec<File>, Callback<FileData>),
116 Submit(FocusEvent),
117}
118
119pub struct Model<T>
120where
121 T: Default + Clone + 'static,
122{
123 props: Props<T>,
124 cb_submit: Callback<FocusEvent>,
125 cb_reset: Callback<()>,
126 link: ComponentLink<Self>,
127 file_reader: ReaderService,
128 tasks: Vec<ReaderTask>,
129}
130
131impl<T> Component for Model<T>
132where
133 T: Default + Clone + 'static,
134{
135 type Message = Msg;
136 type Properties = Props<T>;
137
138 fn create(mut props: Self::Properties, link: ComponentLink<Self>) -> Self {
139 let cb_submit = link.callback(|e: FocusEvent| {
140 e.prevent_default();
141 Msg::Submit(e)
142 });
143 let default = props.default.clone();
144 let cb_reset = props
145 .handle()
146 .reduce_callback(move |state| *state = default.clone());
147 cb_reset.emit(());
149
150 Self {
151 props,
152 cb_submit,
153 cb_reset,
154 link,
155 tasks: Default::default(),
156 file_reader: Default::default(),
157 }
158 }
159
160 fn update(&mut self, msg: Self::Message) -> ShouldRender {
161 match msg {
162 Msg::Submit(e) => {
163 self.props.on_submit.emit(self.props.handle.state().clone());
164 if self.props.auto_reset {
165 let reset_event = Event::new("reset").unwrap();
167 e.target()
168 .map(|target| target.dispatch_event(&reset_event).ok());
169 self.cb_reset.emit(());
171 }
172 false
173 }
174 Msg::Files(files, cb) => {
175 self.tasks.retain(Task::is_active);
176 for file in files.into_iter() {
177 let task = self
178 .file_reader
179 .read_file(file, cb.clone())
180 .expect("Error reading file");
181
182 self.tasks.push(task);
183 }
184 false
185 }
186 }
187 }
188
189 fn view(&self) -> Html {
190 let handle = FormHandle {
191 handle: &self.props.handle,
192 link: &self.link,
193 };
194 html! {
195 <form onreset = self.cb_reset.reform(|_| ()) onsubmit = self.cb_submit.clone()>
196 { (self.props.view)(handle) }
197 </form>
198 }
199 }
200
201 fn change(&mut self, props: Self::Properties) -> ShouldRender {
202 self.props = props;
203 true
204 }
205}
206
207pub type Form<T> = SharedStateComponent<Model<T>>;
208
209pub fn view_form<T: Default + Clone>(f: impl Fn(FormHandle<T>) -> Html + 'static) -> ViewForm<T> {
210 Rc::new(f)
211}