File uploads

Monad doesn't handle file uploads natively, since it's after all a client side framework. But, here is how we would implement it in our admins. Let's assume all file uploads are made to a central point in your API. We'll call it /api/file/ in these examples.

This assumes the angular-file-upload plugin. Install it in your project (example in NPM) and add it as a dependency:

$ npm install --save-dev ng-file-upload
// ES6
import 'ng-file-upload';
// Browserify
require('ng-file-upload');

Then add the module dependency to your Monad application:

var app = angular.module('awesome', ['monad', ['ngFileUpload']);

There are alternative file upload modules out there; you can use whichever suits your needs best.

Option 1: write a custom controller

In your schema.html or list.html (or wherever you need the upload option), add a link to angular-file-upload:

<a ngf-select ngf-change="$ctrl.upload($files)" ng-model="$ctrl.data.item.property" href>
    click to upload files!
</a>

(The above assumes we're doing it on a create/update page and we want to store the upload in item.property.)

angular-file-upload supports many more options; see its documentation.

Now, we need our controller to handle that:

app.component('awesomeFoo', {
    // ...
    controller: ['Upload', function(Upload) {
        this.upload = function ($files) {
            // ...
        };
    }]
});

We've added an extra dependency on Upload. This is the service angular-file-upload provides. The upload method on the controller is where the magic happens, so let's write a simple implementation:

// ...as above...
    this.upload = function ($files) {
        $files.map(function (file) {
            Upload.upload({
                url: '/api/file/',
                // You'll probably want this parameter, since uploading
                // typically requires a valid session. But it depends on
                // your authentication scheme:
                withCredentials: true,
                file: file
            }).progress(function (evt) {
                // Example of showing progress percentage:
                console.log(Math.round(100.0 * evt.loaded / evt.total));
            }).success(function (data, status, headers, config) {
                // Done! This should do something useful, e.g.:
                this.item.property = data.id;
            });
        });
    }

};

Option 2: write a custom directive

In larger projects, it's typical for file uploads to be in multiple places. You should write your own directive in that case:

monad.application('foobar')
    .directive('foobarUpload', ['Upload', Upload => {
        return {
            restrict: 'A',
            require: ['ngModel', 'ngfSelect'],
            controller: ['$scope', $scope => {
                $scope.upload = $files => {
                    $files.map(file => {
                        Upload.upload({
                            url: '/api/file/',
                            // You'll probably want this parameter, since uploading
                            // typically requires a valid session. But it depends on
                            // your authentication scheme:
                            withCredentials: true,
                            file: file
                        }).progress(evt => {
                            // Example of showing progress percentage:
                            console.log(Math.round(100.0 * evt.loaded / evt.total));
                        }).success((data, status, headers, config) => {
                            // Done! This should do something useful, e.g.:
                            this.item.property = data.id;
                        });
                    });
                }
            }]
        }
    }]);

...so you can do this in any template requiring an upload:

<a ngf-select ngf-change="upload($files)" ng-model="$ctrl.data.item.property" foobar-upload href>
    click to upload files!
</a>

The property property will likely also vary, so you would extend your directive to take that from an attribute.

Option 3: registering a handler directly as a resolve

If uploading doesn't need access to the scope, you could also define the upload function as a resolve and inject it into your component:

//...
$routeProvider.when('/some/path/:id/', {
    //...
    resolve: {
        file: ['Upload', function (Upload) {
        }]
    }
//...
app.component('myComponent', {
    template: '<my-component data="$resolve.data" file="$resolve.file"></my-component>',
    bindings: {data: '<', file: '<'}
});

...and in your HTML template...

<!-- schema.html -->
<a href ngf-select="$ctrl.file($file)">Upload something!</a>

In CKEditor

There are Angular modules that handle this in a pure Angular sense. To be honest, we haven't tried them. Google is your friend. This is just a stock CKEditor implementation.

The basic idea is that you extend your CKEditor configuration with a few URLs that handle the file selection. Assuming you need it available globally, you would do this:

app.run(['$rootScope', function ($rootScope) {
    $rootScope.ckeditor = {
        filebrowserBrowseUrl: '/api/browse/file/',
        filebrowserImageBrowseUrl: '/api/browse/image/',
        filebrwoserFlashBrowseUrl: '/api/browse/flash/'
    };
}]);

(The exact URLs are random of course; use what you like.) Now pass that value into the ckeditor directive as an argument (quite how you expose this is up to you - probably you'll want to extend the root controller or something).

Then in your backend, make sure those URLs serve an HTML page showing thumbnails or whatever. For each selectable file, on selection call a CKEditor function on the opening window with two parameters: a function number (passed through a GET parameter called CKEditorFuncNum) and the public URL of that file. E.g.:

<a href="/url/to/file/1"
    onclick="window.opener.CKEDITOR.tools.callFunction(CKEditorFuncNum, '/url/to/file/1')">
    select file 1
</a>

Exactly how you get to the value of CKEditorFuncNum is of course language-specific. In PHP for instance, it'd obviously be $_GET['CKEditorFuncNum'].

You can then do fancy stuff like: