The Power of Memento Design Pattern in JavaScript
Tags: javascript, node, webdev, programming
Published: 2022-04-10 14:03:27

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d2kgw9n872x0e11ksgyj.png)

The Memento Pattern in programming is useful in situations where we need a way to restore an object's state.

As a JavaScript developer we work with this concept in many situations especially now in modern web applications.

If you've been developing in the web for some time then you might have heard of the term [hydration](<https://en.wikipedia.org/wiki/Hydration_(web_development)#:~:text=In%20web%20development%2C%20hydration%20or,handlers%20to%20the%20HTML%20elements.>).

If you don't know what hydration is, it's a technique in web development where the client side takes static content which was stored in any programming language such as JSON, JavaScript, HTML, etc. and converts it into code where browsers are able to run during runtime. At that stage JavaScript is run and is able to do things like attach event listeners when the DOM begins running on the page.

The memento pattern is similar. In this post we are going to implement the Memento pattern for the runtime and will not be storing anything statically.

If you worked with `JSON.parse` and `JSON.stringify` chances are you might have accidentally implemented a memento before.

Usually there are three objects that implement the full flow of the Memento pattern:

1. Originator
2. Memento
3. Caretaker

The _Originator_ defines the interface that triggers the creation and storing of itself as the memento.

The _Memento_ is the internal state representation of the Originator that is passed and retrieved from the Caretaker.

The _Caretaker_ has one job: to _store_ or _save_ the memento to be used later. It can retrieve the memento it stored but it does not mutate anything.

## Implementing the Memento Design Pattern

Now that we described the pattern we are going to implement it to master this practice in code.

We will be creating an interactive email input field as a DOM element. We are going to add one smart behavior to our input field so that our user will immediately become aware that they need to add the `@` symbol before submitting.

They will know this when their input field is in an error state which will look like this:

