Skip to main content

leptos_forms_rs/components/
file_upload_input.rs

1use crate::core::types::{FieldValue, FileConstraints};
2use leptos::prelude::*;
3
4/// File upload input component with drag and drop support
5#[component]
6pub fn FileUploadInput(
7    /// Field name for the input
8    #[prop(into)]
9    name: String,
10    /// Current value of the field
11    #[prop(into)]
12    value: Signal<FieldValue>,
13    /// Callback when the value changes
14    #[prop(into)]
15    _on_change: Callback<FieldValue>,
16    /// Whether the field is required
17    #[prop(optional)]
18    required: Option<bool>,
19    /// Whether the field is disabled
20    #[prop(optional)]
21    disabled: Option<bool>,
22    /// CSS classes
23    #[prop(optional, into)]
24    class: Option<String>,
25    /// Error message to display
26    #[prop(optional, into)]
27    error: Option<String>,
28    /// Whether the field has an error
29    #[prop(optional)]
30    _has_error: Option<bool>,
31    /// File constraints (types, size limits, etc.)
32    #[prop(optional)]
33    constraints: Option<FileConstraints>,
34    /// Maximum number of files allowed
35    #[prop(optional)]
36    max_files: Option<usize>,
37    /// Whether to show file preview
38    #[prop(optional)]
39    show_preview: Option<bool>,
40    /// Whether to show upload progress
41    #[prop(optional)]
42    show_progress: Option<bool>,
43    /// Whether to allow multiple file selection
44    #[prop(optional)]
45    multiple: Option<bool>,
46) -> impl IntoView {
47    let max_files = max_files.unwrap_or(10);
48    let show_preview = show_preview.unwrap_or(true);
49    let _show_progress = show_progress.unwrap_or(true);
50    let multiple = multiple.unwrap_or(true);
51
52    let current_files = move || match value.get() {
53        FieldValue::Array(files) => files,
54        _ => Vec::new(),
55    };
56
57    let file_count = move || current_files().len();
58
59    let constraints_clone = constraints.clone();
60    let constraints_info = move || {
61        if let Some(constraints) = constraints_clone.as_ref() {
62            let mut info = Vec::new();
63
64            if let Some(max_size) = constraints.max_size {
65                info.push(format!("Max size: {}MB", max_size / (1024 * 1024)));
66            }
67
68            if !constraints.accept.is_empty() {
69                info.push(format!("Types: {}", constraints.accept.join(", ")));
70            }
71
72            if constraints.multiple {
73                info.push("Multiple files allowed".to_string());
74            }
75
76            info.join(" • ")
77        } else {
78            "No restrictions".to_string()
79        }
80    };
81
82    let file_list_view = move || {
83        if show_preview && file_count() > 0 {
84            Some(view! {
85                <div class="file-list">
86                    <div class="file-list-header">
87                        <h4>"Selected Files"</h4>
88                    </div>
89                    <div class="file-grid">
90                        {current_files().iter().map(|file| {
91                            match file {
92                                FieldValue::String(file_name) => {
93                                    view! {
94                                        <div class="file-preview">
95                                            <div class="file-icon">"📁"</div>
96                                            <div class="file-info">
97                                                <div class="file-name">{file_name.clone()}</div>
98                                                <div class="file-size">"Unknown size"</div>
99                                            </div>
100                                        </div>
101                                    }
102                                }
103                                _ => view! {
104                                    <div class="file-preview hidden">
105                                        <div class="file-icon">"📁"</div>
106                                        <div class="file-info">
107                                            <div class="file-name">{"Unknown file".to_string()}</div>
108                                            <div class="file-size">"Unknown size"</div>
109                                        </div>
110                                    </div>
111                                }
112                            }
113                        }).collect::<Vec<_>>()}
114                    </div>
115                </div>
116            })
117        } else {
118            None
119        }
120    };
121
122    let error_view = move || {
123        error.as_ref().map(|error_msg| {
124            view! {
125                <div class="error-message">
126                    {error_msg.clone()}
127                </div>
128            }
129        })
130    };
131
132    view! {
133        <div class={format!("file-upload-input {}", class.unwrap_or_default())}>
134            <div class="file-upload-area">
135                <div class="drop-zone">
136                    <div class="upload-icon">"📁"</div>
137                    <div class="upload-text">
138                        <p>"Drag and drop files here or"</p>
139                        <label class="file-input-label">
140                            <input
141                                type="file"
142                                name={name.clone()}
143                                multiple={constraints.as_ref().map(|c| c.multiple).unwrap_or(multiple)}
144                                accept={constraints.as_ref().map(|c| c.accept.join(",")).unwrap_or_default()}
145                                required={required}
146                                disabled={disabled}
147                                class="file-input"
148                            />
149                            "Browse Files"
150                        </label>
151                    </div>
152                    <div class="upload-constraints">{constraints_info()}</div>
153                    <div class="upload-limit">"Files: " {file_count} "/" {max_files}</div>
154                </div>
155            </div>
156
157            {file_list_view}
158            {error_view}
159        </div>
160    }
161}