
Research
Security News
The Growing Risk of Malicious Browser Extensions
Socket researchers uncover how browser extensions in trusted stores are used to hijack sessions, redirect traffic, and manipulate user behavior.
immutable-cursors
Advanced tools
0.1.8
This CommonJS module provides cursors for Facebook's ImmutableJS library. It is essentially a standalone fork of the excellent contrib/cursor module that ships with every distribution of ImmutableJS.
From their README:
Cursors allow you to hold a reference to a path in a nested immutable data structure, allowing you to pass smaller sections of a larger nested collection to portions of your application while maintaining a central point aware of changes to the entire data structure: an onChange function which is called whenever a cursor or sub-cursor calls update.
This is particularly useful when used in conjuction with component-based UI libraries like React or to simulate "state" throughout an application while maintaining a single flow of logic.
This pretty much sums it up.
This module is for most parts a contrib/cursor port to ES2015 including some minor additions and refactorings in order to provide a more extension-friendly and concise interface.
The CommonJS ES5/ES3 distribution is built with Babel.
The cursors implement the complete API of KeyedSeq and IndexedSeq respectively, so if you're familiar with ImmutableJS you should have no problems jumping right in. If not, you should probably have a glance at their guides first.
Install the package from npm:
npm install immutable-cursors
Import the module and provide some state:
import Immutable from 'immutable';
import Cursor from 'immutable-cursors';
let data = Immutable.fromJS({
name: {
first: 'Luke',
last: 'Skywalker'
},
age: 35
});
Retrieve an initial cursor using Cursor.from:
let cursor = Cursor.from(data);
cursor.getIn(['name', 'last']);
// 'Skywalker'
Retrieve nested initial state:
let cursor = Cursor.from(data, ['name']);
cursor.get('last')
// 'Skywalker'
Access the ImmutableJS value that is backing the cursor directly:
let cursor = Cursor.from(data, ['name']);
data.get('name') === cursor.deref();
// true
Cursors are immutable as well:
let modifiedAgeCursor = cursor.set('age', 45);
cursor.get('age');
// 35
modifiedAgeCursor.get('age');
// 45
Use cursors like regular ImmutableJS objects:
let firstNameOnly = cursor.get('name').take(1);
firstNameOnly.deref().toJS();
// {
// name: 'Luke'
// }
Cursors support value equality (see Immutable.is). This is especially helpful in situations where you want to compare current to new nested state or props in React components, most prominently in shouldComponentUpdate
:
let valueEqualCursor = Cursor.from(data);
cursor === valueEqualCursor;
// false
Immutable.is(cursor, valueEqualCursor);
// true
Immutable.is(valueEqualCursor, data);
// true
If a cursor references a Record object, all of the Record's properties are present on the cursor as well:
let Name = Immutable.Record({
first: 'Luke',
last: 'Skywalker'
});
let person = Immutable.Map({
name: new Name();
});
let cursor = Cursor.from(person, ['name']);
cursor.first;
// 'Luke'
Retrieve a sub-cursor:
let nameCursor = cursor.cursor(['name']);
nameCursor.get('first');
// 'Luke'
Methods get
and getIn
also return sub-cursors if they don't point to a primitive value:
let nameCursor = cursor.get('name');
nameCursor.get('last');
// 'Skywalker'
Cursors and their sub-cursors share a common root state and a change handler that gets called, whenever modifications on the cursor tree occur.
Add a change handler to the initial Cursor.from call:
let cursor = Cursor.from(data, [], (nextState, currentState) => {
let newFirstName = nextState.getIn(['name', 'first']);
let currentFirstName = currentState.getIn(['name', 'first']);
console.log(currentFirstName + ' => ' + newFirstName);
});
cursor.setIn(['name', 'first'], 'Anakin');
// 'Luke => Anakin'
You can intercept the state propagation by returning a state in your change handler to perform validation, rollbacks etc.:
let cursor = Cursor.from(data, ['name'], (nextState, currentState) => {
if (nextState.get('first') === 'Leia') {
return nextState.set('last', 'Organa');
}
});
let anakinCursor = cursor.set('first', 'Anakin');
anakinCursor.get('first');
// 'Anakin'
let leiaCursor = cursor.set('first', 'Leia');
leiaCursor.get('last');
// 'Organa'
Note that in a production environment you hardly want to modify cursors in your components directly. We do that here for the sake of simplicity.
import React from 'react';
import Immutable from 'immutable';
import Cursor from 'immutable-cursors';
let data = Immutable.fromJS({
name: {
first: 'Luke',
last: 'Skywalker'
},
age: 35
});
let app;
class Input extends React.Component {
shouldComponentUpdate(nextProps) {
// This is as easy as it gets
let shouldChange = !this.props.cursor.equals(nextProps.cursor);
console.log('\tShould ' + this.props.name + 'update?', shouldChange)
return shouldChange;
}
onChange(event) {
this.props.cursor.set(this.props.key, event.target.value);
}
render() {
return (
<div>
<label>{this.props.name}</label>
<input
type='text'
value={this.props.cursor.get(this.props.key)}
onChange={this.onChange.bind(this)}
/>
</div>
)
}
}
class Application extends React.Component {
render() {
console.log('\n Render root component.');
return (
<div>
<Input name='First name' cursor={this.props.cursor('name')} key='first' />
<Input name='Last name' cursor={this.props.cursor('name')} key='last' />
<Input name='Age' cursor={this.props.cursor} key='age' />
</div>
);
}
}
function changeHandler(nextState) {
app.setProps(Cursor.from(nextState, changeHandler));
}
app = React.render(
<Application cursor={Cursor.from(nextState, changeHandler)} />,
document.body
);
Get the source:
git clone https://github.com/lukasbuenger/immutable-cursors
Install dependencies:
npm install
Lint the code:
npm run lint
Run the tests:
npm test
Build ES5/ES3:
npm run build
Build the docs / README:
npm run docs
Update all local dependencies:
npm run update-dependencies
The main entry point for both client and internals. If you create a new cursor using an API object, a reference of it will get passed to every (sub-)cursor you create from the initial one.
It consists mainly of refactored versions of the non-prototypical private functions found in ImmutableJS's contrib/cursor.
I made them sit in a class construct for extendibility and testability reasons. That way, one can easily roll his/her custom logic by subclassing from API.
The downsides of this approach are that every cursor has to carry another few more references and that more importantly the internal API is not really private anymore.
let cursor = Cursor.from(state, ['data']);
messWithInternals(cursor._api);
API objects, in contrary to contrib/cursor, convert key path arrays to Seq objects internally. This is perfectly fine with the ImmutableJS way of working on nested values and gives a couple of handy methods to work on key paths on top of another level of security because key paths are immutable as well.
Extending The following example shows how you could establish access of nested cursors by using dot-string formatted key paths as well.
import API from 'immutable-cursors/lib/API';
class CustomAPI extends API {
path(...paths) {
if (paths.length === 1 && typeof paths[0] === 'string') {
paths = paths[0].split('.');
}
return super.path(...paths);
}
}
let api = new CustomAPI();
export default api.export();
Returns a new default cursor.
cursorFrom(
rootData: Immutable.Iterable,
keyPath?: Immutable.Seq|Array<string>,
onChange?: Function
): KeyedCursor|IndexedCursor
rootData
- The state.keyPath
- An optional key path to a substate.onChange
- An optional change handler.Decides on and returns a cursor class by analyzing value
. Returns IndexedCursor if Iterable.isIndexed(value) === true
, else KeyedCursor.
getCursorClass(
value: Immutable.Iterable
): Function
value
- Any value in your state.value
.This is the main cursor factory. You probably should not subclass this method as it gives you all the options you need through its arguments. Instead of subclassing it, you should write your own method and call makeCursor from there with your custom values.
Enforce a custom cursor class
class CustomAPI extends API {
getCustomCursor(rootData, keyPath, onChange) {
return this.makeCursor(rootData, keyPath, onChange, undefined, MyCustomCursorClass);
}
export() {
let api = super.export();
api.getCustom = this.getCustomCursor.bind(this);
return api;
}
}
let api = new CustomAPI();
export default api.export();
Equip cursors with shared options Some of the cursor properties like the change handler or the root data will get shared between all cursors that are derived from the same initial cursor, may it be through updating or retrieving a cursor to a nested state etc. In certain situations it might be helpful to have custom shared values in place.
class CustomAPI extends API {
getWithSharedName(rootData, keyPath, onChange, name) {
return this.makeCursor(rootData, keyPath, onChange, undefined, undefined, {
name: name
});
}
export() {
let api = super.export();
api.getWithSharedName = this.getWithSharedName.bind(this);
return api;
}
}
let api = new CustomAPI();
let cursor = api.getWithSharedName(Immutable.fromJS({foo: 'bar'}), [], undefined, 'fooCursor');
cursor._sharedOptions.name;
// 'fooCursor'
cursor.set('foo', 'baz')._sharedOptions.name;
// 'fooCursor'
cursor.cursor('foo')._sharedOptions.name;
// 'fooCursor'
makeCursor(
rootData: Immutable.Iterable,
keyPath?: Immutable.Seq,
onChange?: Function,
value?: Immutable.Iterable|any,
CursorClass?: Function,
sharedOptions?: Object
): KeyedCursor|IndexedCursor
rootData
- An ImmutableJS state.keyPath
- A key path to a nested value.onChange
- A change handler.value
- A value to determine the size and the CursorClass
if not present. Default: rootData.getIn(keyPath)
.CursorClass
- Enforce a custom class to create the cursor with.sharedOptions
- Pass additional shared options.Updates the current state with changeFn
and calls the cursors change handler. Returns a new cursor backed by either the return value of the change handler or the result of changeFn
.
updateCursor(
cursor: KeyedCursor|IndexedCursor,
changeFn: Function,
changeKeyPath?: Immutable.Seq
): KeyedCursor|IndexedCursor
cursor
- The cursor to update.changeFn
- A function that performs and returns modifications on the given state.changeKeyPath
- If present, indicates a deep change.Constant for attempts on nested undefined values.
An empty object.
Returns a sub-cursor if the given value is an Iterable. If not, returns the value itself.
wrappedValue(
cursor: KeyedCursor|IndexedCursor,
keyPath: Immutable.Seq,
value: Immutable.isIterable|any
): KeyedCursor|IndexedCursor|any
cursor
- A cursor from which you want to retrieve a sub-cursor in case of >Iterable.isIterable(value)
.keyPath
- The key path where the value resides.value
- The value to analyze.A sub-cursor or the value.
Creates and returns a sub-cursor of cursor
at keyPath
.
subCursor(
cursor: KeyedCursor|IndexedCursor,
keyPath: Immutable.Seq,
value: any
)
cursor
- The cursor you want to create a sub-cursor from.keyPath
- The key path to the state your sub-cursor should point at.value
- The value at keyPath
.A new sub-cursor
Extends a cursor with Record properties to export the same interface as its backing value. Calls setProp for each property key.
defineRecordProperties(
cursor: KeyedCursor|IndexedCursor,
value: Immutable.Record
): KeyedCursor|IndexedCursor
cursor
- The cursor you want to extend.value
- The Record object whose keys should get mapped on cursor
.An extended cursor.
Defines an alias property on a cursor that delegates to cursor.get(name)
.
setProp(
cursor: KeyedCursor|IndexedCursor,
name: string
)
cursor
- The cursor on which you want to have a getter property with name name
.name
- The name of the propertyNormalizes and concatenates any passed key paths and returns a single Seq object.
See pathToSeq
path(
...paths: Array<Immutable.Seq|Array<string>|string>
): Immutable.Seq
A concatenated, validated key path as Seq
Should return an object containing all (bound) functions and properties that you consider public. Recommended use:
// in ./cursor/CustomAPI.js
import API from 'immutable-cursors/lib/API';
export default class CustomAPI extends API {
export() {
let api = super.export();
api.version = '0.1';
return api;
}
}
// in ./cursor/index.js
import CustomAPI from './CustomAPI';
let api = new CustomAPI();
export default api.export();
// in ./client.js
import Cursor from './cursor';
console.log(Cursor.version);
let cursor = Cursor.from(state);
export(): Object
The client API
The BaseCursor mixin contains methods that represent shared behavior of both KeyedCursor and IndexedCursor. The reason why these live in a mixin is, that prototypical inheritance on the cursor classes is already occupied by the ImmutableJS base classes KeyedSeq and IndexedSeq respectively.
Most of the methods in this mixin override these original Immutable.Seq interface, where cursor implementation has to decorate / circumvent default ImmutableJS behavior.
If your are interested in how these overrides work, check out the source. This document only lists the methods that are not part of the original ImmutableJS.Seq interface.
Returns the ImmutableJS object that is backing the cursor.
deref(
notSetValue?: any
): Immutable.Iterable
notSetValue
- You'll get notSetValue
returned if there is no backing value for this cursor.The ImmutableJS backing state of this cursor.
Alias of deref().
If called with a key
and a value
, the substate at key
will be set to value
. If you only provide one argument, the backing state of the cursor itself is set directly to the given argument. This makes this method a little different from the one found on Iterable.
set(
key: Array|Immutable.Seq|any,
value?: any
): KeyedCursor|IndexedCursor
key
- Either a key path to the substate you want to modify or a value you want to set directly.value
- A value.The cursor representing the new state.
This is a tiny helper function that takes any class / function and extends its prototype with whatever mixins you pass.
import mixed from 'immutable-cursors/lib/extendMixed';
class MyClass extends mixed(BaseClass, Mixin1, Mixin2) {
// your class logic
}
extendMixed(
ParentClass: Function,
...mixins: Array<Object>
)
ParentClass
- The class you want to extend....mixins
- An arbitrary amount of objects whose properties you want to have on the prototype of ParentClass
.A copy of the parent class with all mixin extensions.
The public API
Returns a new cursor for the given state and key path.
from(
state: Immutable.Iterable,
keyPath?: Array<String>|Immutable.Seq,
changeHandler?: Function
): KeyedCursor|IndexedCursor
state
- The root state.keyPath
- The key path that points to the nested state you want to create a cursor for.changeHandler
- A change handler function that gets called whenever changes occur on the cursor itself or on any sub-cursor. Its return value, if !== undefined
, will replace newState
as new root state of the resulting cursors shared state. It gets called with:
newState
- The state after the update.oldState
- The state before the update.keyPath
- An Immutable.Seq key path that indicates where in the state the update occurred.A new root cursor
Extends: IndexedSeq Mixins: BaseCursor
Used to represent indexed ImmutableJS values.
new IndexedCursor(
rootData: immutable.Iterable,
keyPath: Immutable.Seq,
onChange?: Function,
size?: number,
api: API,
sharedOptions?: Object
)
rootData
- An ImmutableJS state.keyPath
- A key path to a nested value.onChange
- A change handler.size
- A value that should be set as the size of the cursor. Default: rootData.getIn(keyPath)
.api
- A reference to the API object from which the cursor was derived.sharedOptions
- Pass additional shared options.Extends: KeyedSeq Mixins: BaseCursor
Used to represent keyed ImmutableJS values.
new IndexedCursor(
rootData: immutable.Iterable,
keyPath: Immutable.Seq,
onChange?: Function,
size?: number,
api: API,
sharedOptions?: Object
)
rootData
- An ImmutableJS state.keyPath
- A key path to a nested value.onChange
- A change handler.size
- A value that should be set as the size of the cursor. Default: rootData.getIn(keyPath)
.api
- A reference to the API object from which the cursor was derived.sharedOptions
- Pass additional shared options.Normalizes and concatenates any passed key paths and returns a single Seq object.
pathToSeq(
...paths: Array<Immutable.Seq|Array<string>|string>
): Immutable.Seq
...paths
- Any values that you want to merge to a Seq pathAn Seq path
It's complicated. See LICENSE file.
FAQs
Provides cursors for Facebook's ImmutableJS library.
The npm package immutable-cursors receives a total of 939 weekly downloads. As such, immutable-cursors popularity was classified as not popular.
We found that immutable-cursors demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket researchers uncover how browser extensions in trusted stores are used to hijack sessions, redirect traffic, and manipulate user behavior.
Research
Security News
An in-depth analysis of credential stealers, crypto drainers, cryptojackers, and clipboard hijackers abusing open source package registries to compromise Web3 development environments.
Security News
pnpm 10.12.1 introduces a global virtual store for faster installs and new options for managing dependencies with version catalogs.