fluente
TypeScript icon, indicating that this package has built-in type declarations

4.0.0 • Public • Published

fluente

npm version JavaScript Style Guide Dependencies Status Actions Status Coverage Status

Make fluent objects like a boss!

Features

  • Zero dependencies: small footprint.
  • Protected state
  • Selective mutability: choose between immutable objects or a classy object.
  • Undo/Redo out of the box
  • TypeScript support

Installation

npm install --save fluente

fluente(options)

Returns the build object.

  • options <Object>
    • state <Object> Initial state.
    • [constants] <Object> Constant values.
    • [getters] <Object> Value getters.
    • [fluents] <Object> Fluent methods.
    • [mappers] <Object> Map (normal) methods.
    • [mutable] <Boolean> See mutability.
    • [historySize] <Number> See undo and redo.
    • [produce] <Function> See state manipulation.
  • Returns: <Object>

Quickstart

The easiest way to create a fluent API is by returning the instance itself inside a class method.

class Calculator {
  constructor (value = 0) {
    this.value = value
  }

  add (value) {
    this.value += value
    return this // Fluent API
  }

  valueOf () {
    return this.value
  }
}

const calculator = new Calculator(0)

const value = calculator
  .add(2)
  .add(4)
  .add(8)
  .add(16)
  .valueOf()

console.log(value) // 30

Fluente provides a different way to declare fluent objects.

const fluente = require('fluente')

const symCalculator = Symbol('my_calculator')

function valueOf (state) {
  return state.value
}

function add (state, value) {
  return {
    value: valueOf(state) + value
  }
}

function subtract (state, value) {
  return {
    value: valueOf(state) - value
  }
}

function multiply (state, value) {
  return {
    value: valueOf(state) * value
  }
}

function divide (state, value) {
  return {
    value: valueOf(state) / value
  }
}

function createCalculator (value = 0) {
  return fluente({
    // Enable state history (undo/redo)
    historySize: 8,
    // Initial state
    state: {
      value
    },
    // Object constants
    constants: {
      [symCalculator]: true
    },
    // Value getters
    getters: {
      value: valueOf,
      integer: state => Number.isInteger(valueOf(state))
    },
    // Fluent methods
    fluents: {
      add,
      subtract,
      multiply,
      divide
    },
    // Map (normal) methods
    mappers: {
      valueOf
    }
  })
}

const zero = createCalculator(0)

// Read constant value
if (zero[symCalculator] !== true) {
  throw new Error('Expected calculator')
}

// Read value getters
console.log(zero.value, zero.integer) // 0 true

// Use fluent method
const pi = zero.add(Math.PI)

console.log(pi.value, pi.integer) // 3.141592653589793 false

const value = zero
  .add(2)
  .subtract(8)
  .multiply(-1)
  .divide(2)
  .undo(2) // Undo 2 mutations: divide(2) and multiply(-1)
  .redo(1) // Redo 1 mutation: multiply(-1)
  .valueOf() // Use map method

console.log(value) // 6

Like a constructor, Fluente returns an object. You can still declare methods, constants, and getters, but with a different signature. Functions do not use the this keyword to access the state. Instead, It's always passed as the first argument. Another key difference is how fluent methods mutate the state: by returning an object that contains the changed values.

These rules abstract the methods (and getters) definition and the state mutation, unlocking some cool features like selective mutability, state protection, and undo/redo.

Mutability

By default, all objects build by Fluente are immutable (all fluent methods will return a new object containing the updated state). You can retrieve mutable objects using the mutable option.

const fluente = require('fluente')

function createCalculator (initialValue = 0) {
  return fluente({
    mutable: true, // Request mutable object
    state: {
      value: initialValue
    },
    fluents: {
      add (state, value) {
        return {
          value: state.value + value
        }
      }
    },
    mappers: {
      valueOf (state) {
        return state.value
      }
    }
  })
}

const calculator = createCalculator(0)
calculator.add(40)
calculator.add(2)
console.log(calculator.valueOf()) // 42

Undo and Redo

An isolated state enables easy undo-redo implementation. The current state represents the present moment. Applying a mutation move the present into the past, and set a new present state. Undoing a mutation will restore a state from the past, and move the present state into the future. Redoing a mutation will do the opposite. A more detailed example is available in this Redux article.

Fluente automatically injects undo-redo functions. Both functions optionally accept the number of mutations to apply, defaulting to 1 mutation. Infinity is accepted, and means "redo/undo all the mutations available".

The historySize option controls the max number of mutations remembered by Fluente. Set the historySize option to a positive number to enable this feature.

State manipulation

By default, all fluent functions need to return an updated state, even partially, to let Fluente know what is changed. Plus state needs to be treated as immutable. Those rules are necessary to ensure correct state isolation. The subtract function in the next example is updating the state's value property by returning an object containing that property.

function subtract (state, value) {
  return {
    value: state.value - value
  }
}

However, other state manipulation systems are viable through a custom Producer. A Producer is a function that accepts both state object and map function. Its purpose is to handle any map mutations applied against the state and then return a new and fully updated state.

const fluente = require('fluente')

function mySimpleProducer (oldState, mapper) {
  // Clone current state
  const newState = Object.assign({}, oldState)
  // Run state mapper (may override some root properties now)
  mapper(newState)
  // Return updated state
  return newState
}

function createCalculator (initialValue = 0) {
  return fluente({
    produce: mySimpleProducer, // Use a custom producer
    state: {
      value: initialValue
    },
    fluents: {
      add (state, value) {
        state.value += value // Direct state manipulation
      }
    },
    mappers: {
      valueOf (state) {
        return state.value
      }
    }
  })
}

The easiest and safest way to support direct state manipulation is to use Immer's produce function.

const fluente = require('fluente')
const { produce } = require('immer')

function createCalculator (initialValue = 0) {
  return fluente({
    produce, // Use Immer to handle state changes
    state: {
      value: initialValue
    },
    fluents: {
      add (state, value) {
        state.value += value // Direct state manipulation
      }
    },
    mappers: {
      valueOf (state) {
        return state.value
      }
    },
  })
}

Immutable.js is also supported.

const fluente = require('fluente')
const { Map } = require('immutable')

function valueOf (state) {
  return state.get('value')
}

function add (state, value) {
  return state.set('value', valueOf(state) + value)
}

function createCalculator (initialValue = 0) {
  return fluente({
    // Direct mapping (fluent mappers need to return the updated state)
    produce: (state, mapper) => mapper(state),
    // Initial state (init with Immutable.js)
    state: Map({
      value: initialValue
    }),
    fluents: {
      add
    },
    mappers: {
      valueOf
    }
  })
}

Package Sidebar

Install

npm i fluente

Weekly Downloads

4

Version

4.0.0

License

MIT

Unpacked Size

16.6 kB

Total Files

5

Last publish

Collaborators

  • greguz