The Rampage

The Rampage

Draft objects in JavaScript

Draft objects in JavaScript

This was a small experiment to see how far you could get with Vanilla JS to build Draft objects. Draft objects are mutable objects that you can undo. An example of a library that handles this functionality is Immer. This was mainly driven by my curiosity of the Prototypal nature of JavaScript - usually this feature is used by framework developers. I think it's a very powerful language feature and most developers I've worked with avoid it. I suspect that this avoidance comes from their experience with classical inheritance.

The main idea is to create an empty object as the draft object that is a prototype of a source object. Since it is a prototype, it will automatically inherit all the properties of the source object. Mutations to this draft object can be viewed as the diff of the source object. We can commit these changes to create a new source object.

Creating a Draft

Creating a Draft is trivial. We can use Object.create to create an object that inherits from the source. Using a function wrapper for Object.create communicates intent, but also to provide a way to add extra functionality in the future. One could add extra calls to protect source if needed, such as Object.seal or Object.freeze or Object.preventExtensions.

const createDraft = source => Object.create(source);

...

var test = require('tape');

test('createDraft', assert => {
  var source = { name: 'Fred' }
  var draft = createDraft(source);

  assert.notEqual(source, draft, 'Should be separate objects');
  assert.Equal(source, Object.getPrototypeOf(draft), 'Should be a child');
});

Committing a Draft

Committing a Draft is equally trivial. Using Object.assign, we create a new empty object by merging all the properties of the source and the draft into the empty. This doesn't mutate the state, but allows the caller to do whatever they want with the new state.

const commitDraft = draft => Object.assign({}, 
                              Object.getPrototypeOf(draft), draft);

...

test('should commit', assert => {
  assert.plan(4);
  const source = { name: "Fred", age: 15 };
  const draft = createDraft(source);
  draft.age = 99;
  const committed = commitDraft(draft);

  assert.equal(99, committed.age);
  assert.equal(source.name, committed.name);

  assert.notEqual(source, committed);
  assert.notEqual(source, Object.getPrototypeOf(committed));
});

Revert a Draft

Reverting a draft is also straight forward. Since we never mutate the source, we can just create a new draft on it.

const revertDraft = draft => createDraft(Object.getPrototypeOf(draft))

Notify on Change

Here was a fun little experiment, say you wanted to be notified of changes on the Draft. A good use case for this would be some kind of validation that you want to run on the Draft object. We could have a service or manager to coordinate this, but we could also observe the draft.

Creating a Proxy we can pass in a callback as a trap to extend the default behaviour of the setter. We pass in a callback to handle any changes to the draft whenever a property is updated.

const notify = (draft, callback) => new Proxy(draft, {
  set: (obj, prop, value) => {
    obj[prop] = value;
    callback(obj, prop);
    return true;
  }
});

...

test("notify", assert => {
  assert.plan(1);
  let called = false
  const source = { name: "Fred", age: 15 };
  const proxy = notify(source, (obj, prop) => {
    called = true;
  });

  proxy.name = 'Charles';
  assert.ok(called);
});

Use Case

The main use case of these experiments was for form logic in a legacy project. This particular organization still had to use IE7 due to constraints based on their systems. Even with legacy systems, the client really wanted modern form validation. These experiments eventually made their way to production (notify didn't as Proxy is a modern feature), but the main learning experience was that you can get quite far with Vanilla JS, even ES5 for that matter.

As the browser specifications keep updating, it may be worth considering instead of grabbing the latest and greatest framework, just reach for the basics. Even as an Agile practice, build for the bare minimum and expand only when needed. Not all projects need React or Angular. Sometimes the basics is all you need.

Resources

 
Share this