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.