GithubHelp home page GithubHelp logo

omniscientjs / immstruct Goto Github PK

View Code? Open in Web Editor NEW
374.0 15.0 21.0 270 KB

Immutable data structures with history for top-to-bottom properties in component based libraries like React. Based on Immutable.js

JavaScript 100.00%

immstruct's Introduction

Immstruct NPM version Build Status Dependency Status Gitter

A wrapper for Immutable.js to easily create cursors that notify when they are updated. Handy for use with immutable pure components for views, like with Omniscient or React.js.

See the API References for more documentation and usage.

Usage

someFile.js

// Require a default instance of immstruct (singleton instance)
var immstruct = require('immstruct');

// Create structure under the later retrievable ID `myKey`
var structure = immstruct('myKey', { a: { b: { c: 1 } } });

// Use event `swap` or `next-animation-frame`
structure.on('swap', function (newStructure, oldStructure, keyPath) {
  console.log('Subpart of structure swapped.');
  console.log('New structure:', newStructure.toJSON());

  // e.g. with usage with React
  // React.render(App({ cursor: structure.cursor() }), document.body);
});

var cursor = structure.cursor(['a', 'b', 'c']);

// Update the value at the cursor. As cursors are immutable,
// this returns a new cursor that points to the new data
var newCursor = cursor.update(function (x) {
  return x + 1;
});

// We unwrap the cursor, by getting the data it is pointing at using deref
// and see that the value of the old `cursor` to is still `1`
console.log(cursor.deref()); //=> 1

// `newCursor` points to the new data
console.log(newCursor.deref()); //=> 2

Note: The cursors you see here are cursors from Facebooks Immutable.js library. Read the complete API in their repo.

anotherFile.js

// Require a default instance of immstruct (singleton instance)
var immstruct = require('immstruct');

// Get the structure we previously defined under the ID `myKey`
var structure = immstruct('myKey');

var cursor = structure.cursor(['a', 'b', 'c']);

var updatedCursor = cursor.update(function (x) { // triggers `swap` in somefile.js
  return x + 1;
});

// Unwrap the value
console.log(updatedCursor.deref()); //=> 3

References

While Immutable.js cursors are immutable, Immstruct lets you create references to a piece of data from where cursors will always be fresh. The cursors you create are still immutable, but you have the ability to retrieve the newest and latest cursor pointing to a specific part of your immutable structure.

var structure = immstruct({ 'foo': 'bar' });
var ref = structure.reference('foo');

console.log(ref.cursor().deref()) //=> 'bar'

var oldCursor = structure.cursor('foo');
console.log(oldCursor.deref()) //=> 'bar'

var newCursor = structure.cursor('foo').update(function () { return 'updated'; });
console.log(newCursor.deref()) //=> 'updated'

assert(oldCursor !== newCursor);

// You don't need to manage and track fresh/stale cursors.
// A reference cursor will do it for you.
console.log(ref.cursor().deref()) //=> 'updated'

Updating a cursor created from a reference will also update the underlying structure.

This offers benefits similar to that of Om's reference cursors, where React.js or Omniscient components can observe pieces of application state without it being passed as cursors in props from their parent components.

References also allow for listeners that fire when their path or the path of sub-cursors change:

var structure = immstruct({
  someBox: { message: 'Hello World!' }
});
var ref = structure.reference(['someBox']);

var unobserve = ref.observe(function () {
  // Called when data the path 'someBox' is changed.
  // Also called when the data at ['someBox', 'message'] is changed.
});

// Update the data using the ref
ref.cursor().update(function () { return 'updated'; });

// Update the data using the initial structure
structure.cursor(['someBox', 'message']).update(function () { return 'updated again'; });

// Remove the listener
unobserve();

Notes

Parents' change listeners are also called when sub-cursors are changed.

Cursors created from references are still immutable. If you keep a cursor from a var cursor = reference.cursor() around, the cursor will still point to the data at time of cursor creation. Updating it may rewrite newer information.

Usage Undo/Redo

// optionalKey and/or optionalLimit can be omitted from the call
var optionalLimit = 10; // only keep last 10 of history, default Infinity
var structure = immstruct.withHistory('optionalKey', optionalLimit, { 'foo': 'bar' });
console.log(structure.cursor('foo').deref()); //=> 'bar'

structure.cursor('foo').update(function () { return 'hello'; });
console.log(structure.cursor('foo').deref()); //=> 'hello'

