copy-edit viget posts
This commit is contained in:
@@ -2,38 +2,37 @@
|
||||
title: "Using Microcosm Presenters to Manage Complex Features"
|
||||
date: 2017-06-14T00:00:00+00:00
|
||||
draft: false
|
||||
needs_review: true
|
||||
canonical_url: https://www.viget.com/articles/using-microcosm-presenters-to-manage-complex-features/
|
||||
---
|
||||
|
||||
We made [Microcosm](http://code.viget.com/microcosm/) to help us manage
|
||||
state and data flow in our JavaScript applications. We think it\'s
|
||||
state and data flow in our JavaScript applications. We think it's
|
||||
pretty great. We recently used it to help our friends at
|
||||
[iContact](https://www.icontact.com/) launch a [brand new email
|
||||
editor](https://www.icontact.com/big-news). Today, I\'d like to show you
|
||||
editor](https://www.icontact.com/big-news). Today, I'd like to show you
|
||||
how I used one of my favorite features of Microcosm to ship a
|
||||
particularly gnarly feature.
|
||||
|
||||
In addition to adding text, photos, and buttons to their emails, users
|
||||
can add *code blocks* which let them manually enter HTML to be inserted
|
||||
into the email. The feature in question was to add server-side code
|
||||
santization, to make sure user-submitted HTML isn\'t invalid or
|
||||
santization, to make sure user-submitted HTML isn't invalid or
|
||||
potentially malicious. The logic is roughly defined as follows:
|
||||
|
||||
- User modifies the HTML & hits \"preview\";
|
||||
- User modifies the HTML & hits "preview";
|
||||
- HTML is sent up to the server and sanitized;
|
||||
- The resulting HTML is displayed in the canvas;
|
||||
- If the code is unmodified, user can \"apply\" the code or continue
|
||||
- If the code is unmodified, user can "apply" the code or continue
|
||||
editing;
|
||||
- If the code is modified, user can \"apply\" the modified code or
|
||||
\"reject\" the changes and continue editing;
|
||||
- If the code is modified, user can "apply" the modified code or
|
||||
"reject" the changes and continue editing;
|
||||
- If at any time the user unfocuses the block, the code should return
|
||||
to the last applied state.
|
||||
|
||||
Here\'s a flowchart that might make things clearer (did for me, in any
|
||||
Here's a flowchart that might make things clearer (did for me, in any
|
||||
event):
|
||||
|
||||

|
||||

|
||||
|
||||
This feature is too complex to handle with React component state, but
|
||||
too localized to store in application state (the main Microcosm
|
||||
@@ -49,18 +48,18 @@ First, we define some
|
||||
[Actions](http://code.viget.com/microcosm/api/actions.html) that only
|
||||
pertain to this Presenter:
|
||||
|
||||
``` {.code-block .line-numbers}
|
||||
```javascript
|
||||
const changeInputHtml = html => html
|
||||
const acceptChanges = () => {}
|
||||
const rejectChanges = () => {}
|
||||
```
|
||||
|
||||
We don\'t export these functions, so they only exist in the context of
|
||||
We don't export these functions, so they only exist in the context of
|
||||
this file.
|
||||
|
||||
Next, we\'ll define the Presenter itself:
|
||||
Next, we'll define the Presenter itself:
|
||||
|
||||
``` {.code-block .line-numbers}
|
||||
```javascript
|
||||
class CodeEditor extends Presenter {
|
||||
setup(repo, props) {
|
||||
repo.addDomain('html', {
|
||||
@@ -81,10 +80,10 @@ invoke the
|
||||
function to add a new domain to the forked repo. The main repo will
|
||||
never know about this new bit of state.
|
||||
|
||||
Now, let\'s instruct our new domain to listen for some actions:
|
||||
Now, let's instruct our new domain to listen for some actions:
|
||||
|
||||
``` {.code-block .line-numbers}
|
||||
register() {
|
||||
```javascript
|
||||
register() {
|
||||
return {
|
||||
[scrubHtml]: this.scrubSuccess,
|
||||
[changeInputHtml]: this.inputHtmlChanged,
|
||||
@@ -100,10 +99,10 @@ method defines the mapping of Actions to handler functions. You should
|
||||
recognize those actions from the top of the file, minus `scrubHtml`,
|
||||
which is defined in a separate API module.
|
||||
|
||||
Now, still inside the domain object, let\'s define some handlers:
|
||||
Now, still inside the domain object, let's define some handlers:
|
||||
|
||||
``` {.code-block .line-numbers}
|
||||
inputHtmlChanged(state, inputHtml) {
|
||||
```javascript
|
||||
inputHtmlChanged(state, inputHtml) {
|
||||
let status = inputHtml === state.originalHtml ? 'start' : 'changed'
|
||||
|
||||
return { ...state, inputHtml, status }
|
||||
@@ -124,11 +123,11 @@ inputHtmlChanged(state, inputHtml) {
|
||||
```
|
||||
|
||||
Handlers always take `state` as their first object and must return a new
|
||||
state object. Now, let\'s add some more methods to our main `CodeEditor`
|
||||
state object. Now, let's add some more methods to our main `CodeEditor`
|
||||
class.
|
||||
|
||||
``` {.code-block .line-numbers}
|
||||
renderPreview = ({ html }) => {
|
||||
```javascript
|
||||
renderPreview = ({ html }) => {
|
||||
this.send(updateBlock, this.props.block.id, {
|
||||
attributes: { htmlCode: html }
|
||||
})
|
||||
@@ -148,10 +147,10 @@ the canvas with the given HTML. And `componentWillUnmount` is noteworthy
|
||||
in that it demonstrates that Presenters are just React components under
|
||||
the hood.
|
||||
|
||||
Next, let\'s add some buttons to let the user trigger these actions.
|
||||
Next, let's add some buttons to let the user trigger these actions.
|
||||
|
||||
``` {.code-block .line-numbers}
|
||||
buttons(status, html) {
|
||||
```javascript
|
||||
buttons(status, html) {
|
||||
switch (status) {
|
||||
case 'changed':
|
||||
return (
|
||||
@@ -183,10 +182,10 @@ that triggers an action when pressed. Its callback functionality (e.g.
|
||||
`onOpen`, `onDone`) lets you update the button as the action moves
|
||||
through its lifecycle.
|
||||
|
||||
Finally, let\'s bring it all home and create our model and view:
|
||||
Finally, let's bring it all home and create our model and view:
|
||||
|
||||
``` {.code-block .line-numbers}
|
||||
getModel() {
|
||||
```javascript
|
||||
getModel() {
|
||||
return {
|
||||
status: state => state.html.status,
|
||||
inputHtml: state => state.html.inputHtml
|
||||
@@ -232,11 +231,11 @@ demonstrates how you interact with the model.
|
||||
The big takeaways here:
|
||||
|
||||
**Presenters can have their own repos.** These can be defined inline (as
|
||||
I\'ve done) or in a separate file/object. I like seeing everything in
|
||||
I've done) or in a separate file/object. I like seeing everything in
|
||||
one place, but you can trot your own trot.
|
||||
|
||||
**Presenters can manage their own state.** Presenters receive a fork of
|
||||
the main app state when they\'re instantiated, and changes to that state
|
||||
the main app state when they're instantiated, and changes to that state
|
||||
(e.g. via an associated domain) are not automatically synced back to the
|
||||
main repo.
|
||||
|
||||
@@ -246,15 +245,15 @@ in `renderPreview` above) to push changes up the chain.
|
||||
|
||||
**Presenters can have their own actions.** The three actions defined at
|
||||
the top of the file only exist in the context of this file, which is
|
||||
exactly what we want, since that\'s the only place they make any sense.
|
||||
exactly what we want, since that's the only place they make any sense.
|
||||
|
||||
**Presenters are just React components.** Despite all this cool stuff
|
||||
we\'re able to do in a Presenter, under the covers, they\'re nothing but
|
||||
we're able to do in a Presenter, under the covers, they're nothing but
|
||||
React components. This way you can still take advantage of lifecycle
|
||||
methods like `componentWillUnmount` (and `render`, natch).
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
So those are Microcosm Presenters. We think they\'re pretty cool, and
|
||||
So those are Microcosm Presenters. We think they're pretty cool, and
|
||||
hope you do, too. If you have any questions, hit us up on
|
||||
[GitHub](https://github.com/vigetlabs/microcosm) or right down there.
|
||||
|
||||
Reference in New Issue
Block a user