API Docs for:
Show:

File: development/modules/ajaxapp/js/ajaxapp.js

import H from 'handlebars';
import {pipe} from '../../../js/utils/func';
import Channels from '../../../js/globals/channels';
import DOMModule from '../../__base/dommodule';
import Pagination from '../../fke-pagination/js/pagination';

/**
 * @module Modules
 */

const handleSubmit = Symbol('_handleSubmit');
const handleResize = Symbol('_handleResize');
    
function fieldsToHashMap (xs) {
    return xs.reduce((hashmap, x) => {
        hashmap[x.name] = x.value;
        return hashmap;
    }, {});
}

function childsHeight (node) {
    return $(node).children().toArray().
        reduce((acc, child) => { 
            return $(child).outerHeight(true) + acc;
        }, 0);
}

/**
 * AjaxApp module
 * @class Modules.AjaxApp
 * @constructor
 * @extends Modules._DOMModule
 */
class AjaxApp extends DOMModule {
    constructor(options) {
        super(options);

        this._action = this.$rootNode.prop('action');
        this._method = this.$rootNode.attr('method') || 'POST';
    }
    /**
     * Creates a new AjaxApp instance without the "new" keyword but does not
     *     initialize it
     * @method of
     * @static
     * @for Modules.AjaxApp
     * @param {node|string|configs} node Node, CSS selector or configuration object
     * @return {AjaxApp} A new instance
     * 
     * @example
     *     import AjaxApp from './modules/ajaxapp/js/ajaxapp';
     *
     *     const myAjaxApp = AjaxApp.of('[data-widget=AjaxApp]');
     *     myAjaxApp.render().startUp();
     */
    static of (node) {
        return new AjaxApp(node);
    }
    /**
     * Finds and initializes all AjaxApps on a page at once
     * @method findAll
     * @static
     * @for Modules.AjaxApp
     * @param {string} selector CSS selector to match nodes against
     * @return {array} A list of instances
     * 
     * @example
     *     import AjaxApp from './modules/ajaxapp/js/ajaxapp';
     *
     *     AjaxApp.findAll('[data-widget=AjaxApp]');
     */
    static findAll(selector) {
        return $(selector).toArray().filter((node) => {
                return !$(node).data('defer');
            }).map((node) => {
                const ajaxapp = AjaxApp.of(node);
                ajaxapp.render().startUp();
                return ajaxapp;
            });
    }


    /**
     * Renders missing nodes into the instance and returns it
     * @method render
     * @for Modules.AjaxApp
     * @return {this} The instance
     * 
     * @example
     *     import AjaxApp from './modules/ajaxapp/js/ajaxapp';
     *
     *     const myAjaxApp = AjaxApp.of('[data-widget=AjaxApp]');
     *     myAjaxApp.render();
     */
    render() {
        var $results = $(this.$data('ajaxapp-result-container'));
        var $spinner = $('<div class="loader">Loading...</div>');

        this.$resultContainer = $results.after($spinner);

        return this;
    }

    /**
     * Initializes sub-components, attaches all eventlisteners to the instance.
     *      Initiates initial Ajax-Request if configured that way 
     *      (data-ajaxapp-initcall)
     * @method startUp
     * @for Modules.AjaxApp
     * @return {undefined} Nothing
     * 
     * @example
     *     import AjaxApp from './modules/ajaxapp/js/ajaxapp';
     *
     *     const myAjaxApp = AjaxApp.of('[data-widget=AjaxApp]');
     *     myAjaxApp.render().startUp();
     */
    startUp() {
        this._initResultTmpl();
        this._initPagination();

        this.onSubmit(handleSubmit);
        this.onResize(handleResize)
        this.$findChilds('input[type="radio"]').on('change', this[handleSubmit].bind(this))
        this.$findChilds('select').on('change', this[handleSubmit].bind(this));
        this.$findChilds('input.datepicker').on('change', this[handleSubmit].bind(this));

        if (this.$data('ajaxapp-initcall')) {
            this.request({ desiredPage: 1}).
                done(this._requestSuccess.bind(this));
                // .fail(); // TODO
        }
    }

    /**
     * Initiates ajax request to server.
     *      Initiates initial Ajax-Request if configured that way 
     *      (data-ajaxapp-initcall)
     * @method request
     * @param {object} info information that have to be stored before requests
     *      at the moment: { desiredPage: 1 } for requested page.
     * @for Modules.AjaxApp
     * @return {jqXHR} pending request
     * 
     * @example
     *     import AjaxApp from './modules/ajaxapp/js/ajaxapp';
     *
     *     const myAjaxApp = AjaxApp.of('[data-widget=AjaxApp]');
     *     myAjaxApp.render().startUp();
     */
    request(info = { desiredPage: 1 }) {
        if (this._requestInFlight) {
            this._requestInFlight.abort();
        }

        this.$findChilds('input[data-ajaxapp-pagination]').val(info.desiredPage);

        this.$resultContainer.
            addClass('is-loading').
            css({ 'height': this.$resultContainer.height() });

        this._requestInFlight = $.ajax({
            url: this._action,
            method: this._method,
            data: fieldsToHashMap(this.$rootNode.serializeArray()),
            dataType: 'json'
        });

        return this._requestInFlight;
    }

    // === PRIVATE STUFF ===
    _initResultTmpl() {
        var $resultTmpl = $(this.$data('ajaxapp-result-tmpl')).remove();
        this._tmpl = H.compile($resultTmpl.first().html());
    }
    _initPagination() {
        var $pagination = $(this.$data('ajaxapp-pagination'));

        if ($pagination.length) {
            this.$$pagination = Pagination.of($pagination[0]);
            this.$$pagination.render().startUp();
            this.$$pagination.addObserver({ update: this._handlePaginationChange.bind(this) });
        }
    }

    _requestSuccess(data) {
        // i know piping doesn't make any sense here, but i'm not sure if i'm
        // gonna need it some time.
        pipe(
            this._updatePagination.bind(this),
            this._renderResults.bind(this),
            this._showResults.bind(this)
        )(data);

        setTimeout(() => {
            this.publish(Channels.CONTENT_HEIGHTCHANGED)
        }, 300); // cause there's a transition on the height of <ul>
    }
    
    _renderResults(data) {
        this.$resultContainer.html(this._tmpl(data));

        return data;
    }
    _showResults(data) {
        this.$resultContainer.
            removeClass('is-loading').
            css({ 'height': childsHeight(this.$resultContainer) });

        return data;
    }
    _updatePagination(data) {
        if (this.$$pagination) {
            this.$$pagination.setCurrentPage(1);
            this.$$pagination.setPageCount(data.pages);
        }

        return data;
    }
    // === OBSERVER HANDLERS ===
    _handlePaginationChange(data) {
        this.request({ desiredPage: data.page }).
            done(pipe(
                this._renderResults.bind(this),
                this._showResults.bind(this)
            ));
            // done((data) => {
            //     setTimeout(() => {
            //         this._renderResults(data),
            //         this._showResults(data)
            //     }, 2000)
            // });
    }
    // === EVENT HANDLERS ===
    [handleSubmit](event) {
        event.preventDefault();
        this.request({ desiredPage: 1 }).
            done(this._requestSuccess.bind(this));
            // done((data) => {
            //     setTimeout(() => {
            //         this._requestSuccess(data);
            //     }, 2000)
            // });
    }
    [handleResize](event) {
        event.preventDefault();
        this.$resultContainer.
            css({ 'height': childsHeight(this.$resultContainer) });
    }
}

export default AjaxApp;