structure.undo();
console.log(structure.cursor('foo').deref()); //=> 'bar'

structure.redo();
console.log(structure.cursor('foo').deref()); //=> 'hello'

Examples

Creates or retrieves structures.

See examples:

var structure = immstruct('someKey', { some: 'jsObject' })
// Creates new structure with someKey
var structure = immstruct('someKey')
// Get's the structure named `someKey`.

Note: if someKey doesn't exist, an empty structure is created

var structure = immstruct({ some: 'jsObject' })
var randomGeneratedKey = structure.key;
// Creates a new structure with random key
// Used if key is not necessary
var structure = immstruct()
var randomGeneratedKey = structure.key;
// Create new empty structure with random key

You can also create your own instance of Immstruct, isolating the different instances of structures:

var localImmstruct = new immstruct.Immstruct()
var structure = localImmstruct.get('someKey', { my: 'object' });

API Reference

See API Reference.

Structure Events

A Structure object is an event emitter and emits the following events:

  • swap: Emitted when cursor is updated (new information is set). Is emitted on all types of changes, additions and deletions. The passed structures are always the root structure. One use case for this is to re-render design components. Callback is passed arguments: newStructure, oldStructure, keyPath.
  • next-animation-frame: Same as swap, but only emitted on animation frame. Could use with many render updates and better performance. Callback is passed arguments: newStructure, oldStructure, keyPath.
  • change: Emitted when data/value is updated and it existed before. Emits values: newValue, oldValue and path.
  • delete: Emitted when data/value is removed. Emits value: removedValue and path.
  • add: Emitted when new data/value is added. Emits value: newValue and path.
  • any: With the same semantics as add, change or delete, any is triggered for all types of changes. Differs from swap in the arguments that it is passed. Is passed newValue (or undefined), oldValue (or undefined) and full keyPath. New and old value are the changed value, not relative/scoped to the reference path as with swap.

NOTE: If you update cursors via Cursor.update or Cursor.set, and if the underlying Immutable collection is not inherently changed, swap and changed events will not be emitted, neither will the history (if any) be applied.

See tests for event examples

License

MIT License

immstruct's People

Contributors

amccloud avatar andrewluetgers avatar dashed avatar frederickfogerty avatar jeffbski avatar jergason avatar leonyu avatar louisremi avatar mikaelbr avatar natew avatar neftaly avatar rufman avatar singggum3b avatar tomasd avatar torgeir avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

immstruct's Issues

Remove current cursor.

Lets say you have a "TodoItem" which is rendered inside of a "TodoList".

The todolist might pass each item a cursor:

<TodoItem todo={ struct.cursor(i) }/>

Now, without giving the todoitem access to the entire todolist it would be handy if the "TodoItem" could remove itself like so:

// Inside TodoItem render function.
let { todo } = props;
<button onClick={ (e)=> todo.remove() }/>

This would be pretty handy as currently I'd have to pass down a "requestRemove/onRemove" handler from the parent.


Edit: This is probably a limitation of immutable.js.

Custom cursor implementation

Figured I start this discussion.

From my rambling: https://gitter.im/omniscientjs/omniscient?at=54f4cc9a9622a2fc57f66790

I suggested that omniscientjs would eventually provide its own implementation of cursors that extend from immutable lib.

This would essentially be a fork/rewrite of https://github.com/facebook/immutable-js/tree/master/contrib/cursor

Some obvious features to add:

  • Official API for keyPath.
  • Cursor.isCursor() See: immutable-js/immutable-js#300 Maintainer seems to disagree to add this static method in favour of type transparency.

Ideas:

  • Handle observations in a more fundamental way than at userland (e.g. immstruct) (possible?)

/cc @Gozala

Don't emit events when updating cursor doesn't actually change structure

This is just an optimization consideration.

When you update value of a cursor, and nothing actually changes, swap and change events are emitted. Semantically, they shouldn't be emitted.

Example:

var immstruct = require('immstruct');
var Immutable = require('immutable');
var Cursor = require('immutable/contrib/cursor');

var calls = 0;

var json = { foo: {} };

var struct = immstruct(json);

var foo = struct.cursor('foo');

var original = struct.current;
struct.on('swap', function(newRoot, oldRoot) {
    // ideally, swap event shouldn't be emitted
    assert(newRoot === original);
    assert(oldRoot === original);
    calls++;
});

