@orbit-online/iframe-file-transfer
v1.4.0
Published
This library is typically used for transfering files, users has provided via input fields in the Orbit web client, to iframes hosted by 3rd parties.
Downloads
580
Readme
Iframe file transfer
This library is typically used for transfering files, users has provided via input fields in the Orbit web client, to iframes hosted by 3rd parties.
Browsers doesn't simply allow moving the protected File objects between browser contexts. It seems that ServiceWorkers support this to some extent, but it is not supported in iframes.
This library chunks the file and serializes the content of the protected File objects (sender) that originates from <input type="file" />
input elements, and transer the chunks to the iframe (receiver) for deserialization and re-assembly.
Though it is not possible to make the re-assembled file appear to the BrowserContext (iframe) as a trusted File, this library facilitates orchestration for mimicking the browsers behavior in a way that it feels close (enough).
Sender
3rd party integrators to Orbit that facilitate uploading files to external systems for processing and centralized asset management won't have to implement the sender part of the protocol, as it is facilitated by Orbit.
A sender is responsible for listening for file input element change events and spawn an iframe that implements the receiver end of the file transfer protocol. Once the iframe is loaded, the sender establish a connection with it via MessageChannel
and starts transferring the file to the receiver along with some metadata.
Example "mock" implementations can be found here and are useful for testing the receiver end of the protocol.
The playground is an implementation for a sender that can be used to simulate a Orbit web client for testing a custom receiver implementation.
Note: When using the playground, which is hosted on Github, it is served over HTTPS and modern browsers doesn't allow serving insecure content from a secure site, to use the playground the iframe must be served over HTTPS as well, it doesn't have to be remotely hosted, it can very well be at
https://localhost/iframe.html
, as long as your browser trusts the SSL certificate. If you need to test on an insecure connection, you'll need to clone the respository and runyarn install --frozen-lockfile --quiet && yarn dev
.
Receiver
Receivers are responsible for listening to file transfer events, and sending complete or cancel events back to the host, along with facilitating a place for error messages can appear to users in case anything should go wrong.
As an implementor of a receiver you are responsible for providing a form submit handler that uploads the file along with form- and metadata to a webservice, and return a resolved promised upon success or rejected promise upon error.
Files are transfered to the receiver along with an unique id, that is important in order to be able to update the file in Orbit afterwards via the Orbit Import Framework.
You can find example of receiver implementations in React or in vanilla JS with a declarative HTML data-*
attribute API.
API
Vanilla JS (declarative HTML)
This API is based on HTML data-*
attributes for specifing which input fields that should be used by the receiver. The following attributes are sorted after "mandatory, name", all mandatory attributes are marked with a trailing *
.
form[data-orbit-file-receiver=submitHandler]*
This attributes must be set on a<form />
element that should be submitted to the external web service with the binary file along with the metadata fields that lives within the form element.The
submitHandler
is a function name that lives in global scope at initialization time.<script type="module"> globalThis.onSubmit = async (url, method, formData, entityData) => { await fetch(...) } </script> <form enctype="multipart/form-data" action="/upload" method="post" data-orbit-file-receiver="onSubmit">...</form>
input[data-orbit-file-receiver-file-id]*
This part of the API contains the unique id of the file coming from Orbit, that should be sent agains the File importer API in order to update the placeholder file in Orbit with the processed data. If you don't want the user to be aware of this information you can set the input type totype=hidden
, the name attribute represents the key the value will be submitted as (provided in theformData
variable in thesubmitHandler
).In the example below
formData
will contain the unique file id at the key namedorbitFileId
.<input type="hidden" name="orbitFileId" data-orbit-file-receiver-file-id />
input[type=file][data-orbit-file-receiver-input]*
The receiver input attribute represents the file sent from the Orbit web client. At initialization time the input must be oftype=file
it will get converted to a<input type=text disabled />
field with the name preserved. The[name]
attribute represents the key for the file lives when the form get submitted.<input type="file" name="file" data-orbit-file-receiver-input />
After initialization the file input element above will be converted into
<input type="text" name="file" value="filename.jpg" disabled data-orbit-file-receiver-input />
[data-orbit-file-receiver-error-container]*
An element that will "host" any errors that might occur, any element will do here, typically a<div />
.This element will be hidden (
display: none
) at initialization time and will restore thedisplay
value when an error occurs. If the value wasnone
at initialization time, it will be set todisplay: initial
. This can cause layout problems, in those cases you can set the[data-display]
attribute to set the value want to be set when an error occurs.<div data-orbit-file-receiver-error-container data-display="inline-block"></div>
In the example above the element will be hidden at initialization time, and will appear with
display: inline-block
when an error occurs.input[type=button][data-orbit-file-receiver-cancel]
Optional element, it is fine to omit this element/attribute. However if you include a cancel button it must be a<input type="button" />
element and not a<button />
element as all buttons that live within a<form />
element, submits the form, but<input type="button" />
does not.<input type="button" value="Cancel" data-orbit-file-receiver-cancel />
The example above will render a button containing the text: Cancel.
[data-orbit-file-receiver-entity-data]
This optional element will is for debugging perposes. It will show a JSON representation of the data received from the Orbit web client as entity data available for rendering via the[data-orbit-file-receiver-entity-data-value=<entityDataProperty>]
attribute. If you render an<input name="..." />
element with this attribute, it will be included in theformData
in the submit handler, and thus automatically included in the request when the form is submitted.<input name="projectName" readonly data-orbit-file-receiver-entity-data-value="externalId" />
Note Beware that disabled input fields will be discarded from the
formData
variable in the submit handler, and thus not automatically be included in the request when the form is submitted. It is recommended if you want to include the data, that you use[readonly]
instead of[disabled]
when the data needs to be included in the form submission reuquest.The example above will not be editable by the user and the data will automatically included in the form submission request on the key
externalId
.input[data-orbit-file-receiver-entity-data-value=<entityDataProperty>]
You can map values from entity data transfered from the via this API. e.g. if entity data contains:
{ "projectName": "Large construction enterprise" }
it can be referenced and automatically be filled out with this data attribute:
<input data-orbit-file-receiver-entity-data-value="projectName" readonly />
You can reference nested data too, like this:
{ "user": { "name": "John Doe", "email": "[email protected]" } }
<input data-orbit-file-receiver-entity-data-value="user.name" name="uploaderName" readonly /> <input data-orbit-file-receiver-entity-data-value="user.email" name="uploaderEmail" readonly />
Note If you use the disabled attribute, the input field won't appear in the data, when the form is submitted.
This is a great/easy way of automatically getting meta information submitted along the form, if the user should be presented with the information, you can still use this API just by declaring the input fields
type=hidden
, this can however also be accomplised by accessing theorbitEntityData
in the submit handler, and populate theformData
data from there.img[data-orbit-file-receiver-image]
You can present a preview of the image (if the file as an image mimetype and is supported by the browser), the user is uploading with this data-attribute.
E.g.
<img src="" data-orbit-file-receiver-image />
Note This API will set the
img.style.display = 'block'
once the transfer is complete, which can give layout issues if not taken into account.progress[data-orbit-file-receiver-progress]
Progress bar for transfering the file from the sender to the receiver (iframe). Mostly useful for debugging purposes, to see if chunks are in deed getting transmitted and receieved, but maybe be corrupted along the away. Often the transmission happens so fast, that the progress jumps from
0
to100
at once. However if the files are large enough it can make sense to utilize to give the users visual feedback that "something is happening".E.g.
<progress min="0" max="100" data-orbit-file-receiver-progress></progress>
React
The react API is mostly reduced to a single hook, where the only that is needed to be passed to the hook is submitHandler`` and an
errorHandler`.
import { useOrbitIframeFileTransferReceiver } from '@orbit-online/iframe-file-transfer/react/receiver.js';
async function onOrbitFileReceiverFormSubmit(
url: string,
method: string,
formData: FormData,
_orbitEntityData: Record<string, any>,
): Promise<void> {
const request = new Request(url, {
body: formData,
method: method,
});
const response = await fetch(request);
if (!response.ok) {
throw new Error(
'Upload was unsuccesful, please contact external system administrator at [email protected] with details on how to reproduce this error.',
);
}
}
const App = () => {
const {
error,
file,
fileInputRef,
formRef,
imageSrc,
onCancel,
onSubmit,
orbitEntityData,
orbitFileId,
progress,
setError,
} = useOrbitIframeFileTransferReceiver(onOrbitFileReceiverFormSubmit);
const errorContainer = error == null ? null : <div style={errorContainerStyles}>{error.message}</div>;
return (
<form action="/upload" encType="multipart/form-data" method="post" onSubmit={onSubmit} ref={formRef}>
{errorContainer}
<input type="hidden" name="orbitFileId" ref={fileIdInputRef} value={orbitFileId} readOnly required />
<input type="text" name="file" ref={fileInputRef} value={file?.name} disabled required />
<input type="button" value="Cancel" onClick={onCancel} />
<input type="submit" value="Upload" />
</form>
);
};
Will take a look at this bare minimum example above, and explain the API in terms of this simplified usage.
function useOrbitIframeFileTransferReceiver(submitHandler): {
readonly error: Maybe<Error>,
readonly file: Maybe<File>,
readonly fileInputRef: React.RefObject<HTMLInputElement>,
readonly formRef: React.RefObject<HTMLFormElement>,
readonly imageSrc: string | undefined,
readonly onCancel: () => void,
readonly onSubmit: (url: string, method: string, formData: FormData, orbitEntityData: React<string, any>) => Promise<unknown>,
readonly orbitEntityData: state.initMessage?.entityData ?? {},
readonly orbitFileId: string | undefined,
readonly progress: string | undefined,
readonly setError: (err: Error) => void,
readonly status: 'initial' | 'transfering' | 'complete' | 'error',
};
submitHandler
argument The submit handler is called with the data from the form input fields, along with metadata from Orbit. It is expected that submit handler posts the data to the server returns aPromise
that resolves when the submission is successful or throw an error (return aPromse
that rejects) if something went wrong.async function onOrbitFileReceiverFormSubmit( url: string, method: string, formData: FormData, _orbitEntityData: Record<string, any>, ): Promise<void> {}
formRef
andonSubmit
The ref that should be passed along to the form element, in order to the form submission/interception andformData
"patch-up" to work.const { formRef, onSubmit ... } useOrbitIframeFileTransferReceiver(...); return ( <form action="/upload" encType="multipart/form-data" method="post" onSubmit={onSubmit} ref={formRef}> ... </form> );
orbitFileId
The id of the file from Orbit, that should be used by the external system to update the file in Orbit after processing it via the Orbit importer.The value in the
[name]
attribute controls the field name in the form submission to the external system.const { orbitFileId, ... } useOrbitIframeFileTransferReceiver(...); return ( <form ...> <input type="hidden" name="orbitFileId" value={orbitFileId} readOnly required /> </form> );
fileInputRef
The id of the file from Orbit, that should be used by the external system to update the file in Orbit after processing it via the Orbit importer.const { orbitFileId, ... } useOrbitIframeFileTransferReceiver(...); return ( <form ...> <input type="hidden" name="orbitFileId" required /> </form> );
orbitEntityData
Meta about the, entity and the user that uploaded the file, sent from Orbit a long with the file. TheorbitEntityData
is available whenstate
istransfering
andcomplete
states.const { orbitEntityData, ... } useOrbitIframeFileTransferReceiver(...); return ( <form ...> <input type="text" readOnly name="projectName" value={orbitEntityData.projectName} /> </form> );
onCancel
The cancel handler that tells the host (sender), that the operation has been cancelled by the user.const { onCancel, ... } useOrbitIframeFileTransferReceiver(...); return ( <form ...> <input type="button" value="Cancel" onClick={onCancel} /> </form> );
imageSrc
Image src useful for preview of image, will have a value whenstatus
iscomplete
and the file that is an image supported by the browser.const { imageSrc, ... } useOrbitIframeFileTransferReceiver(...); return ( ... <img src={imageSrc} /> ... );
progress
The file transfer progress percentage between the sender and the receiver (iframe).const { progress, ... } useOrbitIframeFileTransferReceiver(...); return ( ... <progress ref={progress} value={progress} max="100" /> ... );
status
,file
,error
andsetError
Current status of the transmission from the sender to the receiver.Possible values are
initial
,transfering
,complete
,error
.When status is
initial
,file
,error
,orbitFileId
andprogress
arenull
and has no value. When status iserror
, theerror
property of the hook has a value. When status istransfering
, theorbitFileId
,progress
has values. when status iscomplete
, thefile
property has a value,imageSrc
has a value if the file is an image supported by the browser.file
contains a DOMFile
when the transfering from the sender (host/Orbit web client) is complete.setError
can be used to set custom errors and make them available in theerror
property, and change thestatus
toerror
.const { status, ... } useOrbitIframeFileTransferReceiver(...);