davideisinger.com

My personal website
Log | Files | Refs | README

index.md (8911B)


      1 ---
      2 title: "Using Microcosm Presenters to Manage Complex Features"
      3 date: 2017-06-14T00:00:00+00:00
      4 draft: false
      5 canonical_url: https://www.viget.com/articles/using-microcosm-presenters-to-manage-complex-features/
      6 ---
      7 
      8 We made [Microcosm](http://code.viget.com/microcosm/) to help us manage
      9 state and data flow in our JavaScript applications. We think it's
     10 pretty great. We recently used it to help our friends at
     11 [iContact](https://www.icontact.com/) launch a [brand new email
     12 editor](https://www.icontact.com/big-news). Today, I'd like to show you
     13 how I used one of my favorite features of Microcosm to ship a
     14 particularly gnarly feature.
     15 
     16 In addition to adding text, photos, and buttons to their emails, users
     17 can add *code blocks* which let them manually enter HTML to be inserted
     18 into the email. The feature in question was to add server-side code
     19 sanitization, to make sure user-submitted HTML isn't invalid or
     20 potentially malicious. The logic is roughly defined as follows:
     21 
     22 -   User modifies the HTML & hits "preview";
     23 -   HTML is sent up to the server and sanitized;
     24 -   The resulting HTML is displayed in the canvas;
     25 -   If the code is unmodified, user can "apply" the code or continue
     26     editing;
     27 -   If the code is modified, user can "apply" the modified code or
     28     "reject" the changes and continue editing;
     29 -   If at any time the user unfocuses the block, the code should return
     30     to the last applied state.
     31 
     32 Here's a flowchart that might make things clearer (did for me, in any
     33 event):
     34 
     35 {{<dither URfAcl9.png>}}Hand-drawn state diagram showing a workflow progressing from “Start” through states like “Changed,” a “modified?” decision node, and either “Validated” or “Modified,” with arrows labeled “apply,” “revert,” “update,” and “preview” looping back to an “Unfocused” end state.{{</dither>}}
     36 
     37 This feature is too complex to handle with React component state, but
     38 too localized to store in application state (the main Microcosm
     39 instance). Fortunately, Microcosm gives us the perfect tool to handle
     40 this scenario:
     41 [Presenters](http://code.viget.com/microcosm/api/Presenter.html).
     42 
     43 Using a Presenter, we can build an app-within-an-app, with a unique
     44 domain, actions, and state, and communicate with the main repository as
     45 necessary.
     46 
     47 First, we define some
     48 [Actions](http://code.viget.com/microcosm/api/actions.html) that only
     49 pertain to this Presenter:
     50 
     51 ```javascript
     52 const changeInputHtml = html => html
     53 const acceptChanges = () => {}
     54 const rejectChanges = () => {}
     55 ```
     56 
     57 We don't export these functions, so they only exist in the context of
     58 this file.
     59 
     60 Next, we'll define the Presenter itself:
     61 
     62 ```javascript
     63 class CodeEditor extends Presenter {
     64   setup(repo, props) {
     65     repo.addDomain('html', {
     66       getInitialState() {
     67         return {
     68           originalHtml: props.block.attributes.htmlCode,
     69           inputHtml: props.block.attributes.htmlCode,
     70           unsafeHtml: null,
     71           status: 'start'
     72         }
     73       },
     74 ```
     75 
     76 The `setup` function is invoked when the Presenter is created. It
     77 receives a fork of the main Microcosm repo as its first argument. We
     78 invoke the
     79 [`addDomain`](http://code.viget.com/microcosm/api/microcosm.html#adddomainkey-config-options)
     80 function to add a new domain to the forked repo. The main repo will
     81 never know about this new bit of state.
     82 
     83 Now, let's instruct our new domain to listen for some actions:
     84 
     85 ```javascript
     86       register() {
     87         return {
     88           [scrubHtml]: this.scrubSuccess,
     89           [changeInputHtml]: this.inputHtmlChanged,
     90           [acceptChanges]: this.changesAccepted,
     91           [rejectChanges]: this.changesRejected
     92         }
     93       },
     94 ```
     95 
     96 The
     97 [`register`](http://code.viget.com/microcosm/api/domains.html#register)
     98 method defines the mapping of Actions to handler functions. You should
     99 recognize those actions from the top of the file, minus `scrubHtml`,
    100 which is defined in a separate API module.
    101 
    102 Now, still inside the domain object, let's define some handlers:
    103 
    104 ```javascript
    105       inputHtmlChanged(state, inputHtml) {
    106         let status = inputHtml === state.originalHtml ? 'start' : 'changed'
    107 
    108         return { ...state, inputHtml, status }
    109       },
    110       
    111       scrubSuccess(state, { html, modified }) {
    112         if (modified) {
    113           return {
    114             ...state,
    115             status: 'modified',
    116             unsafeHtml: state.inputHtml,
    117             inputHtml: html
    118           }
    119         } else {
    120           return { ...state, status: 'validated' }
    121         }
    122       },
    123 ```
    124 
    125 Handlers always take `state` as their first object and must return a new
    126 state object. Now, let's add some more methods to our main `CodeEditor`
    127 class.
    128 
    129 ```javascript
    130   renderPreview = ({ html }) => {
    131     this.send(updateBlock, this.props.block.id, {
    132       attributes: { htmlCode: html }
    133     })
    134   }
    135   
    136   componentWillUnmount() {
    137     this.send(updateBlock, this.props.block.id, {
    138       attributes: { htmlCode: this.repo.state.html.originalHtml }
    139     })
    140   }
    141 ```
    142 
    143 Couple cool things going on here. The `renderPreview` function uses
    144 [`this.send`](http://code.viget.com/microcosm/api/presenter.html#sendaction-...params)
    145 to send an action to the main Microcosm instance, telling it to update
    146 the canvas with the given HTML. And `componentWillUnmount` is noteworthy
    147 in that it demonstrates that Presenters are just React components under
    148 the hood.
    149 
    150 Next, let's add some buttons to let the user trigger these actions.
    151 
    152 ```javascript
    153   buttons(status, html) {
    154     switch (status) {
    155       case 'changed':
    156         return (
    157           <div styleName="buttons">
    158             <ActionButton
    159               action={scrubHtml}
    160               value={html}
    161               onDone={this.renderPreview}
    162             >
    163               Preview changes
    164             </ActionButton>
    165           </div>
    166         )
    167       case 'validated':
    168         return (
    169           <div styleName="buttons">
    170             <ActionButton action={acceptChanges}>
    171               Apply changes
    172             </ActionButton>
    173           </div>
    174         )
    175       // ...
    176 ```
    177 
    178 The
    179 [ActionButton](http://code.viget.com/microcosm/api/action-button.html)
    180 component is pretty much exactly what it says on the tin --- a button
    181 that triggers an action when pressed. Its callback functionality (e.g.
    182 `onOpen`, `onDone`) lets you update the button as the action moves
    183 through its lifecycle.
    184 
    185 Finally, let's bring it all home and create our model and view:
    186 
    187 ```javascript
    188   getModel() {
    189     return {
    190       status: state => state.html.status,
    191       inputHtml: state => state.html.inputHtml
    192     }
    193   }
    194 
    195   render() {
    196     const { status, inputHtml } = this.model
    197     const { name } = this.props
    198 
    199     return (
    200       <div>
    201         {this.buttons(status, inputHtml)}
    202 
    203         <textarea
    204           id={name}
    205           name={name}
    206           value={inputHtml}
    207           onChange={e => this.repo.push(changeInputHtml, e.target.value)}
    208           disabled={status === 'modified'}
    209           styleName="textarea"
    210         />
    211       </div>
    212     )
    213   }
    214 }
    215 ```
    216 
    217 The
    218 [docs](http://code.viget.com/microcosm/api/presenter.html#getmodelprops-state)
    219 explain `getModel` better than I can:
    220 
    221 > `getModel` assigns a model property to the presenter, similarly to
    222 > `props` or `state`. It is recalculated whenever the Presenter's
    223 > `props` or `state` changes, and functions returned from model keys are
    224 > invoked every time the repo changes.
    225 
    226 The `render` method is pretty straight-ahead React, though it
    227 demonstrates how you interact with the model.
    228 
    229 ------------------------------------------------------------------------
    230 
    231 The big takeaways here:
    232 
    233 **Presenters can have their own repos.** These can be defined inline (as
    234 I've done) or in a separate file/object. I like seeing everything in
    235 one place, but you can trot your own trot.
    236 
    237 **Presenters can manage their own state.** Presenters receive a fork of
    238 the main app state when they're instantiated, and changes to that state
    239 (e.g. via an associated domain) are not automatically synced back to the
    240 main repo.
    241 
    242 **Presenters can use `send` to communicate with the main repository.**
    243 Despite holding a fork of state, you can still use `this.send` (as we do
    244 in `renderPreview` above) to push changes up the chain.
    245 
    246 **Presenters can have their own actions.** The three actions defined at
    247 the top of the file only exist in the context of this file, which is
    248 exactly what we want, since that's the only place they make any sense.
    249 
    250 **Presenters are just React components.** Despite all this cool stuff
    251 we're able to do in a Presenter, under the covers, they're nothing but
    252 React components. This way you can still take advantage of lifecycle
    253 methods like `componentWillUnmount` (and `render`, natch).
    254 
    255 ------------------------------------------------------------------------
    256 
    257 So those are Microcosm Presenters. We think they're pretty cool, and
    258 hope you do, too. If you have any questions, hit us up on
    259 [GitHub](https://github.com/vigetlabs/microcosm) or right down there.