Multiple file upload component
mNo edit summary
(Replacing message template with parser tag)
 
(16 intermediate revisions by 3 users not shown)
Line 1: Line 1:
It is easy to [[add a single file upload button in the MDriven Designer]], but to handle multiple files you'll have to create a component.
<message>Write the content here to display this box</message>
== The General Idea ==
# Make a ViewModel column with content override and create an [[EXT_Components|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.


== Model for this example ==
Below is an example of how to do this.


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.
== 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.


{| class="wikitable" style="border: none"
{| class="wikitable" style="border: none"
Line 13: Line 21:
== Setting up the ViewModel ==  
== Setting up the ViewModel ==  


* '''Create a view model.''' 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 view model class.
* '''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''' (here named 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 temporary store each file since we are limited to one single upload at a time.
* '''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.


[[File:Upload multiplefiles_create_variable_withroot.png|450px]]
[[File:Upload multiplefiles_create_variable_withroot.png|450px]]


* '''Add a generic view model 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 view model column to be able to handle it in the component client code.
* '''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 view model 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 a MDriven "magic" that, when uploading a file, will look for a column with the same name as referenced 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.
* '''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.''' They are only for temporary storage.
* '''Set Visible Expression to false for these columns.''' These are only for temporary storage.


[[File:Upload_multiplefiles_AttachmentUploadTarget_columns.png|450px]]
[[File:Upload_multiplefiles_AttachmentUploadTarget_columns.png|450px]]


* '''Add a generic view model column, with a name of your choice, for uploading''' (it will be a button).
* '''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 our own component.  
* '''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 [[EXT_Components|external component]].
* '''Click the TV and Attributes button''', which will open a dialogue where you can set up the usage of your own [[EXT_Components|external component]].


[[File:Upload_multiplefiles_create_upload_button_column.png|450px]]
[[File:Upload_multiplefiles_create_upload_button_column.png|450px]]


* '''Add a Angular_Ext_Component tagged value.''' The value part is here multifiles. 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 '''multifiles'''. 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.
* '''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.


[[File:Upload_multiplefiles_ExtComponent.png|450px]]
[[File:Upload_multiplefiles_ExtComponent.png|450px]]


== Creating the component ==
* '''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.
 
[[File:Upload_multiplefiles_action_code.png]]
 
* '''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:
 
<pre>
let x = Document.Create in (
vCurrent_Project.Document.Add(x);
vUploadFileTarget:= x;
x
)
</pre>
 
== Creating the Component ==


* '''In your model directory''' (named something like "xyz123_MDrivenServerA0_AssetsTK") '''add a directory named exactly EXT_Components''' (unless it already exist).
* 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'''
* 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).
* '''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>.
The cshtml file code below is generic except for the class Projects_UploadMultipleDocuments which is <view model name>_<column name for upload button>.


<syntaxhighlightlang="html">
<pre>
<!-- multifiles.cshtml -->
<!-- multifiles.cshtml -->
<div class="Projects_UploadMultipleDocuments <!-- This class will match the view model name and component column. CHANGE! -->
<div class="Project_UploadMultipleFilesButton <!-- This class will match the view model name and component column. CHANGE! -->
             tk-component ctButton tk-button NoLabel"  
             tk-component ctButton tk-button NoLabel"  
             multifiles> <!-- This defines the component. -->
             multifiles> <!-- This defines the component. -->
Line 62: Line 90:
  <span class="tk-file-upload__name">{{ViewModelRoot.ViewData['FileUploadName__undefined.AttachmentUploadTarget']}}</span>
  <span class="tk-file-upload__name">{{ViewModelRoot.ViewData['FileUploadName__undefined.AttachmentUploadTarget']}}</span>
</div>
</div>
</syntaxhighlight>
</pre>


The javascript code below is generic except for
The javascript code below is generic except for:


* '''AddDocumentToProject''' which is the action name of the action that creates new documents for each project.
* '''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.
* '''AttachmentUploadTarget''' which is the name of the view model column storing the file blob.
* All occurences of '''multifiles''', which is the name of the component.
* All occurrences of '''multifiles''', which is the name of the component.


<syntaxhighlightlang="javascript">
<pre>
//
//
// multifiles.js
// multifiles.js
Line 154: Line 182:
}
}
InstallTheDirectiveFor_multifiles(angular.module(MDrivenAngularAppModule));
InstallTheDirectiveFor_multifiles(angular.module(MDrivenAngularAppModule));
</syntaxhighlight>
</pre>
[[Category:View Model]]
{{Edited|July|12|2024}}
 
[[Category:TOC]]

Latest revision as of 07:46, 17 June 2024

The General Idea

  1. Make a ViewModel column with content override and create an external component.
  2. The component has a file input that allows the user to choose multiple files.
  3. Javascript handles each file, in turn, and uploads it.
  4. To receive each file and save it, we create temporary storage where each file is stored.
  5. 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.

Upload multiplefiles project document.png Upload multiplefiles document.png

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.

Upload multiplefiles create variable withroot.png

  • 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.

Upload multiplefiles AttachmentUploadTarget columns.png

  • 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.

Upload multiplefiles create upload button column.png

  • 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.

Upload multiplefiles ExtComponent.png

  • 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.

Upload multiplefiles action code.png

  • 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));
This page was edited 12 days ago on 06/17/2024. What links here