The General Idea
- Make a ViewModel column with content override and create an external component.
- The component has a file input that allows the user to choose multiple files.
- Javascript handles each file, in turn, and uploads it.
- To receive each file and save it, we create temporary storage where each file is stored.
- Before a new file is uploaded, an action has to be called to create a new item where we want to save the next file and make the variable refer to this new item.
Below is an example of how to do this.
Model for This Example
The model for the example is a project with some documents. Each document holds the file (a Blob) and a filename (a String). The goal is to make it possible to choose several documents in the file browser and upload all of them.
Setting up the ViewModel
- Create a ViewModel. It should have a root, which in this case should be a project. The project holds the list/container of documents. We will need the ID of the ViewModel class.
- Create a variable (named here as vUploadFileTarget). It should be of a type that could store a file (Blob). In this example, we choose the Document type. This variable will be used to temporarily store each file since we are limited to a single upload at a time.
- Add a generic ViewModel column named AttachmentUploadTarget with the value of the file part of your variable (here vUploadFileTarget.File). The name here will be used in the component code. We create a ViewModel column to be able to handle it in the component client code.
- Add a generic ViewModel column named AttachmentUploadTarget_FileName with the value of the string part of your variable (here vUploadFileTarget.FileName). The _FileName suffix is important since there is an MDriven "magic" that, when uploading a file, will look for a column with the same name as the referenced column but with a "_FileName" suffix, and if such a column exists, it will assign the filename to it. Thus, this part is optional, but if you skip it, you will lose the name of the file.
- Set Visible Expression to false for these columns. These are only for temporary storage.
- Add a generic ViewModel column, with a name of your choice, for uploading (it will be a button).
- Check the Content override checkbox. This is the column that will use your own component.
- Click the TV and Attributes button, which will open a dialogue where you can set up the usage of your own external component.
- Add an Angular_Ext_Component tagged value. The value part is here as multi-files. This is the name of your component and this name is matched to the name of your component folder and all the component files. If you mess this name up, nothing will work. If you want to copy the component code from this example (below), you should write multi-files. If you want to write your own component, please choose whatever name you like, and if you're up for a real adventure, you can even try to use uppercase letters and camel case.
- Create an action for the ViewModel with the name AddDocumentToProject. This name is referenced from the component code. If you choose another name, you will have to reference that name instead in the component.
- Document is the name of the type that holds the file and filename.
- vCurrent_Project references the current project, and Project is the name of the type.
- vUploadFileTarget is the name of the variable in the view model.
This code creates a new document and assigns the variable to this new document. This is necessary to prevent the new file from overriding the last file. After the last uploaded file, vUploadFileTarget will contain the last file, until a new multifile upload is initiated.
Action "Execute expression" code:
let x = Document.Create in ( vCurrent_Project.Document.Add(x); vUploadFileTarget:= x; x )
Creating the Component
- In your model directory (named something like "xyz123_MDrivenServerA0_AssetsTK"), add a directory named exactly EXT_Components (unless it already exists).
- In the EXT_Components directory, create a new directory with the name of your component. In this example: multifiles
- Create one multifiles.js and one multifiles.cshtml in that directory (change "multifiles" to your own component name if you have another).
The cshtml file code below is generic except for the class Projects_UploadMultipleDocuments which is <view model name>_<column name for upload button>.
<!-- multifiles.cshtml --> <div class="Project_UploadMultipleFilesButton <!-- This class will match the view model name and component column. CHANGE! --> tk-component ctButton tk-button NoLabel" multifiles> <!-- This defines the component. --> <div class="tk-file-upload__inner"> <input type="file" id="addAttachments" class="tk-file-upload__native" accept=".pdf,.docx,.doc,.odt" <!-- The file types to accept --> readmultifiles <!-- This is the directive --> multiple /> <!-- This makes it possible to add several files --> <label for="addAttachments" title="[ViewModelColumnLabel]" class="tk-file-upload__interactive ripple-effect"> [ViewModelColumnLabel] <div class="tk-file-upload__progress" style="width: {{ViewModelRoot.ViewData['FileUploadPercent__undefined.AttachmentUploadTarget']}}%;"></div> </label> </div> <span class="tk-file-upload__name">{{ViewModelRoot.ViewData['FileUploadName__undefined.AttachmentUploadTarget']}}</span> </div>
The javascript code below is generic except for:
- AddDocumentToProject which is the action name of the action that creates new documents for each project.
- AttachmentUploadTarget which is the name of the view model column storing the file blob.
- All occurrences of multifiles, which is the name of the component.
// // multifiles.js // function UploadFiles(files, scope, index = 0) { const file = files[index]; // scope.StreamingViewModelClient.ExecuteAfterFullRoundtrip is a // synchronous call to server. Since calls are executed in order at the // server this will ensure that the previous asynchronous call has been // executed. scope.StreamingViewModelClient.ExecuteAfterFullRoundtrip('waitForReadyToExecute' + index, null, () => { // Creates a new attachment which next upload will target scope.data.Execute('AddDocumentToProject'); // CHANGE IF NOT APPLICABLE - this is the name of the action scope.StreamingViewModelClient.ExecuteAfterFullRoundtrip('waitForSkapaBilagaToFinish' + index, null, () => { // Uploads file to specified target scope.StreamingViewModelClient.UploadFile(file.Target, file.VMClassId, file.Data, file.FileName); scope.StreamingViewModelClient.ExecuteAfterFullRoundtrip('waitingForUploadFileToFinish' + index, null, () => { // Start next upload if there are any more uploads if (index < files.length - 1) { UploadFiles(files, scope, index + 1); } }); }); }); } function InstallTheDirectiveFor_multifiles(streamingAppController) { streamingAppController.directive('readmultifiles', ['$document', function ($document) { return { link: function (scope, element, attr) { element.bind('change', function () { const maxSizeInBytes = 100000000; var nrExpectedFiles = element[0].files.length; var filesToUpload = []; angular.forEach(element[0].files, function (item) { if (maxSizeInBytes === 0 || item.size <= maxSizeInBytes) { const filereader = new FileReader(); const filename = item.name; const vmclassid = scope.data.VMClassId.asString; const viewModelName = scope.data.ViewModelName; const target = viewModelName + '.AttachmentUploadTarget'; // CHANGE IF NOT APPLICABLE filereader.onloadend = function (loadendevent) { var data = loadendevent.target.result; var fileToUpload = { Target: target, VMClassId: vmclassid, Data: data, FileName: filename }; filesToUpload.push(fileToUpload); if (filesToUpload.length == nrExpectedFiles) { UploadFiles(filesToUpload, scope); } }; filereader.onabort = function (abortevent) { filesToUpload = []; nrExpectedFiles = -1; }; filereader.onerror = function (erroevent) { nrExpectedFiles--; if (filesToUpload.length == nrExpectedFiles) { UploadFiles(filesToUpload, scope); } }; filereader.readAsArrayBuffer(item); } else { window.alert(item.name + " is larger than " + maxSizeInBytes + " bytes, can't upload."); } }); }); } } }]); console.trace("multifiles component Loaded"); } InstallTheDirectiveFor_multifiles(angular.module(MDrivenAngularAppModule));