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
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>
);
}
}Before (HAML, helpers etc)
Now (JSON - explicit)
An array of js objects, where each item has:
An array of js objects, where each item has:
{
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,
}
],
}
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>{
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,
}
],
}
{
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);
},
},
},
{
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';
},
},
]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.
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.
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
PowrDrilldown Component
Inner Components
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',
},
},
],
},
https://mobx.js.org/getting-started.html
https://www.sitepoint.com/javascript-decorators-what-they-are/