1.2.0 • Published 6 years ago

partial.lenses.history v1.2.0

Weekly downloads
3
License
MIT
Repository
github
Last release
6 years ago

Partial Lenses History · Gitter GitHub stars npm

Partial Lenses History is a JavaScript library for state manipulation with Undo-Redo history. Basic features:

npm version Build Status Code Coverage npm.io npm.io

Contents

A basic example

This section describes the Basic Undo-Redo CodeSandbox example that was written to demonstrate usage of this library. There is a text area and edits retain history that can then be viewed through the undo and redo buttons. You probably want to open the example beside this tutorial.

Looking at the code, the first thing you might notice is the import statement:

import * as H from 'kefir.partial.lenses.history'

When used in a Karet UI, this library is intended to be used through the Kefir Partial Lenses History library, which is a simple lifted wrapper around this library. Lifting allows the functions of this library to be directly used on Kefir properties and atoms representing time-varying values and reactive variables. However, this library does not depend on Karet or Kefir and can be used with pretty much any UI framework.

To use history, one must first use H.init to create the initial history value and then store the value:

const history = U.atom(H.init({}, ''))

In this case we use U.atom to create an atom to store the history.

In a plain React UI, for example, one would typically store the history in component state:

this.state = {history: H.init({}, '')}

To access the present value from history, one uses the H.present lens:

const text = U.view(H.present, history)

As we are using atoms, we can use the U.view function to create a bidirectional view of the present that we can then use to both read and write the present value.

In a plain React UI, one could use L.get to read the present value from component state:

const currentText = L.get(['history', H.present], this.state)

and L.set to write to the present value in component state:

this.setState(L.set(['history', H.present], newText))

The point here is that this library is not at all limited to Karet UIs. In the remainder we will only discuss the actual example.

Now that we have the text view, we can use it to access the text without knowing anything about the history. So we can simply instantiate a U.TextArea with the text as the value:

<U.TextArea placeholder="Retains history" value={text} />

Now edits through the text area generate history. Note that, while in this case we only store simple strings in history, values stored in history can be arbitrarily complex trees of objects.

Of course, to actually make use of the history, we need to provide access to the history itself, rather than just the present value. To that end we implement a countdown button component:

const CountdownButton = ({count, shortcut, children, ...props}) => (
  <button disabled={R.not(count)} onClick={U.doModify(count, R.dec)} {...props}>
    {children}
    {U.when(count, U.string` (${count})`)}
    {U.when(
      shortcut,
      U.thru(
        U.fromEvents(document.body, 'keydown', false),
        U.skipUnless(shortcut),
        U.consume(U.actions(U.preventDefault, U.doModify(count, R.dec)))
      )
    )}
  </button>
)

The above CountdownButton component expects to receive a count atom containing a non-negative integer and it then renders a button that is enabled when the count is positive. Clicking the button decrements the count. Additionally, given a shortcut event predicate, it also binds a keyboard event handler to the document that performs the same decrement action. Note that the above CountdownButton knows nothing about history. It is just a generic button that decrements a counter.

To wire countdown buttons to perform undo and redo actions on history, we use the H.undoIndex and H.redoIndex lenses to view the history. Here is how it looks like for the undo button:

<CountdownButton
  count={U.view(H.undoIndex, history)}
  title="Ctrl-z"
  shortcut={e => e.ctrlKey && e.key === 'z'}>
  Undo
</CountdownButton>

Modifying the undo index actually modifies the history. That pretty much covers basic usage of this library.

Reference

The combinators provided by this library are available as named imports. Typically one just imports the library as:

import * as H from 'partial.lenses.history'

The examples also use the Partial Lenses library imported as

import * as L from 'partial.lenses'

and the following helper function, thru, that pipes a value through the given sequence of functions:

function thru(x, ...fns) {
  return fns.reduce((x, fn) => fn(x), x)
}

Basic properties

On serializability

The history data type should be considered opaque. However, the history data structure itself only uses JSON compatible types. Assuming that JSON.parse(JSON.stringify(v)) is considered equivalent to v for any value v put into history, then it is guaranteed that JSON.parse(JSON.stringify(history)) is considered equivalent to history.