struct.on('change', function() {
    // ideally, change event shouldn't be emitted either
    calls++;
});

// update cursor that actually doesn't actually change anything
struct.cursor('foo').update(function() {
    return Immutable.Map();
});

assert(calls === 2);

In this line, we can assert: https://github.com/omniscientjs/immstruct/pull/13/files#diff-dc0aa2a7df4698297c4ccd9dc0b071caR52

assert(self.current === oldRoot) && assert(self.current === newRoot)

Add user defined changeListeners

Just throwing ideas around.

We should have a way to add user-land changeListeners:

changeListener = handleHistory(this, changeListener);
changeListener = handleSwap(this, changeListener);
changeListener = handlePersisting(this, changeListener);
return Cursor.from(self.current, path, changeListener);

var Structure = require('immstruct/structure');
var s = new Structure({
  key: 'foo', // default random string
  data: someObject, // default empty object
  withHistory: true, // default `false`
  changeListeners: [ ... ]
});

There was a use-case where I wanted to do this, and I wanted to be able to "block" changeListeners from propagating.

Possible to get child reference from reference?

I want to be able to pass a reference around, and create a reference to a child cursor at a later stage, where I might not have access to the entire structure:

var userRef = structure.reference('user');
// Somewhere else
var childRef = userRef.reference('profile');

Any thoughts on how to accomplish this? Or, how to get a reference by passing a cursor to the structure, perhaps?

documentation for the why of "key"

I don't see a single use case anywhere for why keys even exist even though they are sprinkled throughout the docs, or how you would implement multiple keys.

Even in the very first example you find this line but no mention of "myKey" ever again.

var structure = immstruct('myKey', { a: { b: { c: 1 } } });

Some places even imply that keys are a ho-hum feature:

Provide optional key to be able to retrieve it from list of instances. If no key is provided, a random key will be generated.

optional: - defaults to random string

Maximum call stack size exceeded

Hi,
here is a little sample that crash the call stack in the current 1.4.0 and the master one.
I want to swap 2 element in an List and modify their content.

var structure = Immstruct('a', [ {a:1}, {b:2}]);
var cursor = structure.cursor();
var ca = cursor.get(0);
var res  = cursor.withMutations(function(c){
      c.set(0, cursor.get(1))
      .set(1, ca)});
structure.cursor().setIn([0, 'b'], 3)

This raise a stack overflow error.
Any idea ?
Thanks.

Computed values based on sets of cursors

This is more an idea than a specific feature suggestion.

I often find myself wanting to update pieces of a structure based on when other specific pieces of a structure or other structures change; think knockout computed observables but with cursors.

How about

var m = immstruct({ m: 42 }).cursor('m');

var somewhere = immstruct({ somewhere: { else: { n : 42 } } });
var n = somewhere.cursor(['somewhere', 'else', 'n']);

