@wcstack/upload 1.9.1 → 1.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +367 -282
- package/README.md +367 -282
- package/dist/auto.js +3 -3
- package/dist/auto.min.js +3 -3
- package/dist/index.d.ts +17 -1
- package/dist/index.esm.js +67 -6
- package/dist/index.esm.js.map +1 -1
- package/dist/index.esm.min.js +1 -1
- package/dist/index.esm.min.js.map +1 -1
- package/package.json +71 -71
package/README.md
CHANGED
|
@@ -1,283 +1,368 @@
|
|
|
1
|
-
# @wcstack/upload
|
|
2
|
-
|
|
3
|
-
`@wcstack/upload` is a declarative file upload component for the wcstack ecosystem.
|
|
4
|
-
|
|
5
|
-
It is not a visible UI widget.
|
|
6
|
-
It is a hidden **upload I/O node** that turns file upload into bindable state.
|
|
7
|
-
|
|
8
|
-
With `@wcstack/state`, `<wcs-upload>` exposes a small async state surface:
|
|
9
|
-
|
|
10
|
-
- input / command surface: `files`, `trigger`
|
|
11
|
-
- configuration surface: `url`, `method`, `field-name`, `accept`, `max-size`, `manual`, `multiple`
|
|
12
|
-
- output state surface: `value`, `loading`, `progress`, `error`, `status`
|
|
13
|
-
|
|
14
|
-
This means file upload can be expressed as state transitions and DOM bindings instead of ad-hoc `XMLHttpRequest` glue code.
|
|
15
|
-
|
|
16
|
-
`@wcstack/upload` follows the
|
|
17
|
-
|
|
18
|
-
- **Core** (`UploadCore`) handles XHR upload, progress tracking, abort, and async state
|
|
19
|
-
- **Shell** (`<wcs-upload>`) exposes that state as a custom element and
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
## Why this exists
|
|
23
|
-
|
|
24
|
-
File upload usually spreads across too many places:
|
|
25
|
-
|
|
26
|
-
- file input handling
|
|
27
|
-
- `FormData` creation
|
|
28
|
-
- progress events
|
|
29
|
-
- loading flags
|
|
30
|
-
- error handling
|
|
31
|
-
- abort on disconnect
|
|
32
|
-
|
|
33
|
-
`@wcstack/upload` moves that logic into a reusable component and exposes the result as bindable state.
|
|
34
|
-
|
|
35
|
-
## Install
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
npm install @wcstack/upload
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Quick Start
|
|
42
|
-
|
|
43
|
-
### 1. Auto upload when files are assigned
|
|
44
|
-
|
|
45
|
-
```html
|
|
46
|
-
<script type="module" src="https://esm.run/@wcstack/upload/auto"></script>
|
|
47
|
-
|
|
48
|
-
<wcs-upload id="avatar-upload" url="/api/upload"></wcs-upload>
|
|
49
|
-
<input id="avatar-input" type="file" accept="image/*">
|
|
50
|
-
|
|
51
|
-
<script type="module">
|
|
52
|
-
const upload = document.getElementById("avatar-upload");
|
|
53
|
-
const input = document.getElementById("avatar-input");
|
|
54
|
-
|
|
55
|
-
input.addEventListener("change", () => {
|
|
56
|
-
upload.files = input.files;
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
upload.addEventListener("wcs-upload:progress", (event) => {
|
|
60
|
-
console.log("progress", event.detail);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
upload.addEventListener("wcs-upload:response", (event) => {
|
|
64
|
-
console.log("uploaded", event.detail.value);
|
|
65
|
-
});
|
|
66
|
-
</script>
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Default behavior:
|
|
70
|
-
|
|
71
|
-
- assigning `files` starts upload immediately
|
|
72
|
-
- files are sent as `multipart/form-data`
|
|
73
|
-
- request method defaults to `POST`
|
|
74
|
-
- field name defaults to `file`
|
|
75
|
-
|
|
76
|
-
### 2. Manual upload with `trigger`
|
|
77
|
-
|
|
78
|
-
Use `manual` when you want to choose files first and upload later.
|
|
79
|
-
|
|
80
|
-
```html
|
|
81
|
-
<script type="module" src="https://esm.run/@wcstack/upload/auto"></script>
|
|
82
|
-
|
|
83
|
-
<wcs-upload id="resume-upload" url="/api/upload" manual></wcs-upload>
|
|
84
|
-
|
|
85
|
-
<input id="resume-input" type="file">
|
|
86
|
-
<button id="resume-button">Upload</button>
|
|
87
|
-
|
|
88
|
-
<script type="module">
|
|
89
|
-
const upload = document.getElementById("resume-upload");
|
|
90
|
-
const input = document.getElementById("resume-input");
|
|
91
|
-
const button = document.getElementById("resume-button");
|
|
92
|
-
|
|
93
|
-
input.addEventListener("change", () => {
|
|
94
|
-
upload.files = input.files;
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
button.addEventListener("click", () => {
|
|
98
|
-
upload.trigger = true;
|
|
99
|
-
});
|
|
100
|
-
</script>
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
`trigger` is a one-way command surface:
|
|
104
|
-
|
|
105
|
-
- writing `true` starts `upload()`
|
|
106
|
-
- after completion it resets itself to `false`
|
|
107
|
-
- that reset dispatches `wcs-upload:trigger-changed`
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
input.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
input.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
- the
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
|
190
|
-
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
-
|
|
244
|
-
|
|
245
|
-
- `
|
|
246
|
-
- `
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
1
|
+
# @wcstack/upload
|
|
2
|
+
|
|
3
|
+
`@wcstack/upload` is a declarative file upload component for the wcstack ecosystem.
|
|
4
|
+
|
|
5
|
+
It is not a visible UI widget.
|
|
6
|
+
It is a hidden **upload I/O node** that turns file upload into bindable state.
|
|
7
|
+
|
|
8
|
+
With `@wcstack/state`, `<wcs-upload>` exposes a small async state surface:
|
|
9
|
+
|
|
10
|
+
- input / command surface: `files`, `trigger`
|
|
11
|
+
- configuration surface: `url`, `method`, `field-name`, `accept`, `max-size`, `manual`, `multiple`
|
|
12
|
+
- output state surface: `value`, `loading`, `progress`, `error`, `status`
|
|
13
|
+
|
|
14
|
+
This means file upload can be expressed as state transitions and DOM bindings instead of ad-hoc `XMLHttpRequest` glue code.
|
|
15
|
+
|
|
16
|
+
`@wcstack/upload` follows the [CSBC](https://github.com/csbc-dev/arch/blob/main/README.md) (Core / Shell / Binding Contract) architecture:
|
|
17
|
+
|
|
18
|
+
- **Core** (`UploadCore`) handles XHR upload, progress tracking, abort, and async state
|
|
19
|
+
- **Shell** (`<wcs-upload>`) exposes that state as a custom element and DOM-facing runtime surface
|
|
20
|
+
- **Binding Contract** (`static wcBindable`) declares observable `properties`, writable `inputs`, and callable `commands`
|
|
21
|
+
|
|
22
|
+
## Why this exists
|
|
23
|
+
|
|
24
|
+
File upload usually spreads across too many places:
|
|
25
|
+
|
|
26
|
+
- file input handling
|
|
27
|
+
- `FormData` creation
|
|
28
|
+
- progress events
|
|
29
|
+
- loading flags
|
|
30
|
+
- error handling
|
|
31
|
+
- abort on disconnect
|
|
32
|
+
|
|
33
|
+
`@wcstack/upload` moves that logic into a reusable component and exposes the result as bindable state.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @wcstack/upload
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
### 1. Auto upload when files are assigned
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<script type="module" src="https://esm.run/@wcstack/upload/auto"></script>
|
|
47
|
+
|
|
48
|
+
<wcs-upload id="avatar-upload" url="/api/upload"></wcs-upload>
|
|
49
|
+
<input id="avatar-input" type="file" accept="image/*">
|
|
50
|
+
|
|
51
|
+
<script type="module">
|
|
52
|
+
const upload = document.getElementById("avatar-upload");
|
|
53
|
+
const input = document.getElementById("avatar-input");
|
|
54
|
+
|
|
55
|
+
input.addEventListener("change", () => {
|
|
56
|
+
upload.files = input.files;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
upload.addEventListener("wcs-upload:progress", (event) => {
|
|
60
|
+
console.log("progress", event.detail);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
upload.addEventListener("wcs-upload:response", (event) => {
|
|
64
|
+
console.log("uploaded", event.detail.value);
|
|
65
|
+
});
|
|
66
|
+
</script>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Default behavior:
|
|
70
|
+
|
|
71
|
+
- assigning `files` starts upload immediately
|
|
72
|
+
- files are sent as `multipart/form-data`
|
|
73
|
+
- request method defaults to `POST`
|
|
74
|
+
- field name defaults to `file`
|
|
75
|
+
|
|
76
|
+
### 2. Manual upload with `trigger`
|
|
77
|
+
|
|
78
|
+
Use `manual` when you want to choose files first and upload later.
|
|
79
|
+
|
|
80
|
+
```html
|
|
81
|
+
<script type="module" src="https://esm.run/@wcstack/upload/auto"></script>
|
|
82
|
+
|
|
83
|
+
<wcs-upload id="resume-upload" url="/api/upload" manual></wcs-upload>
|
|
84
|
+
|
|
85
|
+
<input id="resume-input" type="file">
|
|
86
|
+
<button id="resume-button">Upload</button>
|
|
87
|
+
|
|
88
|
+
<script type="module">
|
|
89
|
+
const upload = document.getElementById("resume-upload");
|
|
90
|
+
const input = document.getElementById("resume-input");
|
|
91
|
+
const button = document.getElementById("resume-button");
|
|
92
|
+
|
|
93
|
+
input.addEventListener("change", () => {
|
|
94
|
+
upload.files = input.files;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
button.addEventListener("click", () => {
|
|
98
|
+
upload.trigger = true;
|
|
99
|
+
});
|
|
100
|
+
</script>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`trigger` is a one-way command surface:
|
|
104
|
+
|
|
105
|
+
- writing `true` starts `upload()`
|
|
106
|
+
- after completion it resets itself to `false`
|
|
107
|
+
- that reset dispatches `wcs-upload:trigger-changed`
|
|
108
|
+
|
|
109
|
+
Only the `false` reset is observable: the `true` transition (upload start) does **not** dispatch `wcs-upload:trigger-changed`. A binding system writes `true` to start and observes the single `false` edge to know the command finished. This is the same trade-off as `@wcstack/fetch`'s `trigger`.
|
|
110
|
+
|
|
111
|
+
### 3. Declarative trigger target
|
|
112
|
+
|
|
113
|
+
When auto trigger is enabled, a clickable element can point at a `<wcs-upload>` by id.
|
|
114
|
+
|
|
115
|
+
```html
|
|
116
|
+
<script type="module" src="https://esm.run/@wcstack/upload/auto"></script>
|
|
117
|
+
|
|
118
|
+
<wcs-upload id="photo-upload" url="/api/upload" manual></wcs-upload>
|
|
119
|
+
<input id="photo-input" type="file">
|
|
120
|
+
<button data-uploadtarget="photo-upload">Upload</button>
|
|
121
|
+
|
|
122
|
+
<script type="module">
|
|
123
|
+
const upload = document.getElementById("photo-upload");
|
|
124
|
+
const input = document.getElementById("photo-input");
|
|
125
|
+
|
|
126
|
+
input.addEventListener("change", () => {
|
|
127
|
+
upload.files = input.files;
|
|
128
|
+
});
|
|
129
|
+
</script>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
By default, the trigger attribute is `data-uploadtarget`.
|
|
133
|
+
|
|
134
|
+
### 4. With `@wcstack/state`
|
|
135
|
+
|
|
136
|
+
```html
|
|
137
|
+
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
|
|
138
|
+
<script type="module" src="https://esm.run/@wcstack/upload/auto"></script>
|
|
139
|
+
|
|
140
|
+
<wcs-state>
|
|
141
|
+
<script type="module">
|
|
142
|
+
export default {
|
|
143
|
+
uploadResult: null,
|
|
144
|
+
uploadLoading: false,
|
|
145
|
+
uploadProgress: 0,
|
|
146
|
+
uploadError: null,
|
|
147
|
+
};
|
|
148
|
+
</script>
|
|
149
|
+
|
|
150
|
+
<wcs-upload
|
|
151
|
+
id="state-upload"
|
|
152
|
+
url="/api/upload"
|
|
153
|
+
manual
|
|
154
|
+
data-wcs="
|
|
155
|
+
value: uploadResult;
|
|
156
|
+
loading: uploadLoading;
|
|
157
|
+
progress: uploadProgress;
|
|
158
|
+
error: uploadError
|
|
159
|
+
">
|
|
160
|
+
</wcs-upload>
|
|
161
|
+
|
|
162
|
+
<input id="state-upload-input" type="file">
|
|
163
|
+
<button data-uploadtarget="state-upload">Upload</button>
|
|
164
|
+
|
|
165
|
+
<progress max="100" data-wcs="value: uploadProgress"></progress>
|
|
166
|
+
<p data-wcs="textContent: uploadLoading"></p>
|
|
167
|
+
|
|
168
|
+
<script type="module">
|
|
169
|
+
const upload = document.getElementById("state-upload");
|
|
170
|
+
const input = document.getElementById("state-upload-input");
|
|
171
|
+
|
|
172
|
+
input.addEventListener("change", () => {
|
|
173
|
+
upload.files = input.files;
|
|
174
|
+
});
|
|
175
|
+
</script>
|
|
176
|
+
</wcs-state>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
In this setup, upload becomes a bindable async node:
|
|
180
|
+
|
|
181
|
+
- the element performs the request
|
|
182
|
+
- async state flows back as `value`, `loading`, `progress`, `error`, `status`
|
|
183
|
+
- the UI binds to those paths declaratively
|
|
184
|
+
|
|
185
|
+
## Public API
|
|
186
|
+
|
|
187
|
+
### Element attributes and properties
|
|
188
|
+
|
|
189
|
+
| Name | Type | Default | Description |
|
|
190
|
+
|---|---|---|---|
|
|
191
|
+
| `url` | `string` | `""` | Upload endpoint |
|
|
192
|
+
| `method` | `string` | `"POST"` | HTTP method |
|
|
193
|
+
| `field-name` | `string` | `"file"` | FormData field name |
|
|
194
|
+
| `multiple` | `boolean` | `false` | Declarative marker only — it advertises multi-file intent but does not enforce file count (any number of files in `files` is sent regardless) |
|
|
195
|
+
| `max-size` | `number` | `Infinity` | Maximum allowed file size in bytes |
|
|
196
|
+
| `accept` | `string` | `""` | Accepted MIME types or file extensions |
|
|
197
|
+
| `manual` | `boolean` | `false` | Disables auto upload on `files` assignment |
|
|
198
|
+
| `files` | `FileList \| File[] \| null` | `null` | Files to upload |
|
|
199
|
+
| `trigger` | `boolean` | `false` | Write-only command surface for manual upload |
|
|
200
|
+
| `value` | `any` | `null` | Parsed response body or response text |
|
|
201
|
+
| `loading` | `boolean` | `false` | Upload state flag |
|
|
202
|
+
| `progress` | `number` | `0` | Upload progress from `0` to `100` |
|
|
203
|
+
| `error` | `any` | `null` | Validation, network, or response error |
|
|
204
|
+
| `status` | `number` | `0` | HTTP response status |
|
|
205
|
+
| `promise` | `Promise<any>` | resolved `null` | Current upload promise |
|
|
206
|
+
|
|
207
|
+
### Methods
|
|
208
|
+
|
|
209
|
+
#### `upload()`
|
|
210
|
+
|
|
211
|
+
Starts upload with the current `files` and returns a promise.
|
|
212
|
+
|
|
213
|
+
The promise **resolves** in every terminal case and never rejects:
|
|
214
|
+
|
|
215
|
+
- success → resolves to the parsed response body (`value`)
|
|
216
|
+
- no files / no `url` → resolves to `null` (no-op; no request is started and no error is dispatched)
|
|
217
|
+
- validation failure → resolves to `null` (and dispatches `wcs-upload:error`)
|
|
218
|
+
- HTTP error (status >= 400) → resolves to `null` (the error object is exposed on `error` / `wcs-upload:error`)
|
|
219
|
+
- network error → resolves to `null` (the error is exposed on `error` / `wcs-upload:error`)
|
|
220
|
+
- abort → resolves to `null`
|
|
221
|
+
|
|
222
|
+
Because `null` is also a valid resolved value, do not use the resolved value to detect failure — observe `error` / `status` (or the `wcs-upload:error` / `wcs-upload:response` events) instead. This mirrors `@wcstack/fetch`, where errors flow through state rather than promise rejection.
|
|
223
|
+
|
|
224
|
+
> Note on the headless Core: `UploadCore.upload(url, files)` is `async` and **rejects** synchronously-detectable argument errors (missing `url` or empty `files`) by throwing `[@wcstack/upload] ...`. The Shell's `upload()` instead returns `null` for a missing `url` or missing files (it owns the `url`/file lifecycle and treats "no destination" / "no files" as a no-op rather than an error), so the Shell never reaches the Core's throw and never rejects.
|
|
225
|
+
|
|
226
|
+
#### `abort()`
|
|
227
|
+
|
|
228
|
+
Aborts the current request. Loading is cleared through the request's abort path (consistent with `@wcstack/fetch`).
|
|
229
|
+
|
|
230
|
+
## Events
|
|
231
|
+
|
|
232
|
+
| Event | `detail` | Description |
|
|
233
|
+
|---|---|---|
|
|
234
|
+
| `wcs-upload:files-changed` | `FileList \| File[] \| null` | Fired when `files` changes |
|
|
235
|
+
| `wcs-upload:trigger-changed` | `boolean` | Fired when `trigger` resets to `false` |
|
|
236
|
+
| `wcs-upload:loading-changed` | `boolean` | Fired when loading state changes |
|
|
237
|
+
| `wcs-upload:progress` | `number` | Fired on upload progress updates |
|
|
238
|
+
| `wcs-upload:error` | error object | Fired on validation, network, or HTTP error |
|
|
239
|
+
| `wcs-upload:response` | `{ value, status }` | Fired on successful HTTP response |
|
|
240
|
+
|
|
241
|
+
## Validation
|
|
242
|
+
|
|
243
|
+
`<wcs-upload>` validates files before sending:
|
|
244
|
+
|
|
245
|
+
- `max-size` rejects files larger than the configured byte size
|
|
246
|
+
- `accept` supports MIME types like `image/*`, exact MIME types like `application/pdf`, and extensions like `.pdf`
|
|
247
|
+
|
|
248
|
+
Files whose `type` is empty (the OS could not determine a MIME type) cannot be matched against MIME patterns. Such files are accepted only if `accept` contains a matching extension pattern (e.g. `.png`); if `accept` lists MIME patterns exclusively, an empty-type file is rejected because its type cannot be verified.
|
|
249
|
+
|
|
250
|
+
Validation failure dispatches `wcs-upload:error` and the request is not started.
|
|
251
|
+
|
|
252
|
+
### Error vs response on the state surface
|
|
253
|
+
|
|
254
|
+
On a successful response (status 2xx), both `value` and `status` are updated via `wcs-upload:response`. On an HTTP error (status >= 400), only `error` is updated (via `wcs-upload:error`) — **`status` is not propagated to the state surface in the error case**, because `status` is bound to the `wcs-upload:response` event, which is not dispatched for errors. The HTTP status code is still available inside the `error` object (`error.status`). This is the same trade-off as `@wcstack/fetch`: error details flow through the single `error` channel rather than splitting across response/error events.
|
|
255
|
+
|
|
256
|
+
> Reading `core.status` / `el.status` directly returns the HTTP status of the last response, including error statuses such as `413` or `500` (the getter reflects the raw XHR status). This differs from the bound `status` path (driven by `wcs-upload:response`), which stays at its previous value on an error. Code that reads the getter imperatively and code that binds to `status` therefore observe different values after an HTTP error; prefer one path consistently. This is the same structure as `@wcstack/fetch`.
|
|
257
|
+
|
|
258
|
+
### Progress on error
|
|
259
|
+
|
|
260
|
+
`progress` is only reset to `0` at the start of each upload and set to `100` on success. On an HTTP, network, or abort error, **`progress` is intentionally left at its last value** (e.g. `70`) so the UI can show where the transfer stopped. Use `error` / `loading` (not `progress`) to detect failure, and reset or hide the progress indicator from your UI in response to `wcs-upload:error` if you do not want a stale value displayed. A subsequent `upload()` resets `progress` back to `0`.
|
|
261
|
+
|
|
262
|
+
## wc-bindable-protocol
|
|
263
|
+
|
|
264
|
+
Both `UploadCore` and `<wcs-upload>` declare `wc-bindable-protocol` compliance, making them interoperable with any framework or component that supports the protocol.
|
|
265
|
+
|
|
266
|
+
The declaration follows the full wc-bindable interface model — three independent surfaces:
|
|
267
|
+
|
|
268
|
+
- **`properties`** — observable outputs that `bind()` subscribes to (`value`, `loading`, `progress`, `error`, `status`, and the Shell's `trigger` / `files`)
|
|
269
|
+
- **`inputs`** — the settable surface (`url`, `method`, `fieldName`, …); declarative metadata that tooling, codegen, and remote proxying read
|
|
270
|
+
- **`commands`** — invocable methods (`upload`, `abort`); a binding system such as `@wcstack/state` can invoke them by name
|
|
271
|
+
|
|
272
|
+
Per the protocol, only `properties` is interpreted by core `bind()`; `inputs` / `commands` (and the `attribute` / `async` hints) are descriptive. They do **not** create implicit two-way data flow.
|
|
273
|
+
|
|
274
|
+
### Core (`UploadCore`)
|
|
275
|
+
|
|
276
|
+
`UploadCore` declares the bindable async state that any runtime can subscribe to, plus its portable input/command surface:
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
static wcBindable = {
|
|
280
|
+
protocol: "wc-bindable",
|
|
281
|
+
version: 1,
|
|
282
|
+
properties: [
|
|
283
|
+
{ name: "value", event: "wcs-upload:response",
|
|
284
|
+
getter: (e) => e.detail.value },
|
|
285
|
+
{ name: "loading", event: "wcs-upload:loading-changed" },
|
|
286
|
+
{ name: "progress", event: "wcs-upload:progress" },
|
|
287
|
+
{ name: "error", event: "wcs-upload:error" },
|
|
288
|
+
{ name: "status", event: "wcs-upload:response",
|
|
289
|
+
getter: (e) => e.detail.status },
|
|
290
|
+
],
|
|
291
|
+
inputs: [
|
|
292
|
+
{ name: "url" },
|
|
293
|
+
{ name: "method" },
|
|
294
|
+
{ name: "fieldName" },
|
|
295
|
+
],
|
|
296
|
+
commands: [
|
|
297
|
+
{ name: "upload", async: true },
|
|
298
|
+
{ name: "abort" },
|
|
299
|
+
],
|
|
300
|
+
};
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Headless consumers call `core.upload(url, files)` directly — no `trigger` needed.
|
|
304
|
+
|
|
305
|
+
### Shell (`<wcs-upload>`)
|
|
306
|
+
|
|
307
|
+
The Shell extends the Core declaration with the `trigger` / `files` outputs and the DOM-driven input surface; `commands` (`upload` / `abort`) are inherited unchanged:
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
static wcBindable = {
|
|
311
|
+
...UploadCore.wcBindable,
|
|
312
|
+
properties: [
|
|
313
|
+
...UploadCore.wcBindable.properties,
|
|
314
|
+
{ name: "trigger", event: "wcs-upload:trigger-changed" },
|
|
315
|
+
{ name: "files", event: "wcs-upload:files-changed" },
|
|
316
|
+
],
|
|
317
|
+
inputs: [
|
|
318
|
+
{ name: "url" },
|
|
319
|
+
{ name: "method" },
|
|
320
|
+
{ name: "fieldName" },
|
|
321
|
+
{ name: "multiple" },
|
|
322
|
+
{ name: "maxSize" },
|
|
323
|
+
{ name: "accept" },
|
|
324
|
+
{ name: "manual" },
|
|
325
|
+
{ name: "files" },
|
|
326
|
+
{ name: "trigger" },
|
|
327
|
+
],
|
|
328
|
+
};
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
The Shell's inputs intentionally carry no `attribute` hint: each attribute-backed setter (`url`, `method`, `fieldName`, `multiple`, `maxSize`, `accept`, `manual`) already reflects to its attribute, so a binding system that mirrors `inputs[].attribute` would set the attribute twice.
|
|
332
|
+
|
|
333
|
+
This makes the element consumable from any wc-bindable-aware system, including `@wcstack/state`.
|
|
334
|
+
|
|
335
|
+
## Headless API
|
|
336
|
+
|
|
337
|
+
If you do not need the custom element shell, you can use `UploadCore` directly:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { UploadCore } from "@wcstack/upload";
|
|
341
|
+
|
|
342
|
+
const core = new UploadCore();
|
|
343
|
+
const result = await core.upload("/api/upload", files, {
|
|
344
|
+
method: "PUT",
|
|
345
|
+
fieldName: "attachment",
|
|
346
|
+
headers: {
|
|
347
|
+
Authorization: "Bearer token",
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`UploadCore` exposes the same async state as properties and dispatches the same events.
|
|
353
|
+
|
|
354
|
+
## Manual bootstrap
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
import { bootstrapUpload } from "@wcstack/upload";
|
|
358
|
+
|
|
359
|
+
bootstrapUpload({
|
|
360
|
+
autoTrigger: true,
|
|
361
|
+
triggerAttribute: "data-uploadtarget",
|
|
362
|
+
tagNames: {
|
|
363
|
+
upload: "wcs-upload",
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
283
368
|
Use this when you want to customize the tag name or trigger attribute instead of relying on `@wcstack/upload/auto`.
|