On performance

The internal implementation of history uses a simple but fairly efficient data structure (currently a radix search trie) that can perform all the operations exposed by this library in either O(1) or O(log n) time.

On immutability

Since version 1.1.0 the history data structure is kept frozen when NODE_ENV is not production. Only the history data structure itself is frozen. Values inserted into history are not frozen by this library.

On side-effects

Certain operations, namely H.init and L.set(H.present) in this library are not pure functions, because they take timestamps underneath.

Creating

H.init({[maxCount, pushEquals, replacePeriod]}, value) ~> history v0.1.0

H.init creates a new history state object with the given initial value. The named parameters, maxCount, replacePeriod, and pushEquals, are optional and control how history is updated when the state is modified through H.present.

  • maxCount defaults to 2^31-1 and specifies the maximum number of entries to keep in history.
  • pushEquals defaults to false and determines whether writing a value that is equal to the present value updates history or not.
  • replacePeriod defaults to 0 and specifies a period in milliseconds during which an update replaces the present value without adding history.

For example:

thru(
  H.init({}, 101),
  L.get(H.present)
)
// 101

Note that H.init is not a pure function, because it takes a timestamp underneath.

Present

H.present ~> valueLens v0.2.0

H.present is a lens that focuses on the present value of history.

For example:

thru(
  H.init({}, 42),
  L.modify(H.present, x => -x),
  L.get(H.present)
)
// -42

Note that modifications through H.present are not referentially transparent operations, because setting through H.present takes a timestamp underneath.

Undo

H.undoForget(history) ~> history v0.1.0

H.undoForget removes all entries prior to present from history.

For example:

thru(
  H.init({}, '1st'),
  L.set(H.present, '2nd'),
  L.set(H.present, '3rd'),
  H.undoForget,
  L.get(H.undoIndex)
)
// 0

H.undoIndex ~> numberLens v0.2.0

H.undoIndex is a lens that focuses on the undo position of history.

For example:

thru(
  H.init({}, '1st'),
  L.set(H.present, '2nd'),
  L.set(H.present, '3rd'),
  L.modify(H.undoIndex, n => n-1),
  L.get(H.present)
)
// '2nd'

Redo

H.redoForget(history) ~> history v0.1.0

H.redoForget removes all entries following present from history.

For example:

thru(
  H.init({}, '1st'),
  L.set(H.present, '2nd'),
  L.set(H.present, '3rd'),
  L.set(H.index, 0),
  H.redoForget,
  L.get(H.redoIndex)
)
// 0

H.redoIndex ~> numberLens v0.2.0

H.redoIndex is a lens that focuses on the redo position of history.

For example:

thru(
  H.init({}, '1st'),
  L.set(H.present, '2nd'),
  L.set(H.present, '3rd'),
  L.set(H.index, 0),
  L.modify(H.redoIndex, n => n-1),
  L.get(H.present)
)
// '2nd'

Time travel

H.count(history) ~> number v0.1.0

H.count returns the number of entries in history. See also H.indexMax.

For example:

thru(
  H.init({}, '1st'),
  L.set(H.present, '2nd'),
  L.set(H.present, '3rd'),
  H.count
)
// 3

H.index ~> numberLens v0.2.0

H.index is a lens that focuses on the index of present of history.

For example:

thru(
  H.init({}, '1st'),
  L.set(H.present, '2nd'),
  L.set(H.present, '3rd'),
  L.set(H.index, 1),
  L.get(H.present)
)
// '2nd'

H.indexMax(history) ~> number v0.2.3

H.indexMax returns the maximum history index. See also H.count.

For example:

thru(
  H.init({}, '1st'),
  L.set(H.present, '2nd'),
  L.set(H.present, '3rd'),
  H.indexMax
)
// 2
1.2.0

6 years ago

1.1.0

6 years ago

1.0.1

6 years ago

1.0.0

6 years ago

0.2.3

6 years ago

0.2.2

6 years ago

0.2.1

6 years ago

0.2.0

6 years ago

0.1.2

6 years ago

0.1.1

6 years ago

0.1.0

7 years ago