SumOfUs/Champaign

View on GitHub
app/javascript/legacy/member-facing/backbone/survey.js

Summary

Maintainability
A
1 hr
Test Coverage
import $ from 'jquery';
import I18n from 'champaign-i18n';
import _ from 'lodash';
import Backbone from 'backbone';
import ErrorDisplay from '../../../shared/show_errors';
import GlobalEvents from '../../../shared/global_events';
import MobileCheck from './mobile_check';

const Survey = Backbone.View.extend({
  el: '.survey',
  HIDDEN_FIELDS: ['source', 'referrer_id', 'rid', 'akid', 'referring_akid'],
  DEFAULT_SCROLL_OFFSET: 80,

  events: {
    'click .survey__skip-button': 'skipSection',
    'ajax:success form.survey__form': 'handleSuccess',
    'ajax:error form.survey__form': 'handleFailure',
    'ajax:send form.survey__form': 'handleSend',
  },

  // options: object with any of the following keys
  //    source: the referring source to save
  //    referrer_id: the champaign id of the referrer
  //    referring_akid: the AK id of the referrer
  //    prefill: an object with fields that will prefill the form
  //    followUpUrl: the url to redirect to after the survey is completed
  //    autoAdvance: if the page has all the data necessary to complete the first
  //      form section by the time the page loads (eg from url params) then if this
  //      param is truthy, the page will automatically scroll.
  //    scrollOffset: the gap to leave between the top of the browser
  //      window and the start of a form when scrolling down. default is 80
  initialize(options = {}) {
    const hasScrollOffset = options.hasOwnProperty('scrollOffset');
    this.scrollOffset = hasScrollOffset
      ? options.scrollOffset
      : this.DEFAULT_SCROLL_OFFSET;
    this.$forms = this.$('.survey__form');
    this.insertHiddenFields(options);
    this.prefill(options.prefill);
    this.revealFirstForm();
    if (options.autoAdvance) this.submitFirstFormIfComplete();
    this.followUpUrl = options.followUpUrl;
    if (!MobileCheck.isMobile()) {
      this.selectizeDropdowns();
    }
    GlobalEvents.bindEvents(this);
  },

  revealFirstForm() {
    this.$('.survey__form')
      .first()
      .removeClass('hidden-closed');
  },

  selectizeDropdowns() {
    this.$(
      '.action-form__country-selector, .survey-form__dropdown'
    ).selectize();
  },

  // prefillValues - an object mapping form names to prefill values
  prefill(prefillValues) {
    this.prefillValues = prefillValues;
    if (!_.isObject(prefillValues)) {
      return;
    }
    this.$('.survey__form input, select').each((ii, field) => {
      const $field = $(field);
      const name = $field.prop('name');
      if (prefillValues.hasOwnProperty(name)) {
        const radioOrCheck =
          $field.prop('type') === 'radio' || $field.prop('type') === 'checkbox';
        if (radioOrCheck && $field.val() == prefillValues[name]) {
          $field.prop('checked', 'checked');
        } else {
          $field.val(prefillValues[name]);
        }
      }
    });
  },

  submitFirstFormIfComplete() {
    // form.serialize() returns the forms values as URL encoded params,
    // so `=&` only happens when there's a blank value in the form, eg
    // name=&weight=180, and `=` at the end means the last param is blank
    const $form = this.$forms.first();
    const serialized = $form.serialize();
    const noneEmpty =
      serialized.indexOf('=&') === -1 && serialized.slice(-1) !== '=';
    const allPresent = _.every($form.find('input, select'), el => {
      if (this.$(el).prop('type') === 'hidden') return true;
      const name = this.$(el).prop('name');
      const outputIncludesName =
        serialized.indexOf(`&${name}=`) !== -1 ||
        serialized.indexOf(`${name}=`) === -0;
      const prefillIncludesName = this.prefillValues[name] !== undefined;
      return outputIncludesName && prefillIncludesName;
    });
    if (noneEmpty && allPresent) $form.submit();
  },

  insertHiddenFields(options) {
    for (const field of this.HIDDEN_FIELDS) {
      if (options[field] && this.$el) {
        this.insertHiddenInput(field, options[field], this.$forms.first());
      }
    }
  },

  insertHiddenInput(name, value, element) {
    $('<input>')
      .attr({
        type: 'hidden',
        name: name,
        value: value,
      })
      .appendTo(element);
  },

  handleSuccess(e, data) {
    ErrorDisplay.clearErrors(this.$(e.target));
    this.enableButton(e);
    this.followForm(this.$(e.target));
  },

  handleFailure(e, data) {
    ErrorDisplay.clearErrors(this.$(e.target));
    ErrorDisplay.show(e, data);
    this.enableButton(e);
  },

  handleSend(e) {
    ErrorDisplay.clearErrors(this.$(e.target));
    this.disableButton(e);
  },

  // we assume all sections are skippable, since required sections should
  // not have a visible skip button
  skipSection(e) {
    this.followForm(this.$(e.target).parents('.survey__form'));
  },

  followForm($form) {
    const $nextForm = $form.next('.survey__form');
    if ($nextForm.length) {
      this.revealForm($nextForm);
    } else {
      this.followUp();
    }
  },

  revealForm($form) {
    $form.removeClass('hidden-closed');
    const position = $form.offset().top - this.scrollOffset; // leave room for header
    $('html, body').animate({ scrollTop: position }, 500);
  },

  followUp() {
    if (this.followUpUrl) {
      this.redirectTo(this.followUpUrl);
    } else {
      window.alert('Thanks for completing our survey!');
    }
  },

  redirectTo(url) {
    window.location.href = url;
  },

  disableButton(e) {
    this.$(e.target)
      .find('.survey__button')
      .each((ii, el) => {
        const $btn = this.$(el);
        if ($btn.data('enabled-text') === undefined) {
          $btn.data('enabled-text', $btn.text());
        }
        if (!$btn.hasClass('survey__skip-button')) {
          $btn.text(I18n.t('form.processing'));
        }
        $btn.addClass('button--disabled');
      });
  },

  enableButton(e) {
    this.$(e.target)
      .find('.survey__button')
      .each((ii, el) => {
        const $btn = this.$(el);
        $btn.text($btn.data('enabled-text'));
        $btn.removeClass('button--disabled');
      });
  },
});

export default Survey;