![memento-input-error1-overview1.png](https://jsmanifest.s3.us-west-1.amazonaws.com/posts/the-power-of-memento-design-pattern-in-javascript/memento-input-error1-overview1.png 'Memento Input Error 1 Overview 1 Png')

This is the html markup we are going to work right on top of:

```html
<!DOCTYPE html>
<html>
  <head>
    <title>Memento</title>
    <meta charset="UTF-8" />
  </head>
  <body style="margin:50px;text-align:center;background:linear-gradient(
    76.3deg,
    rgba(44, 62, 78, 1) 12.6%,
    rgba(69, 103, 131, 1) 82.8%
  );height:250px;overflow:hidden;">
    <input type="email" id="emailInput" style="padding:12px;border-radius:4px;font-size:16px;" placeholder="Enter your email"></input>
    <script src="src/index.js"></script>
  </body>
</html>
```

This will start us off with this interface:

![memento-state-pattern-overview2.png](https://jsmanifest.s3.us-west-1.amazonaws.com/posts/the-power-of-memento-design-pattern-in-javascript/memento-state-pattern-overview2.png 'Memento State Pattern Overview 2 Png')

Now the first thing we are going to do is define a couple of constant variables for the _error_ state that we will use throughout our code to assign as values to the error styles. This is to ensure that we don't make any typos when writing our code since we will be reusing them multiple times:

```js
const ERROR_COLOR = 'tomato'
const ERROR_BORDER_COLOR = 'red'
const ERROR_SHADOW = `0px 0px 25px rgba(230, 0, 0, 0.35)`
const CIRCLE_BORDER = '50%'
const ROUNDED_BORDER = '4px'
```

This has nothing to do with the pattern but I think it's a good habit for me to randomly slip in some best practices just so you can get extra tips from this post, why not right? ;)

Now we are going to create a helper function that toggles between the _error state_ and the _normal state_ since we are going to be using this multiple times as well:

```js
const toggleElementStatus = (el, status) => {
  if (status === 'error') {
    return Object.assign(el.style, {
      borderColor: ERROR_BORDER_COLOR,
      color: ERROR_COLOR,
      boxShadow: ERROR_SHADOW,
      outline: 'red',
    })
  }
  return Object.assign(el.style, {
    borderColor: 'black',
    color: 'black',
    boxShadow: '',
    outline: '',
  })
}
```

I might as well just slip in a helper to toggle the border radius while we toggle between the two style presets. This is to make our code feel more "natural" as if it was a real app so we don't just focus directly on the relationship between the colors and the memento in this post. Sometimes I think we learn better when we also see the perspective of random code vs the actual code that we are going over with:

```js
const toggleBorderRadius = (el, preset) => {
  el.style.borderRadius =
    preset === 'rounded'
      ? ROUNDED_BORDER
      : preset === 'circle'
      ? CIRCLE_BORDER
      : '0px'
}
```

The next thing we are going to do is write the _Originator_.

Remember, the originator defines the interface that triggers the creation and storing of itself as the memento.

```js
function createOriginator({ serialize, deserialize }) {
  return {
    serialize,
    deserialize,
  }
}
```

Actually, we just created a simply factory that produces the originator for us.

Here is the real originator:

```js
const originator = createOriginator({
  serialize(...nodes) {
    const state = []

    nodes.forEach(
      /**
       * @param { HTMLInputElement } node
       */
      (node) => {
        const item = {
          id: node.id || '',
        }

        item.tagName = node.tagName.toLowerCase()

        if (item.tagName === 'input') {
          item.isError =
            node.style.borderColor === ERROR_BORDER_COLOR &&
            node.style.color === ERROR_COLOR
          item.value = node.value
        }

        item.isRounded = node.style.borderRadius === ROUNDED_BORDER
        item.isCircle = node.style.borderRadius === CIRCLE_BORDER

        state.push(item)
      },
    )

    return state
  },
  deserialize(...state) {
    const providedNode = state[state.length - 1]

    if (providedNode) state.pop()

    const nodes = []

    state.forEach((item) => {
      const node = providedNode || document.createElement(item.tagName)

      if (item.tagName === 'input') {
        if (item.isError) {
          toggleElementStatus(node, 'error')
        }
        if (item.isRounded) {
          toggleBorderRadius(node, 'rounded')
        } else if (item.isCircle) {
          toggleBorderRadius(node, 'circle')
        }
        node.value = item.value || ''
        if (item.placeholder) node.placeholder = item.placeholder
        if (item.id) node.id = item.id
      }

      nodes.push(node)
    })

    return nodes
  },
})
```

In the originator, the `serialize` method takes in a DOM node and returns us a state representation of the DOM node so that we can store it inside the local storage as a string. This is required because the local storage only accepts strings.

Right now we are the peak of this pattern in JavaScript. The serialization is the only reason why this pattern is important to us otherwise we'd be able to directly store DOM nodes to the local storage and call it a day.

Inside our `serialize` method we implicitly defined a couple of rules that help us determine the representation.

Here are the lines I'm referring to:

```js
if (item.tagName === 'input') {
  item.isError =
    node.style.borderColor === ERROR_BORDER_COLOR &&
    node.style.color === ERROR_COLOR
  item.value = node.value
}

item.isRounded = node.style.borderRadius === ROUNDED_BORDER
item.isCircle = node.style.borderRadius === CIRCLE_BORDER
```

When storing mementos of input elements we have a choice whether to implement it that way or this way:

```js
if (item.tagName === 'input') {
  item.style.borderColor = node.style.borderColor
  item.style.color = node.style.color
  item.value = node.value
}

item.style.borderRadius = node.style.borderRadius
```

Take my advice on this: A good practice is to create useful meaning out of your code _especially in your design pattern implementations_. When you inaugurate meaning in your code it it helps you to think of higher level abstractions that might be useful in other areas of your code.

Using `item.isError` to represent a preset of error styles opens up wider opportunities to make interesting reusable mementos that we can reuse as our project grows more complex over time as opposed to assigning arbitrary styles directly.

For example, it's common for forms to not submit when a crucial field is left unblank. The form must transition to some kind of state where it needs to stop itself from submitting.

If we were to save a memento of a form we need to ensure that when we restore this state the user is restored to the "disabled" state:

```js
const originator = createOriginator({
  serialize(...nodes) {
    const state = []

    nodes.forEach(
      /**
       * @param { HTMLInputElement } node
       */
      (node) => {
        const item = {
          id: node.id || '',
        }

        item.tagName = node.tagName.toLowerCase()

        if (item.tagName === 'input') {
          item.isError =
            node.style.borderColor === ERROR_BORDER_COLOR &&
            node.style.color === ERROR_COLOR
          item.value = node.value
        }

        item.isRounded = node.style.borderRadius === ROUNDED_BORDER
        item.isCircle = node.style.borderRadius === CIRCLE_BORDER

        if (node.textContent) item.textContent = node.textContent

        state.push(item)
      },
    )

    return state
  },
  deserialize(state) {
    const nodes = []

    if (!Array.isArray(state)) state = [state]

    state.forEach((item) => {
      const node = document.createElement(item.tagName)

      if (item.style) {
        Object.entries(item.style).forEach(([key, value]) => {
          node.style[key] = value
        })
      }

      if (item.isRounded) {
        toggleBorderRadius(node, 'rounded')
      } else if (item.isCircle) {
        toggleBorderRadius(node, 'circle')
      }

      if (item.spacing) {
        node.style.padding = item.spacing
      }

      if (item.id) node.id = item.id

      if (item.tagName === 'input') {
        if (item.isError) {
          toggleElementStatus(node, 'error')
        }
        node.value = item.value || ''
        if (item.placeholder) node.placeholder = item.placeholder
      } else if (item.tagName === 'label') {
        if (item.isError) {
          node.style.color = ERROR_COLOR
        }
      } else if (item.tagName === 'select') {
        if (item.options) {
          item.options.forEach((obj) => {
            node.appendChild(...originator.deserialize(obj, node))
          })
        }
      }

      if (item.textContent) node.textContent = item.textContent

      nodes.push(node)
    })

    return nodes
  },
})

const caretaker = createCaretaker()

function restore(state, container, { onRendered } = {}) {
  let statusSubscribers = []
  let status = ''

  const setStatus = (value, options) => {
    status = value
    statusSubscribers.forEach((fn) => fn(status, options))
  }

  const renderMemento = (memento, container) => {
    return originator.deserialize(memento).map((el) => {
      container.appendChild(el)

      if (memento.isError && status !== 'error') {
        setStatus('error')
      }

      if (memento.children) {
        memento.children.forEach((mem) => {
          renderMemento(mem, el).forEach((childEl) => el.appendChild(childEl))
        })
      }

      return el
    })
  }

  const render = (props, container) => {
    const withStatusObserver = (fn) => {
      statusSubscribers.push((updatedStatus) => {
        if (updatedStatus === 'error') {
          // Do something
        }
      })

      return (...args) => {
        const elements = fn(...args)
        return elements
      }
    }

    const renderWithObserver = withStatusObserver(renderMemento)

    const elements = renderWithObserver(props, container)
    statusSubscribers.length = 0
    return elements
  }

  const elements = render(state, container)

  if (onRendered) onRendered(status, elements)

  return {
    status,
    elements,
  }
}

const container = document.getElementById('root')

const { status, elements: renderedElements } = restore(mementoJson, container, {
  onRendered: (status, elements) => {
    if (status === 'error') {
      const submitBtn = container.querySelector('#submit-btn')
      submitBtn.disabled = true
      submitBtn.textContent = 'You have errors'
      toggleElementStatus(submitBtn, 'error')
    }
  },
})
```

Instead of returning the elements directly we make sure that what's also returned is the _current state of rendering the memento_.

Looking at this in a higher level perspective we take advantage of the fact that `isError` can represent and overview of something like a form. A form should not be submitted if either one little required field is missing or a value was not entered correctly.

In that case we make sure that the form _should not be interactive_ by disabling the _submit button_ right before displaying to the user:

![memento-pattern-with-error-state-disabled- button.png](<https://jsmanifest.s3.us-west-1.amazonaws.com/posts/the-power-of-memento-design-pattern-in-javascript/memento-pattern-with-error-state-disabled- button.png> 'Memento Pattern With Error State Disabled Button Png')

If you haven't noticed, our `restore` wraps our original `deserialize` method from our _Originator_.

What we have now is a higher level abstracted memento that supports deep children _and_ the _rendering state_ (`isError`) of our entire memento.

## Conclusion

And that concludes the end of this post! I hope you found this to be valuable and look out for more in the future!

Find me on [medium](https://medium.com/@jsmanifest)