var result = immstruct({ sum: undefined });
immstruct.listen([m, n], (m, n) => result.set('sum', m.deref() + n.deref());

Refactor Structure to be cursor type agnostic

While sketching out API for some new cursor types that extend Immutable cursors, one type uses Structure of a different cursor type in a meta manner. I needed something akin to immstruct Structures.

So I figured that it could be configured to use a different cursor type.

This is needed for some cursor types I'm sketching out to solve the following issues in a more fundamental way:

/cc @Gozala


By default, Structure would use some extended cursor type.

This is how I would envision the refactor of Structure:

  • API is largely the same; some are omitted for brevit
  • Reference cursor API are removed, and its feature set are shifted to a cursor type.
// this is cursor type agnostic

function Structure(options) {
    this.state = options.state || Immutable.Map();
    this.cursorType = options.cursorType || Carbon;
    this.onChange = options.onChange || null;
    this.updateCurrent = options.updateCurrent; // required
    this.captureEvent = options.captureEvent || analyze;
    this.__onCommit = function() {
        self.onCommit.apply(self, arguments)
    };

    EventEmitter.call(this, arguments);
}

inherits(Structure, EventEmitter);

Structure.prototype.onCommit = function() {

    const oldState = this.state;
    const self = this;

    this.state = this.updateCurrent.apply(this._onChange, arguments);

    const event = this.captureEvent.apply(this.captureEvent, arguments);

    setTimeout(function() {
        self.onChange && self.onChange();
        self.emit.call(self, 'swap', state, oldState);
        self.emit.apply(self, [event.name].concat(event.arguments));
    }, 0);

    if(!this.history) return;

    // update history
    this.history = this.history
      .take(++this._currentRevision)
      .push(this.current);
}

Structure.prototype.cursor = function(path) {
    path = path || [];
    return this.cursorType.from(this.state, path, this.__onCommit);
}

// this was forceHasSwapped(...)
// I think this is a better name; inspired by 
// http://facebook.github.io/react/docs/component-api.html#replacestate
Structure.prototype.replaceState = function(newState) {
    this.state = newState;

    const self = this;
    setTimeout(function() {
        self.emit.call(self, 'swap', state, oldState);
    }, 0);
}

// all of these are the same API. but by semantics, they should call swap.
// user has access to this.history anyways
Structure.prototype.undo = // same ...
Structure.prototype.redo = // same ...
Structure.prototype.undoUntil = // same ...

references does not work at all (or aren't so powerful as I was expecting).

I'm trying to understand this behavior:

'use strict';

var ims = require('immstruct'),
  im = require('immutable');

var ds = ims();

// initialize somewhere in the code
ds.cursor().update(function() {
  return im.fromJS({
    k: 'hello world'
  });
});

// create a reference somewhere else
var ref = ds.reference('k');

// and finally update the structure in some way
ds.cursor().update(function() {
  return im.fromJS({
    k: {
      value: 10
    }
  });
});

// print the result
console.log(ref.cursor().deref());

But, if I'm specific enough about the key I'm interested to change:

'use strict';

var ims = require('immstruct'),
  im = require('immutable');

var ds = ims();

ds.cursor().update(function() {
  return im.fromJS({
    k: 'hello world'
  });
});

var ref = ds.reference('k');

ds.cursor('k').update(function(oldValue) {
  return 10;
});

console.log(ref.cursor().deref());

Seems it works perfectly, so could be this behavior considered a bug? Because, at the moment I create the references, I don't know the state of the immutable data structure, so I can't be as specific as the API requires to make my code works.

Thanks in advance

Make immstruct instantiable?

I'm mulling over an idea to make immstruct instantiable. Pretty much in the same manner that EventEmitter is instantiable.

I'm suggesting this so that something like new immstruct().instances would be compartmentalized. I have a library that's a work-in-progress that decorates the immstruct, and one feature I have is that it creates an associated meta-structure for a given immstruct structure.

Revise how `self.current` syncs

Following conversation from: https://gitter.im/omniscientjs/omniscient/archives/2014/12/17

Following lines seem redundant:

return self.current = self.current.updateIn(path, function (data) {
return newRoot.getIn(path);
});

If self.current === oldRoot, you can just do self.current = newRoot, otherwise you'll need to splice subtrees to resync.

I'm raising this issue, because on delete event: return self.current = newRoot;

return self.current = newRoot;

I'm unsure how to replicate out-of-sync Immutable collection.
EDIT: see comments below

[question] immstruct version of this immutable code

Just messing around and trying to figure out the proper way to build up a data structure in immstruct. First here is the immutable.js version of what I want to do.

var Immutable = require('immutable');                                                                                                             
var map = Immutable.fromJS({a:undefined});                                                                                                        
map = map.updateIn( [ 'a' ], () => Immutable.Map( { b: undefined } ) )                                                                            
map = map.updateIn( [ 'a', 'b' ], () => Immutable.Map( { c: 'here' } ) )                                                                          
console.log(map.getIn(['a','b','c'])) //=> 'here 

Here is the immstruct version. It works but what bothers me is that is uses Immutable.Map(). Is there a strictly immstruct way to do this?

var immstruct = require('immstruct');                                                                                                             
var structure = immstruct({a:undefined});                                                                                                         
var cursor = structure.cursor( [ 'a' ] ).update( () => Immutable.Map( { b: undefined } ) )                                                        
var cursor = structure.cursor( [ 'a', 'b' ] ).update( () => Immutable.Map( { c: 'there' } ) )                                                     
console.log(structure.cursor(['a','b', 'c']).deref()) //=> 'there' 

'next-animation-frame' with multiple structures

If you create multiple instances of Structure, event handlers registered for 'next-animation-frame' are only called for the first instance for which queuedChange was false.

If multiple Structure instances are modified within a frame, this leads to event handlers not being called when expected.

I see two possible solutions:

  • Define queuedChange per instance, rather than as a module global, or:
  • Have requestAnimationFrameEmitter add to a list of [emitter, newStructure, oldData, keyPath] entries every time it is called, and emit 'next-animation-frame' on every item in that list.

Reference observation doesn't listen in on updated subtrees

tl;dr

let structure = immstruct();

// this doesn't work as intended. very unexpected when user doesn't know how
// Cursor.from(...) works.
structure
.reference(['config', 'profile'])
.observe(function(newObj, oldObj, hotKeyPath) {
    // no output
    console.log(arguments);
});


// update some object
structure.cursor().set('config', Immutable.fromJS({
    'profile': 'throwaway',
    'color': 'green'
}));

working solution

let immstruct = require('immstruct');
let Immutable = require('immutable');

let structure = immstruct();

// this doesn't work as intended. very unexpected when user doesn't know how
// Cursor.from(...) works.
structure
.reference(['config', 'profile'])
.observe(function(newObj, oldObj, hotKeyPath) {
    // no output
    console.log(arguments);
});

const NOT_SET = {};

const rootRef = structure.reference();

// much more useful observation...
function betterObserve(observeKeyPath, eventName, newFn) {

    // this probably calls for self._pathListeners to be a tree structure or
    // something.

    return rootRef.observe(function(newRoot, oldRoot, hotKeyPath) {

        if(newRoot === oldRoot) {
            return;
        }

        let
        idx = 0,
        newObj = newRoot,
        oldObj = oldRoot,
        hotKeyPathLen = hotKeyPath.length;

        for(let len = observeKeyPath.length; idx < len; idx++) {

            let atomPath = observeKeyPath[idx];

            // verify if hotKeyPath is a subpath of observeKeyPath
            if(idx < hotKeyPathLen && atomPath !== hotKeyPath[idx]) {
                return;
            }

            if((newObj == NOT_SET && oldObj !== NOT_SET)
                || (oldObj == NOT_SET && newObj !== NOT_SET)) {
                continue;
            }

            newObj = newObj.get(atomPath, NOT_SET);
            oldObj = oldObj.get(atomPath, NOT_SET);

            if(newObj === oldObj) {
                return;
            }
        }

        if (eventName && eventName !== 'swap') {
            let info = analyze(newRoot, oldRoot, observeKeyPath);
            if (info.eventName !== eventName) return;
            return newFn.apply(newFn, info.arguments);
        }

        return newFn(newRoot, oldRoot, observeKeyPath);
    });

};

betterObserve(['config', 'profile'], 'add', function() {
    // outputs: { '0': [ 'config', 'profile' ], '1': 'default' }
    console.log(arguments);
});

// update some object
structure.cursor().set('config', Immutable.fromJS({
    'profile': 'default',
    'color': 'green'
}));

Delete an instance?

There's a way to clear all instances of structure, but no way to delete a specific one.

Wrap `handleSwap` and `handlePersisting` to be executed in async manner

Wrap handleSwap and handlePersisting to be executed in async manner. That is, setTimeout(fn, 0); or similar.


If one does cursor.update(), then it won't return until all observers completely execute:

  • .on events
  • reference cursor observers

React.render is tied to swap event or similar, so cursor.update() won't even return until after the next React.render completes!

All of this needs to be in sync with cursor.update() call:

immstruct/src/structure.js

Lines 105 to 120 in 6b3ff14

var changeListener = function (newRoot, oldRoot, path) {
if(self.current === oldRoot) {
return self.current = newRoot;
}
// Othewise an out-of-sync change occured. We ignore `oldRoot`, and focus on
// changes at path `path`, and sync this to `self.current`.
if(!hasIn(newRoot, path)) {
return self.current = self.current.removeIn(path);
}
// Update an existing path or add a new path within the current map.
return self.current = self.current.setIn(path, newRoot.getIn(path));
};
changeListener = handleHistory(this, changeListener);

Thus, handleSwap and handlePersisting can be executed asynchronously.

structure.cursor, structure.reference, and reference.cursor do not properly handle valid but falsey keys

Immutable's cursors, including Cursor.from, use valToKeyPath under the hood to convert '' to [''], but structures and references handle .cursor(''), as well as .cursor(0) and other falsey keys, as though they are all .cursor() (and structure.reference('') is handled as structure.reference()). Because of this, the following happens:

> var s = immstruct({'': 3, a: {'': 5}})
> s.cursor().cursor('').deref()
3
> s.cursor('').deref()
Immutable.Map({'': 3, a: {'': 5}}) // Prettified for brevity
> var r = s.reference('a')
> r.cursor().cursor('').deref()
5
> r.cursor('').deref()
Immutable.Map({'': 5})

While an edge case, this means users who can't enforce a policy restricting keys to values that do not evaluate to false have to use the same sort of checks valToKeyPath does already to ensure any supplied values are properly converted to key paths.

`handleSwap` and `handlePersisting` incorrectly use previous state.

handleSwap and handlePersisting incorrectly use previous state

This is bad when a stale cursor updates, and a stale "previous" state (e.g oldData) is passed instead of the actual previous state.

I'll eventually PR.

Can be fixed by doing something like this:

// Map changes to update events (delete/change/add).
function handlePersisting (emitter, fn) {
  return function (newData, oldData, path) {
    var previous = emitter.current;
    var newStructure = fn.apply(fn, arguments);
    if(newData === previous) return newStructure;
    var info = analyze(newData, previous, path);

    if (info.eventName) {
      emitter.emit.apply(emitter, [info.eventName].concat(info.arguments));
    }
    return newStructure;
  };
}

// ...

// Emit swap event on values are swapped
function handleSwap (emitter, fn) {
  return function (newData, oldData, keyPath) {
    var previous = emitter.current;
    var newStructure = fn.apply(fn, arguments);
    if(newData === previous) return newStructure;

    emitter.emit('swap', newStructure, previous, keyPath);
    possiblyEmitAnimationFrameEvent(emitter, newStructure, previous, keyPath);

    return newStructure;
  };
}

value propagation check

I was thinking on this some more: omniscientjs/omniscient#104 (comment)

Rather than doing this naive check in shouldComponentUpdate, this may be done during development as an indicator that it should never occur. This can warn users that they're not really using the immutable library effectively.

API sketch:

// enable debug mode
if ('production' !== process.env.NODE_ENV) {
    immstruct.debug();
}

const struct = immstruct({foo: [1, 2]});

// adding a listener through .on(), .observe(), etc enables value propagation check.
// 
// internally, do something like this when values are propagated to listeners:
//
// if('production' !== process.env.NODE_ENV && 
//    newRoot.getIn(path) !== oldRoot.getIn(path) && 
//    Immutable.is(newRoot.getIn(path), oldRoot.getIn(path))) {
//
//   console.warn('detected node with distinct reference but have same value');
// }
struct.on('swap', _ => _);

// deliberate bad practice
struct.cursor('foo').update(function() {
     return Immutable.List.of(1, 2);
});

Working examples for React -> Immutable -> Immstruct?

Hi guys,

I know you are mostly concerned about Omniscient but for people who want to stay with React only, I wonder, if there are nice examples already of stacks using React -> Immutable -> Immstruct?

Basically this question is about experiences, problems that might pop up, advantages/disadvantages compared to other solutions such as Omniscient or Morearty.

Handling structures within components

Not sure what to title this one. I have the following setup, using react-router's asyncProps branch which passes in async fetched data to components as props:

var ArticlePage = React.createClass({
  statics: {
    getAsyncProps: (params) => GetStores(params, ['article'])
  },

  getInitialState: () => ({ version: 0 }),

  componentWillReceiveProps(nextProps) {
    if (nextProps.article) {
      this.structure = Immstruct({ article: nextProps.article[0].data });
      this.structure.on('next-animation-frame', () => {
        this.setState({ version: ++this.state.version });
      });
    }
  },

  render() {
    if (!this.structure) return <span />;
    var article = window.articleCursor = this.structure.cursor().get('article');
    return ArticleComponent(`AP-${article.get('id')}-${this.state.version}`, article);
  }
});
module.exports = Component('Article', article => {

  //... render comments

});
var Comment = Component('Comment', function(cursor) {
  var comment = cursor.data;

  var toggleOpened = (e) => {
    e.stopPropagation();
    comment.update('closed', closed => !closed);
  };

  var classes = { closed: comment.get('closed') };

  return (
    <div className={cx(classes)} onClick={toggleOpened}>
      // ... comment content
    </div>
  );
});

Now, using the version state in ArticlePage, when the comment is toggled the entire ArticlePage HTML is swapped out on the page (rather than just the individual comment). BUT, if I remove the version tracking in ArticlePage, nothing is updated on page when I toggle comments. It seems like it needs the key to be unique in order to re-render.

I've tried a few different things here but am stuck... what is the right way to have just the single comment HTML update and not have the entire ArticlePage HTML get swapped out by React?

Listen for changes to specific paths

We can easily add listeners to when specific pieces of data inside a structure changes.

Of course, you can aldready do

structure.on('change', (path, _, _) => {
  if (path === 'this.path') doThis();
  else if (path == 'something.else') doSomethingElse();
});

But something along the lines of this would be nice

structure.on('change', 'this.path', doThis);
structure.on('change', 'something.else', doSomethingElse);

Question

I'm new to using github, so please forgive me if this is in the wrong place. I couldn't find anywhere else to pose the question.

I'm loving immstruct, but not for the life of me can I figure out how to create a cursor for an object within a collection/array. I have the following:

var structure = immstruct({
    catalog: [],
    cartItems: [],
});

// populate catalog for now...
for (var i = 1; i < 9; i++) {
    structure.cursor('catalog').push({
        'id': 'Widget' + i,
        'title': 'Widget #' + i,
        'summary': 'This is an awesome widget!',
        'description': 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Ducimus, commodi.',
        'img': '/assets/product.png',
        'cost': i
    });
}

// My _increaseItem function accepts an index; is this the way I should be getting
// the index?  By casting the cursor of the collection to an array and then
// using indexOf()?
function _addItem(item) {
    if (!item.inCart) {
        item['qty'] = 1;
        item['inCart'] = true;
        structure.cursor('cartItems').push(item);
    } else {
        var cartArray = structure.cursor('cartItems').toArray();
        var itemIndex = cartArray.indexOf(item);
        _increaseItem(itemIndex);
    }
}

// Because I have no idea how to create a cursor to the actual entry,
// I am changing its value and then calling forceHasSwapped()
// Is there a better way?
function _increaseItem(index) {
    var item = structure.cursor('cartItems').get(index);
    item.qty++;
    structure.forceHasSwapped();
}

Thanks in advance
Richard

Be able to dynamically disable/enable changeListeners

Use case:

// stop changeListeners from triggering
struct.trigger(false);

// Do multiple modifications.
// 
// Don't trigger swap, add, change, or delete events.
// Also, don't save snapshot into history.
struct.cursor('a').set('foo', 0);
struct.cursor('b').set('bar', 1);

// re-enable changeListeners
struct.trigger(true);

// apply changes
struct.forceHasSwapped();

Async/Remote Cursors for App Sync

So not sure if this would be better as a separate lib vs part of Immstruct but here's the idea. I'm implementing the "all your state in one place" pattern with my latest react project using Omniscient, great, love the pattern, using cursors etc. Then I think how do I do the network stuff?? I'm doing custom ajax off of a cursor but then I have to sync changes from the server, cool I can update state via web-sockets. Then I think, wait a sec, couldn't this work generically? So I think it could if there was some kind of remote cursor idea the server and client could stay in sync on a shared cursor location. If this could work all the server and client code could essentially ignore the fact that they are not on the same machine and just deal with shared cursors. In the client it would render state changes and push them to the server, on the server it would persist state changes and push them to the client. You might also want to persist on the client side for offline support, eagerly reading from and writing to local-storage optimistically while network activity takes place. If the network activity fails you can easily roll back to the old state!

So obviously this is a tall order, but I think it could be unbelievably awesome so I will be experimenting with this idea and wanted to get your general input on it. How feasible this is for naive last write wins remote syncing using shared cursors. Also where would I best start looking for implementing such a thing in Immstruct or if you even think this is the right place for it?

Thanks!

Observe Events on forceHasSwapped

Should calling forceHasSwapped forcibly trigger a swap event for observing reference cursors?

For example:

const ref = structure.reference(['data']);
ref.observe('swap', () => {
  //should this be called?
});

const old = structure.current;
structure.forceHasSwapped(structure.undo(1), old);

Currently it does not appear as though it is getting called.

Be able to override `randString`

In the spirit of omniscientjs/omniscient#36, I think it'll be good to be able to override randString utility function.

In addition, I recommend to rename from randString to generateKey.


For my app, I'm planning on manually entering keys for Structure instances from an external key generator. I figured it'd be good to do this internally in the library.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.