yew_input/
lib.rs

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    /// Current form state.
26    pub fn state(&self) -> &T {
27        self.handle.state()
28    }
29
30    /// Callback that sets state, ignoring callback event.
31    pub fn set<E: 'static>(&self, f: impl FnOnce(&mut T) + 'static) -> Callback<E> {
32        self.handle.reduce_callback_once(f)
33    }
34
35    /// Callback that sets state from callback event
36    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    /// Callback for setting state from `InputData`.
41    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    /// Callback for setting state from select elements.
48    ///
49    /// # Panics
50    ///
51    /// Panics if used on anything other than a select element.
52    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    /// Callback for setting files
65    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    // #[prop_or_default]
100    // pub errors: InputErrors
101}
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        // Make sure default is set.
148        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                    // Clear form
166                    let reset_event = Event::new("reset").unwrap();
167                    e.target()
168                        .map(|target| target.dispatch_event(&reset_event).ok());
169                    // Reset state
170                    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}