Settings

new and improved

“A presentation is something you write to give people information. Presentations are for important things. A presentation can bring people together. A presentation can be a call to arms, a manifesto, a poem. A presentation can change the world. A presentation is something you write to give people information. Presentations are for important things. A presentation can bring people together. A presentation can be a call to arms, a manifesto, a poem. A presentation can change the world.”
Foreword from Sergey

Diagram of settings

 

const contentConfig = require('./../../config/apps/form_builder/content').default;
const ObservableStore = require('./../../config/apps/form_builder/observable_store').default;
self.observableStore = new ObservableStore();
self.observableStore.model = this.model;        
self.observableStore.experiments = window.GLOBALS;

const content = ReactDOM.render(
  <PowrSection componentsList={contentConfig} store={self.observableStore} />,
  document.querySelector('#powr-form-builder-content')
);

This is how settings are built

PowrSection

 

  • Is the main Mobx Observer.
  • All the sections share the same store. Each section knows about the model which is used in entire settings. We are still using the backbone model.
  • So the PowrSection has two main things in it. One is a random number and the second thing is all the children elements of the sections created from the Config.
  • This is responsible for reading the Config for each section and  then recursively rendering the contents within.
  • It also exports a function which will help you recursively create child elements.

 

MOBX

React and MobX together are a powerful combination. React renders the application state by providing mechanisms to translate it into a tree of renderable components. MobX provides the mechanism to store and update the application state that React then uses.

 

https://mobx.js.org/getting-started.html

https://www.sitepoint.com/javascript-decorators-what-they-are/

import React, {Fragment} from 'react';
import {observer} from 'mobx-react';

@observer
class PowrComponent extends React.Component {

  constructor() {
    this.state = { internalThingOnly: false }
  }
 
   
  render() {
    return (
      <Fragment>
        { this.props.mobxStore.somethingGlobal? this.renderX() : this.renderY() }
        { this.state.internalThingOnly ? this.renderA() : this.renderB() }
      </Fragment>
    );
  }
}


// observable store
import {decorate, observable, action} from 'mobx';

class ObservableStore {
  somethingGlobal = 1;
  updateSomethingGLobal(newValue) { this.somethingGlobal = newValue; }
}


decorate(ObservableStore, {
  somethingGlobal: observable,
  updateSomethingGLobal: action
});



some other component 
this.store.updateSomethingGLobal('2')

<PowrComponent mobxStore=this.store/>

The @observer decorator from the mobx-react package wraps React component render method in autorun, automatically keeping your components in sync with the state.

import React, {Fragment} from 'react';
import {observer} from 'mobx-react';

@observer
class PowrSection extends React.Component {
  render() {
    return (
      <Fragment>
        <div className="hid">{this.props.store.randomNumber}</div>
        {recursivelyBuildComponents(this.props)}
      </Fragment>
    );
  }
}

[New Syntax Alert]

Section Config

  • This is an array of components that go within a section
  • This is the replacement for data passing through hidden inputs haml.

Before (HAML, helpers etc)

Now (JSON - explicit)

What Section Config is

An array of js objects, where each item has:

  • powrComponent - actual React component
  • passedProps - react component's props (they are hardcoded and don't change based on user or app)
  • dynamicProps - these are based on the model (for example if you need to pass an ID or createdAt). They come from the store
  • conditionals - when to show this component
  • innerComponents - children of component

What Section Config is

An array of js objects, where each item has:

  • powrComponent - actual React component
  • passedProps - react component's props (they are hardcoded and don't change based on user or app)
  • dynamicProps - these are based on the model (for example if you need to pass an ID or createdAt). They come from the store
  • conditionals - when to show this component
  • innerComponents
{
    powrComponent: PowrPanel,
    passedProps: {
      namespace: 'importExisting',
    },
    dynamicProps: [
      {
        propName: 'panelTitle',
        key: 'store.model.meta.app_common_name',
        evaluate: currentValue => {
          return `${sc('import_existing')} ${currentValue}`;
        },
      },
    ],
    innerComponents: [
        {
            powrComponent: AppsList,
        }
    ],
}

Dynamic Props

dynamicProps: [
  {
    propName: 'mailchimpList',
    key: 'observableStore.model.attributes.mailchimpList',
  },
  {
    propName: 'mailchimpListName',
    key: 'observableStore.model.attributes.mailchimpListName',
  },
  {
    propName: 'appId',
    key: 'observableStore.model.meta.id',
  },
  {
    propName: 'externalUserId',
    key: 'observableStore.model.meta.external_user_id',
  },
],

propName is a React component's prop

key is where the data comes from

 

<MailchimpButton
    appId={134}
    external_user_id={456}>
</MailchimpButton>

More about dynamicProps

{
    powrComponent: PowrPanel,
    passedProps: {
      namespace: 'importExisting',
    },
    dynamicProps: [
      {
        propName: 'panelTitle',
        key: 'store.model.meta.app_common_name',
        evaluate: currentValue => {
          return `${sc('import_existing')} ${currentValue}`;
        },
      },
    ],
    innerComponents: [
        {
            powrComponent: AppsList,
        }
    ],
}

Conditionals - when to show this component

{
    powrComponent: PowrToggle,
    passedProps: {
      label: sc('require_payment'),
      namespace: 'paymentRequired',
      additionalClasses: 'form-element',
      helpText: sc('require_payment_help'),
      handleChangeComplete: (value, store) => {
        store.updateValue('model.paymentRequired', value, true);
      },
    },
  },

Conditionals - when to show this component

