rx/presenters

View on GitHub
views/mdc/assets/js/components/events/posts.js

Summary

Maintainability
C
1 day
Test Coverage
import {VBase} from './base';
import appConfig from '../../config';
import {expandParams} from './action_parameter';
import {encode} from './encode';
import {getEventTarget} from '../get_event_target';
import {buildFormData} from '../../utils/form-data'

// Replaces a given element with the contents of the call to the url.
// parameters are appended.
export class VPosts extends VBase {
    constructor(options, url, params, method, event, root) {
        super(options, root);
        this.url = url;
        this.params = params;
        this.method = method;
        this.event = event;
        this.headers = options.headers;
    }

    call(results, eventParams) {
        this.clearErrors();
        let method = this.method;

        const ev = new CustomEvent('V:postStarted', {
            bubbles: true,
            cancelable: false,
            detail: this,
            composed: true,
        });

        this.dispatchLifecycleEvent(this.event, ev);

        // Manually build the FormData.
        // Passing in a <form> element (if available) would skip over
        // unchecked toggle elements, which would be unexpected if the user
        // has specified a value for the toggle's `off_value` attribute.
        const formData = new FormData();

        // NB: `inputValues` will appropriately handle `input_tag`.
        for (const [name, value] of this.inputValues()) {
            formData.append(name, value);
        }

        if (eventParams){
            buildFormData(formData, eventParams);
        }

        // Add CSRF authenticity token if present
        const csrf_meta_token = document.querySelector('meta[name=csrf-token]');
        const csrf_meta_param = document.querySelector('meta[name=csrf-param]');
        if (csrf_meta_token && csrf_meta_param) {
            formData.append(csrf_meta_param.content, csrf_meta_token.content);
        }

        // Add params from presenter:
        const expandedParams = expandParams(results, this.params);

        for (const [name, value] of Object.entries(expandedParams)) {
            formData.append(name, encode(value));
        }

        if (this.paramCount(formData) < 1) {
            console.warn(
                'Creating request with no data!'
                + ' Are you sure you\'ve hooked everything up correctly?');
        }

        let errors = this.validate(formData);
        if (errors.length > 0) {
            return new Promise(function(_, reject) {
                results.push({
                    action: 'posts',
                    method: method,
                    statusCode: 400,
                    contentType: 'v/errors',
                    content: errors,
                });
                reject(results);
            });
        }

        const httpRequest = new XMLHttpRequest();
        const url = this.url;
        const callHeaders = this.headers;
        const root = this.root;
        const vEvent = this;
        if (!httpRequest) {
            throw new Error(
                'Cannot talk to server! Please upgrade your browser to one that supports XMLHttpRequest.');
        }

        if (formData.has('rich_text_payload')) {
            callHeaders['X-Rich-Text-Payload'] = true;
        }

        const snackbarCallback = function(contentType, response) {
            const element = root.querySelector('.mdc-snackbar');

            if (!(element && element.vComponent)) {
                return;
            }

            const snackbar = element.vComponent;

            if (contentType && contentType.includes('application/json')) {
                const messages = JSON.parse(response).message;
                if (messages && messages.snackbar) {
                    const message = messages.snackbar.join('<br/>');

                    if (message !== '') {
                        snackbar.display(message);
                    }
                }
            }
        };

        return new Promise((resolve, reject) => {
            httpRequest.onreadystatechange = (event) => {
                if (httpRequest.readyState === XMLHttpRequest.DONE) {
                    const contentType = httpRequest.getResponseHeader('content-type');
                    console.debug(httpRequest.status + ':' + contentType);

                    const result = {
                        action: 'posts',
                        method: httpRequest.method,
                        statusCode: httpRequest.status,
                        contentType: contentType,
                        content: httpRequest.responseText,
                        responseURL: httpRequest.responseURL,
                    };


                    var postFailed = httpRequest.status >= 400;
                    const ev = new CustomEvent(postFailed ? 'V:postFailed' : 'V:postSucceeded', {
                        bubbles: true,
                        cancelable: false,
                        detail: {event: vEvent, result: result},
                        composed: true,
                    });
                    this.dispatchLifecycleEvent(this.event, ev);

                    if (httpRequest.status >= 200 && httpRequest.status < 300) {
                        results.push(result);
                        snackbarCallback(contentType,
                            httpRequest.responseText);
                        resolve(results);
                    }
                    // Response is an html error page
                    else if (contentType && contentType.indexOf('text/html') !== -1) {
                        root.open(contentType);
                        root.write(httpRequest.responseText);
                        root.close();
                        results.push(result);
                        resolve(results);
                    }
                    else {
                        results.push(result);
                        reject(results);
                    }
                    const evFinished = new CustomEvent('V:postFinished', {
                        bubbles: true,
                        cancelable: false,
                        detail: {event: vEvent, result: result},
                        composed: true,
                    });
                    this.dispatchLifecycleEvent(this.event, evFinished);
                }
            };
            // Set up our request
            httpRequest.open(method, url);

            const configHeaders = appConfig.get('request.headers.POST', {});

            for (const [key, value] of Object.entries(configHeaders)) {
                httpRequest.setRequestHeader(key, value);
            }

            if (callHeaders) {
                for (const [key, value] of Object.entries(callHeaders)) {
                    httpRequest.setRequestHeader(key, value);
                }
            }

            // Send our FormData object; HTTP headers are set automatically
            // Rack 2.2 will throw an exception https://github.com/rack/rack/issues/1603
            // if we set the header as multi-part form data with no data in the body
            // So we set the body to null in this case.
            httpRequest.send(this.paramCount(formData) < 1 ? null : formData);
        });
    }

    paramCount(formData){
        return Array.from(formData).length;
    }

    isForm() {
        var parentElement = this.parentElement();
        return parentElement && parentElement.elements;
    }

    form() {
        if (this.isForm()) {
            return this.parentElement();
        }
        return null;
    }

    dispatchLifecycleEvent(domEvent, lifecycleEvent) {
        let target = getEventTarget(domEvent);

        if (!target || !target.isConnected) {
            // If an action has hidden `target` or its parent (via e.g.
            // `hides :some_element`), it will no longer be connected to the DOM
            // and its dispatched lifecycle events won't make it up to the root.
            // Instead, dispatch straight from the root instead of bubbling up
            // from the DOM event's target.
            target = this.root;
        }

        return target.dispatchEvent(lifecycleEvent);
    }
}