Never Patronize Mothers

    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
        }
      })
    }

    Install

    npm i fluente

    DownloadsWeekly Downloads

    22

    Version

    4.0.0

    License

    MIT

    Unpacked Size

    16.6 kB

    Total Files

    5

    Last publish

    Collaborators

    • greguz