{
    powrComponent: 'div',
    passedProps: {
      className: 'margin-top-l',
    },
    conditionals: [
      {
        key: 'store.model.attributes.paymentRequired',
        value: true,
      },
    ],
    innerComponents: [
      {
        powrComponent: PowrTextInput,
        passedProps: {
          label: sc('Paypal Account'),
          namespace: `${namespace}PaypalAccount`,
          additionalClasses: 'form-element',
          placeholder: sc('enter_paypal_email'),
          type: 'email',
        },
      },
...
 conditionals: [
          {
            key: `store.model.attributes.${namespace}PurchaseType`,
            check: currentVal => {
              return currentVal !== 'Recurring';
            },
          },
]

There're 3 ways to handle handleChangeComplete in passedProps

handleChangeComplete: value => {
    if (passedProps.handleChangeComplete) {
      passedProps.handleChangeComplete(value, store);
    } else {
      if (passedProps.namespace) {
        store.updateValue(`model.${passedProps.namespace}`, value);
      }
    }
}

1) Default: don't pass handleChangeComplete if you don't want to modify saved data. It will be done automatically.

There're 3 ways to handle handleChangeComplete in passedProps

handleChangeComplete: value => {
    if (passedProps.handleChangeComplete) {
      passedProps.handleChangeComplete(value, store);
    } else {
      if (passedProps.namespace) {
        store.updateValue(`model.${passedProps.namespace}`, value);
      }
    }
}

1) Default: don't pass handleChangeComplete if you don't want to modify saved data. It will be done automatically.

There're 3 ways to handle handleChangeComplete in passedProps

handleChangeComplete: value => {
    if (passedProps.handleChangeComplete) {
      passedProps.handleChangeComplete(value, store);
    } else {
      if (passedProps.namespace) {
        store.updateValue(`model.${passedProps.namespace}`, value);
      }
    }
}

1) Default: don't pass handleChangeComplete if you don't want to modify saved data. It will be done automatically.

You don't have to do anything if namespace matches the model name.

{
    powrComponent: PowrColorPicker,
    passedProps: {
      label: sc('border_color'),
      namespace: 'backgroundBorderColor',
      additionalClasses: 'form-element',
      handleChangeComplete: (value, store) => {
       store.updateValue(`model.${namespace}`, makeColorString(value));
      },
      alpha: true,
    },
},

2) Needs to change value before saving to db

We are using a function makeColorString to modify value before saving it to database.

 

{
    powrComponent: PowrToggle,
    passedProps: {
      label: sc('require_payment'),
      namespace: 'paymentRequired',
      additionalClasses: 'form-element',
      helpText: sc('require_payment_help'),
      handleChangeComplete: (value, store) => {
        store.updateValue('model.paymentRequired', value, true);
      },
    },
  },

3) When we need to re-render something in the settings after changing the value.

3) When we need to re-render something in the settings after changing the value.

Mobx is not great in working with backbone models so we force rerender using small hack.

Mobx

updateValue(key, value, refresh = false) {
    const backboneModel = key.match(/model\.(.*)/);
    if (backboneModel) {
      const newVal = [];
      newVal[backboneModel[1]] = value;
      this.model.set(newVal);
      // This is sort of a hack so we can continue using models and mobx together
      // set does not really tell mobx observers that anything changed but it tells everyplace else what needs to be done.
      // anytime we need to change the settings portion of things to follow something that changed on the model, pass refresh as true, otherwise we don't really need to do anything.
      if (refresh) {
        this.randomNumber = new Date().getTime();
      }
    } else {
      this[key] = _.clone(value);
    }
  }

Small hack to force rerender

updateValue(key, value, refresh = false) {
    const backboneModel = key.match(/model\.(.*)/);
    if (backboneModel) {
      const newVal = [];
      newVal[backboneModel[1]] = value;
      this.model.set(newVal);
      // This is sort of a hack so we can continue using models and mobx together
      // set does not really tell mobx observers that anything changed but it tells everyplace else what needs to be done.
      // anytime we need to change the settings portion of things to follow something that changed on the model, pass refresh as true, otherwise we don't really need to do anything.
      if (refresh) {
        this.randomNumber = new Date().getTime();
      }
    } else {
      this[key] = _.clone(value);
    }
  }

Small hack to force rerender

Inner Components

Inner Components

PowrDrilldown Component

Inner Components

Inner Components in Code:

const formbuilderDesign = [
  {
    powrComponent: PowrDrilldown,
    passedProps: {
      namespace: 'layout',
      label: sc('layout'),
    },
    innerComponents: [
      {
        powrComponent: PowrMultitoggle,
        passedProps: {
          label: sc('select_label_style'),
          namespace: 'labelType',
          options: [[sc('inline_labels'), 'inlineLabels'], [sc('block_labels'), 'blockLabels']],
          additionalClasses: 'form-element',
        },
      },
      {
        powrComponent: PowrMultitoggle,
        passedProps: {
          label: sc('title_alignment'),
          namespace: 'titleAlign',
          options: [[sc('left'), 'left'], [sc('center'), 'center'], [sc('right'), 'right']],
          additionalClasses: 'form-element',
        },
      },
      {
        powrComponent: PowrSlider,
        passedProps: {
          label: sc('input_field_border_radius'),
          namespace: 'inputBorderRadius',
          min: 0,
          max: 20,
          measure: 'px',
          step: 1,
          renderStyleOnly: true,
          additionalClasses: 'form-element',
        },
      },
    ],
  },

To Be Continued ...

https://mobx.js.org/getting-started.html

https://www.sitepoint.com/javascript-decorators-what-they-are/

deck

By Praneeta Mhatre