-
-
Notifications
You must be signed in to change notification settings - Fork 118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve public API #316
Comments
Thanks a lot for proposing that API. I agree with the overall direction this is going. The only point I'm not conviced yet is the {{#let (file-upload-queue @name="photos" @onFileAdd=(action this.uploadImage)) as |queue|}}
<FileDropzone @queue={{queue}} @onDrop={{action this.filesSelected}} as |dropzone|>
{{! ... }}
</FileDropzone>
{{/let}} With such an API it's more clear that <FileSelect
@queue={{file-upload-queue @name="photos" @onFileAdd=(action this.uploadImage)}}
/> Sorry that I haven't had the time to reply earlier. |
Agree with you and like your solution very much. I kind of forgot about helpers 😅 |
Current version of working document – editing in suggestions from #58 {{#let (file-upload-queue
name="photos"
onFileAdd=this.uploadImage
onFileUpload=this.trackUpload
onFileUploaded=this.handleUploadSuccess
onFileUploadFailed=this.handleUploadFailure
) as |queue|}}
<FileDropzone @queue={{queue}} @onDrop={{this.filesSelected}} as |dropzone|>
{{#if dropzone.active}}
{{#if dropzone.valid}}
Drop to upload
{{else}}
Invalid
{{/if}}
{{else if queue.files.length}}
Uploading {{queue.files.length}} files. ({{queue.progress}}%)
{{else}}
<h4>Upload Images</h4>
<p>
{{#if dropzone.supported}}
Drag and drop images onto this area to upload them or
{{/if}}
<FileSelect @queue={{queue}}
@for="upload-photo"
@accept="image/*"
@multiple={{true}}
@onSelected={{this.filesSelected}}>
<a tabindex="0">Add an Image.</a>
</FileSelect>
</p>
{{/if}}
</FileDropzone>
{{/let}} |
I like the events on
Wondering how
I'm not sure about I'm not sure about To be honest I'm not sure if all that questions are specified for the existing implementation. Not having them defined might be the reason for some of the bugs and confusion we are facing. |
We are having some problems with the shortcomings of the current API as pointed in #572. I tried the suggested fix with recent beta, just to learn the API is actually misleading and not solving the problem we are having. After starring long enough at the code, trying to make sense of it, I finally got it and am here to share what that is and my suggested solution to it. Ambiguity and UnclarityThat's actually a thing I see quite often happening to myself in my projects, especially when they grow. You start with some thing (such as an object or entity) and over time you'll add more and more functionality to it. At some point the original intent becomes blurry and when I realize this, I'll stop and start redefining things, settle on a naming or whatever is necessary to bring back clarity. Well I'm not part of this from the beginning but as an addon consumer this isn't clear to me either. Some examples where things are blurry:
Naming is important and here things don't go well together in my opinion, which is one factor of confusion. Another one is the mix of concepts and blurring the lines of responsibilities:
On the other hand, the things I do like (judging from the code) is especially the AquisitionIn order to do that, I was unconsciously using the OOUX definitions for objects, actions and relationships (+ events). I started with an inventory of things I could find relevant for the purpose of handling uploads. Not necessarily related to what's available in this addon but purposely exaggeration to probe the scope, to later reduce it is what I like to do. Objects (nouns):
Actions (active verbs):
Relationships:
Events (passive verbs):
+ drag and drop events (as I'm totally not into this, I'll leave them out). They all trigger a state change in the Possibly also these events (more for completeness, but I don't consider them relevant for the addon):
Objects and their Actions and EventsSo far, the list was a flat discovery to grasp the (whole) domain. Instead when you read the descriptions one more time, they contain a reference to the object where they can appear (queue and/or file). So, let's do these assignments. I will use TS to express this (in some sort of DSL). I will also only focus on the discovery list above (and by that skip existing methods and properties on those objects already). // actions
/* Actions for files */
interface FileActions {
upload?: (file: File) => Promise<void>;
}
/* Actions for selection */
interface SelectionActions {
filter?: (file: File) => boolean;
}
// events
// they take a name with an optional _payload_ (not the real structure, just to express what might be carried)
/* Events for queue(-likes) */
interface QueueEvents {
added: File | File[];
removed: File | File[];
cleared: undefined;
}
/* Events for upload */
interface UploadEvents {
started: File | File[];
failed: File | File[];
uploaded: undefined;
}
/* Events for selection */
interface SelectionEvents {
selected: File | File[];
}
// objects
enum FileState {
// ...
}
interface File<Params extends {
Actions: FileActions;
Events: UploadEvents;
}> {
state: FileState;
upload();
}
interface Queue<Params extends {
Actions: FileActions & SelectionActions;
Events: QueueEvents & UploadEvents;
}> {
files: File[];
add(fileOrFiles: File | File[]);
remove(fileOrFiles: File | File[]);
clear();
upload();
}
interface UIElement<Params extends {
Options: {
/* The assigned queue for that particular element */
queue: Queue;
multiple?: boolean;
}
Actions: SelectionActions,
Events: QueueEvents & UploadEvents & SelectionEvents
}> {
/* A subset of the files from the queue associated with this UI element */
files: File[];
select(): File[];
} Quite some stuff, yet grouped and distinctively organized, bringing clarity. Which object is responsible for what, what it can do and what does it "report" on. I might not nailed all cases nor would I expect this to be accurate. Usually, this is somewhat around 80% correct and you'll figure out the 20% "oupsy" while implementing ;) Even with that ~80% that's ok to move forward. ResponsibilitiesLet's clear up some responsibilities: Queue:
UI Elements:
Solution / Suggested APIThe existing API enforces quite some conventions that are unfortunately not overridable, which heavily limits the usage of this addon (and prohibits for example our use case). With the achieved clarity and cleared responsibilities, a new API can be thought of, that allows flexibility of use when needed while providing a sensible default out of the box (ok, the sensible default in this case means backwards compatibility). For that, I really, really... really (!) like the idea of having a helper over a provider component to manage the queue. I'm happily expand on your idea here. Let's go into a verbose sneak peak: {{#let
(file-queue
name="gallery"
{{! ++ actions ++ }}
upload=this.upload
filter=this.filter
{{! ++ event listeners ++ }}
started=this.handleUploadStartForQueue
)
as |queue|
}}
Overall progress: {{queue.progress}}
<button {{on "click" queue.upload}}>Start the upload!</button>
All files in the queue:
{{#each queue.files as |file|}}
{{#if (eq file.state 'UPLOADED')}}
:white_check_mark: this will almost never be shown (or too short to be visible)
as uploaded files will be removed from the queue
{{/if}}
{{file.name}} - Uploading at {{file.progress}}
{{#if (eq file.state 'FAILED')}}
oh oh, sample error message herer
{{/if}}
{{/each}}
<DropZone
@queue={{queue}}
@multiple={{true}}
{{! ++ event listeners ++ }}
{{!
block automatic upload after files got dropped, aka "selected" }}
collecting files first and start upload later
also see next example for default behavior made explicit
}}
@selected={{false}}
as |zone|
>
Files in this dropzone:
- inside the block through yielded files
- with the ability to remove the from the queue
- to realize something like a "review" behavior
{{#each zone.files as |file|}}
{{file.name}} <button {{on "click" (fn queue.remove file)}}>remove file</button>
{{/each}}
<button {{on "click" queue.clear}}>remove all files</button>
</DropZone>
<FileSelect
@queue={{queue}}
{{! ++ actions ++ }}
{{! local overwrite to the handler on queue - dunno if useful at all }}
@filter={{this.filterForSmall}}
{{! ++ event listeners ++ }}
@started={{this.associate}}
@uploaded={{this.clear}}
{{! that *is* the internal default behavior, *explicit* verbosity only for demo purposes}}
@selected={{queue.upload}}
>
Choose Files
</FileSelect>
Files selected as siblings to the UI element:
{{#if this.file}}
{{#if (eq this.file.state 'UPLOADED')}}
:white_check_mark:
{{/if}}
{{this.file.name}} - Uploading at {{file.progress}}
{{#if (eq file.state 'FAILED')}}
oh oh, sample error message herer
{{/if}}
{{/if}}
{{/let}} backing class: import { File as UploadFile, Queue } from 'ember-file-upload';
export DemoComponent extends Component {
@tracked file?: UploadFile;
@action
associate(files: UploadFile[]) {
this.file = files.length > 0 ? files[0] : undefined;
}
@action
clear() {
this.file = undefined;
}
@action
upload(file: UploadFile) {
// send the bytes to the backend in here
}
@action
filter(file: File) {
return file.type.includes('image'); // images only
}
@action
filterForSmall(file: File) {
return this.filterFile(file) && file.size < 500 * 1024; // max 500kb
}
@action
handleUploadStartForQueue(queue: Queue) {
console.log(`let's get the whole **queue** going!`, queue);
}
} This allows to distinctively work on queue or file/ui element level, choose whatever scope works for addon consumers. It favors pull-based reactivity where possible and expands to push-based reactivity where pull-based APIHere is the public API for the usage. For components I follow RFC #748: type QueueHelperArgs = {
name: string;
}
& FileActions
& SelectionActions;
& QueueEvents
& UploadEvents
;
type QueueHelper = (positional: unknown[], named: QueueHelperArgs): Queue;
interface FileSelectSignature {
Element: ???;
Args: {
queue: Queue;
multiple?: boolean;
// ... + more
}
& SelectionActions
& QueueEvents
& UploadEvents
& SelectionEvents;
BlockParams: {
default: {
files: File[];
progress: number;
// ... maybe more here?
}
}
} MigrationYou already named the new component Also, I think, the existing components can continue to work. The existing args will be marked as ImplementationOverall, I don't think, this is much work tbh (the hard part is mostly getting everything structured). Please have a look and validate my suggestion, try to break it. If we can agree on something in week 45, I can support implementation of this in week 46. |
Hi @gossi. Appreciate your enthusiasm!
Yes – current maintainers did not design much of the API 😅. This has been through a few iterations in its past and we picked up maintenance when it was in a sorry state. It started as a single component with args that closely mimicked html attributes for an We've been working hard to unblock the upgrade path for users by paying tech debt and ensuring we have a released version that is compatible with Embroider/Ember v4. That has been a lot of work. This is really the main remaining job – and from a code perspective you're right that it's not that difficult. It's the design that is hard – and you have a lot to add here! My progressA few design decisions I had made.
Provider demo: <FileQueue @upload={{this.upload}} as |queue|>
<queue.Dropzone as |zone|>
{{#if zone.supported}}
{{if zone.active 'Let go to upload' 'Drop files here'}}
<queue.Select as |select|>
Or choose a file with queue.Select
<select.Input accept="image/*,video/*,audio/*" multiple />
</queue.Select>
<label>
Or modify a file input with queue.fileSelect
<input
type="file"
accept="image/*,video/*,audio/*"
multiple
{{queue.fileSelect}}
/>
</label>
{{/if}}
{{#if queue.files.length}}
Uploading {{queue.files.length}} files. ({{queue.progress}}%)
{{/if}}
</queue.Dropzone>
</FileQueue> Same codebase supports helper version: {{#let (file-queue upload=this.upload) as |queue|}}
<FileDropzone @queue={{queue}} as |zone|>
{{#if zone.supported}}
{{if zone.active 'Let go to upload' 'Drop files here'}}
Drag into queue.Dropzone
<FileSelect @queue={{queue}} as |select|>
Or choose a file with queue.Select
<select.Input accept="image/*,video/*,audio/*" multiple />
</FileSelect>
<label>
Or modify a file input with queue.fileSelect
<input
type="file"
accept="image/*,video/*,audio/*"
multiple
{{queue.fileSelect}}
/>
</label>
{{/if}}
{{#if queue.files.length}}
Uploading {{queue.files.length}} files. ({{queue.progress}}%)
{{/if}}
</FileDropzone>
{{/let}} My blockers
This is an interesting idea – worth trying. I guess as long as the select method is called from an interaction event (and the button that triggers it has a tabindex) this may work well 😄. I feel like my modifier solution is still helpful as it makes no DOM decisions at all for users and is a future-proof "escape hatch" for emergencies. Event naming and deprecationI'm happy to make a bold release for v5 which has an entirely new component API. Personally I don't think it's worth maintaining the old components for another major. I like your ideas about the events so keen to follow your lead there. Going forwardIf you'd like please make a PR. Appreciate that users are waiting for v5 - particularly for accessibility. Would appreciate if you could seriously consider the decisions I made in "My progress". I can assist with review, tests and docs. Would be good to hear any guidance from @jelhan too |
Thanks @gilest for sharing your way of coming here. I think both of ours very much align here :) I'm pretty much on it to submit a PR to make it work. After processing your ideas, I think this is what sounds the minimal effort to help out our situation:
Either the helper of the modifier will take another parameter to be notified that a particular upload is started (to handle visual feedback for that particular element). Would figure out during implementation, what seems to be a good way (but I gonna use the domain model I outlined above). Provier components are much like a thing swapping over from react land, where ember has helpers for that - so gonna use them. The only reason to use components over helpers is, when they should yield a component (which does not work from within helpers). However this addon allows a strict separation between helpers and components and their distinctive use-case, so it's fine to keep them that way. wdyt ? |
Fair – I think @jelhan might be of the same opinion so I'll defer to you on this one.
Great! I do think it's important here to implement some of the other API changes at the same time. I don't want to do a series of releases that make API changes with Upgrading instructions. For example the "default" queue and optional I'd also question whether we should keep Anyway, I'm comfortable with you making a PR and happy to help with support. |
@gossi How are you getting on? Anything I can help with? |
I just started getting on it. Will work on it locally (linked). As soon as I have something "ready" I will open a draft PR. |
…on-docs (#620) Based on discussion in #316 - here is what I did: Modernizations: - Use `ember-cli-typescript` (not entirely, but for the key parts) - Dropped `Ember.A()` (on those files I touched with TS) in favor of `@tracked` - Use `@ember/destroyables` (depending on support matrix, this requires to ship the polyfill) - I think this also prepares for addon format v2 Changes: - Mainly the `{{file-queue}}` helper, which can be beautifully used like so: ```hbs {{#let (file-queue fileAdded=this.addFile fileRemoved=this.removeFile) as |queue|}} {{#each queue.files as |file|}} <span data-test-file> {{file.name}} </span> <button type="button" data-test-remove {{on "click" (fn queue.remove file)}} > x </button> {{/each}} <label> <input type="file" {{queue.selectFile filter=this.filter filesSelected=this.selectFiles}} hidden> Select File </label> {{/let}} ``` - I kept original API of `<FileUpload>` component and used new helper under the hood, rewired appropriately - `FileQueueService` can now work with `DEFAULT_QUEUE` name (instead of super-global queue) Cutbacks: - With usage of TS I had to disable `ember-cli-addon-docs` (maybe it is time to move, ie. field-guide or docfy?) - Had to drop `@onSelect` (couldn't rewire to the new API - I don't see this as an issue, the API was never public) Questions: - I think, a lot of things can be marked deprecated for v5 and removed in v6 - I left quite some `@TODO` in the code, some things I marked as removal/deprecation candidates or questions I have
Since @jelhan brought up significant API changes in #306 . I thought I'd post some of my "RFC" notes about the public APIs.
Interested to hear thoughts.
The text was updated successfully, but these errors were encountered: