Git Product home page Git Product logo

cerebral's Introduction

Cerebral

A declarative state and side effects management solution for popular JavaScript frameworks

NPM version Build status Coverage Status bitHound Score Commitizen friendly Discord

Maintainer needed

https://gist.github.com/christianalfoni/f1c4bfe320dcb24c403635d9bca3fa40

Documentation

Contribute

The entire Cerebral codebase has been rewritten to encourage contributions. The code is cleaned up, commented and all code is in a "monorepo". That means you can run tests across projects and general management of the code is simplified a lot.

  1. Clone the monorepo: git clone https://github.com/cerebral/cerebral.git
  2. In root: npm install

The packages are located under packages folder and there is no need to run npm install for each package.

Using monorepo for your own apps

If you want to use Cerebral 2 directly from your cloned repo, you can create a symlinks for following directories into the node_modules directory of your app:

  • packages/node_modules/cerebral
  • packages/node_modules/function-tree
  • packages/node_modules/@cerebral

If your app and the cerebral monorepo are in the same folder you can do from inside your app directory:

$ ln -s ../../cerebral/packages/node_modules/cerebral/ node_modules/
# ...

Just remember to unlink the package before installing it from npm:

$ unlink node_modules/cerebral
# ...

Running demos

Go to the respective packages/demos/some-demo-folder and run npm start

Testing

You can run all tests in all packages from root:

npm test

Or you can run tests for specific packages by going to package root and do the same:

npm test

Changing the code

When you make a code change you should create a branch first. When the code is changed and backed up by a test you can commit it from the root using:

npm run commit

This will give you a guide to creating a commit message. Then you just push and create a pull request as normal on Github.

Release process

  • Review and merge PRs into next branch. It is safe to use "Update branch", the commit created by Github will not be part of next history
  • If changes to repo-cooker, clean Travis NPM cache
  • From command line:
$ git checkout next
$ git pull
$ npm install # make sure any new dependencies are installed
$ npm install --no-save repo-cooker # needed to test release, make sure you have latest
$ npm run release # and check release notes
$ git checkout master
$ git pull
$ git merge --ff-only next
$ git push

cerebral's People

Contributors

abalmos avatar andrewvmail avatar bannerintheuk avatar bdjnk avatar bfitch avatar br1anchen avatar cerebraljs avatar christianalfoni avatar edgesoft avatar fmal avatar fopsdev avatar fvgoto avatar fweinb avatar garth avatar gaspard avatar guria avatar henri-hulski avatar hipertracker avatar itflies avatar johannesand avatar jtasek avatar julio-saito-linx avatar maxfrigge avatar menberg avatar nd0ut avatar reflog avatar saulshanabrook avatar schotime avatar sladiri avatar yusufsafak 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  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

cerebral's Issues

Debugger time adds promise execution time as well

When I watched one of your tutorials on YouTube, I think I noticed that the debugger added the execution time of promises as well. The way I understood the purpose of the shown value was, that it is used to determine, whether your app is fast enough.

Is it still accurate, when the promises are included?

Allow parallell async actions

cerebral.signal('applicationRendered', [getProjects, getUsers, getTemplates], setInitialState);

Actions in arrays runs in parallell

Todomvc issues

Hi @christianalfoni

Testing out http://christianalfoni.com/todomvc/ I found some issues with recording and the debugger.

Reproduced a crash consistently by adding items, hitting record, adding more items, playing back, then console errors appear when scrubbing with the debugger timeline.

See attached console screenshots (wish I could be of more help but don't have access to code :))

screen shot 2015-06-01 at 11 58 05 am
screen shot 2015-06-01 at 11 59 22 am

Abort signal chain?

Is there a way to abort a signal chain? I have a register signal, that tries to register the user, and then after that it logs them in. If the register fails, I don't want to continue down the signal chain. Another option is the ability to call signals from within signals, but this seems like a less than ideal solution that would make it harder to reason about signal chains and how they flow.

Handle XHR / Promise rejection

How do I handle promise rejection / XHR errors in Cerebral? Can't find much info about this.. Any guidance would be appreciated :).

namespacing in cerebral

It's not clear on whether it's not mentioned anywhere, if it's planned and not implemented or if it's a design decision to have a flat state tree at the core of Cerebral. How do I separate the things from each other and put them in their own namespace in the cerebral instance?

unset working correctly?

Should the following work?

let tree = Cerebral({
  unsetTest: {
    foo: 'bar',
    removeMe: 'please'
  }
})

tree.unset(['unsetTest', 'removeMe'])

The normal use case for this would be removing a model on delete. tree.unset(['projects', '123']).

It doesn't seem to work at the moment. You can try it out on my fork:
https://github.com/marbemac/cerebral-boilerplate

Prevent signals from running when running async stuff

If you click around in the interface creating new signals while Cerebral is running async stuff it should not run the signal, but display a message about that is not possible yet. The reason is that Cerebral can not abort async signals... at least not currently.

Work with React-Router?

Hey @christianalfoni, this look awesome! Giving it a go this weekend :).

I've been trying to get Cerebral to work with React-Router with mixed results.

Using the code below, I see a "Cannot read property 'signals' of undefined" error in app.jsx.

I also get a warning "Warning: owner-based and parent-based contexts differ (values: undefined vs [object Object]) for key (cerebral) while mounting App (see: http://fb.me/react-context-by-parent)".

Do you have a quick little example repo of cerebral + react-router working with a couple of routes?

main.jsx

import React from 'react'
import router from './router.js'
import cerebral from './store/cerebral.js'
import AppWrapper from './layouts/appWrapper'

var Wrapper = cerebral.injectInto(AppWrapper)

// run the app. this is the entry point for every route
router.run((Root, state) => {
  let app = (
    <Wrapper>
      <Root {...state} />
    </Wrapper>
  )

  React.render(app, document.getElementById('sl-app-container'))
})

appWrapper.jsx

import React from 'react'
import CerebralMixin from 'cerebral/mixin'

let AppWrapper = React.createClass({
  mixins: [ CerebralMixin ],

  propTypes: {
    children: React.PropTypes.node.isRequired
  },

  getCerebralState() {
    return {
      loading: ['appLoading']
    }
  },

  componentDidMount() {
    this.signals.appMounted()
  },

  render() {
    if (this.state.loading) {
      return (
        <div className='sl-app-wrapper h-100'>
          Loading...
        </div>
      )
    }

    return (
      <div className='sl-app-wrapper h-100'>
        {this.props.children}
      </div>
    )
  }
})

export default AppWrapper

app.jsx

import React from 'react'
import { RouteHandler } from 'react-router'
import CerebralMixin from 'cerebral/mixin'

let App = React.createClass({
  mixins: [ CerebralMixin ],

  getCerebralState() {
    return {
      session: ['session']
    }
  },

  render() {
    return (
      <div className='sl-app h-100'>
        <RouteHandler />
      </div>
    )
  }
})

export default App

login.jsx

import React from 'react'
import CerebralMixin from 'cerebral/mixin'

let Login = React.createClass({
  mixins: [ CerebralMixin ],

  getCerebralState() {
    return {
      session: ['session']
    }
  },

  render() {
    return (
      <div>Login Page</div>
    )
  }
})

export default Login

routes.jsx

import React from 'react'
import { Route, DefaultRoute } from 'react-router'
import App from './layouts/app'
import Login from './pages/login'

let routes = (
  <Route handler={App} path='/'>
    <Route name='login' handler={Login} />
  </Route>
)

export default routes

Return a promise from this.signals.mySignal()?

This would be useful in a variety of circumstances. For example, when deleting a project from a react component, I'd like to route away (using the react-router context present in the component) if this.signals.deleteProject() succeeds. I also want to call another signal this.signals.createAppNotification('Project Deleted!') on success.

let ProjectPage = React.createClass({
  mixins: [ CerebralMixin ],

  getCerebralState() {
    return {
      project: ['currentProject']
    }
  },

  contextTypes: {
    router: React.PropTypes.func.isRequired
  },

  handleDelete() {
    let _this = this
    let r = confirm('Are you sure you want to delete this project? This will also delete all associated environments and requests.')
    if (r === true) {
      this.signals.deleteProject({id: this.state.project.id}).then(function() {
        _this.signals.createAppNotification('Project Deleted!')
        _this.context.router.transitionTo('entries')
      })
    }
  }

})

Does this make sense?

How to conditionals in signals

Let us just start out with an example:

controller.signal('appMounted', isAdmin, [getAdminData]);

Very often you have some conditional in your action chain. The question is, where does this conditional belong?

onMount: function () {
  if (this.state.isAdmin) {
    controller.signals.appMounted();
  }
}

It could have been handled at the call site, but then you have to make sure each callsite does this check.

Maybe we could configure the signal?

controller.signal({
  name: 'appMounted',
  filter: isAdmin
}, [getAdminData]);

But what if you wanted multiple conditionals... maybe even inside the chain somewhere?

controller.signal('appMounted', [getUser, getItems], isAdmin, [getAdminData]);

Or maybe this actually belongs with the action?

const getAdminData = function (args, state, promise) {
  if (state.get('isAdmin') {
    args.utils.ajax.get('/admindata').then(promise.resolve).catch(promise.reject);
  } else {
    promise.resolve({data: null});
  }
}

Any thoughts and scenarios would be appreciated :-)

Currently my cents on this is:

  1. An admin signal should never be triggered by the UI if the user is not an admin... as that button (or whatever) should not be visible. The ajax request would also fail, as the user really is not an admin
  2. If a signal could be triggered with an admin/non admin I think the action should handle it, resolving "no data" if the user is not admin and getting data if it is an admin

Thoughts on Cerebral with the different Stores

Hi Christian, great stuff here. Really appreciate you putting it out there, with the great vids too.
I'm eager to try Cerebral on my next project. As I approach it I am also wondering which Store to use. I have been using Baobab on my last couple of projects, and I know you've used Baobab a bunch in the recent past too. Can you share your experience/preferences so far on using say Baobab or any of the immutable store libs with Cerebral? With Baobab you get cursors/facets...does that become less important when using Cerebral eventing, etc? Which store tends to "feel" better when plugging into Cerebral?
Many thanks!
:Larry

toJS vs extractState

This works:

myCerebral.get("foo").toJS()

... and i expected this to work, but it doesn't:

myCerebral.toJS()

so i'm using this, to grab the entire state so i can serialise it:

myCerebral.extractState()

but extractState isn't mentioned in the API.md file.

Finer control to "Memory" storage

We need to ability to choose what signals get remembered, so either I will do this myself or if you feel it is a worthwhile feature we can add it to the API.

Controller.signal('somethingHappened', ...actions ).remember(false)

The default would be remember(true) so you don't have to define it.

The reason is that I am building a history component that is interested in undo/redo for worthwile signals such as add/edit/delete. It is not interested in remembering the state of a text input or other 'minor' signals.

Error from passing mouse event to signal has no stacktrace

When I pass a mouse event to a signal, I get an Uncaught Error: You have to pass a serializeable object to a signal. Did you pass a mouse event maybe?(anonymous function) @ inserted-script.js:21 error.

However, it doesn't give any stacktrace, so it is hard to know what signal is being passed the incorrect object. It doesn't need a stacktrace, just some hints so it is easier to debug.

Facet name change

Hey, hope I am not being presumptuous! :)

But what is a facet anyway? http://dictionary.reference.com/browse/facet

Closest I could come to in this context was number 3. aspect - as in, an aspect of the data tree.

What are facets doing? getting, combing, composing, transforming, reducing...

Personally I like the word 'getter' but maybe we could create a new one? what is your take on facets?

Close this thread if it is irrelevant :)

Mutations in async signals?

Must one manage ALL state in cerebral? Take the example below, where I have a login component with a couple of input fields. Rather than adding a new "loginFields" property to the cerebralTree, and adding all the cerebral plumbing to the login component to track input changes, I just call a single signal, "login", with the login payload, on submit. I'd like to set the loading indicator to true, and I do this in it's own signal (session.loading) since I can't mutate the tree in session.create (async). Thus, the payload I'm passing is sent to the loading function, instead of the create function.

Is there a reason we can't do simple mutations in signals that return an async function (perhaps only outside of the returned async function?)? Basically what I want to do is put "cerebral.set(['session', '$loading'], true)" at the top of the session.create function below.

cerebral.signal('login', session.loading, session.create, session.set)
handleSubmit(event) {
    event.preventDefault()

    let payload = {
      email: this.refs.email.getDOMNode().value,
      password: this.refs.password.getDOMNode().value
    }

    this.signals.login(payload)
}
import Remote from '../remote'

let loading = function (cerebral) {
  cerebral.set(['session', '$loading'], true)

  return arguments
}

let create = function (cerebral, payload) {
  // this is an async function
  return Remote.create(Remote.models.SESSION, payload, false)
}

let set = function (cerebral, result) {
  if (result.data) {
    localStorage.session = JSON.stringify(result.data)
    cerebral.set('session', result.data)
  } else {
    cerebral.set('session', {
      $loading: false,
      $error: result.error
    })
  }

  return result.data
}

export default {
  loading: loading,
  create: create,
  set: set
}

Question: how to handle a modal

Hi Christian,

I have a question about modeling state for a stack of modals and what the best way would be to separate actions and signals. Here are the details.

First, let me just explain the modal service which I call as follows

var instance = modal(Component, ...componentArgs)
// instance.open() returns a promise
instance.open().then(modalClosedOnSave).catch(modalDismissed)

When instance.open() is called, the modal is pushed onto a stack; when a backdrop is clicked or user presses escape, etc. the top most modal is popped off the stack. On each event, my view layer simply renders the whole stack. This way, I am able to have nested modals.

Ex. I have a form with some fields. Some of the fields then can be clicked to open a selector modal which has a long list of options for them to select. When they select an option, the selector modal gets closed but the form modal underneath remains open and the field gets filled in with the option.

So, unlike dropdown menu state, or input focus state, which is limited to just one component, the state of the modal stack is globally relevant so I would like to store it in baobab.

My question is how should I handle this state with cerebral actions and signals. Here is the full flow:

  1. User is on documents page. They can click a button to open the modal which contains the add-document form component. Stack consists of [add-document]
  2. The user can start filling out the form. They get to a project field where they can assign the document to a project. They click a button which opens another modal responsible for loading projects. They can type a few letters to search for the project by name and the projects are then loaded. Modal stack at this point consists of [add-document, project-selector]
  3. They select a project and the top modal closes. Selector closes and updated stack consists of [add-document]
  4. They want to finish the form later so they dismiss the add-document modal. Stack [ ]; all modals closed but the form state is saved

What would be a good separation of the above flow into signals?

Here is my attempt

cerebral.signal('addDocumentClicked', [openModal, {
  resolved: [saveAddDocumentForm, submitAddDocument, {
     success: [clearAddDocumentForm],
     error: [showNotification]
  }],
  dismissed: [saveAddDocumentForm]
}])

What mutations should be made to state in openModal?

Remember, the modal service requires a component. So is it okay to pass a component as args to an action? Finally, the modal stack stores a dynamic instance of the component and not a serializable object so how should the state be stored in baobab, for example?

Sorry for the long question, but I am just trying to wrap my head around this. I also think this is sufficiently complex example that people will run into when using cerebral, so I would like to understand the best practice so we can hopefully turn it into an article or tutorial for the cerebral wiki.

Refactor map to functions inside tree

Convert this:

cerebral.map('visibleTodos', ['todos'], function (cerebral, refs) {});

to this:

let Map = {
  visibleTodos:  function () {
    return {
      value: [],
      deps: ['todos'],
      get: function (cerebral, deps, value) {}
    };
  }
};

Cerebral({
  todos: [],
  visibleTodos: Map.visibleTodos
});

New Feature: "Conditionals"

It is suggested that the signal chain needs the ability to conditionally run actions. The current suggestion is to move even closer to traditional middleware where you have a "next" resolver. We also introduce the use of {} arbitrarily to allow paths to be resolved.

const SUCCESS = 'SUCCESS';
const ERROR = 'ERROR';

// We introduce a "next" resolver instead. It is to be used to resolve values 
// to the args object and/or choose path

const syncOrAsyncAction = function (args, state, next) {
  if (condition) {
    next(SUCCESS, {foo: 'bar'});
  } else {
    next(ERROR, {foo: 'bar'});
  }
}

// With Babel and ES6 syntax this can be expressed like this

controller.signal('appMounted', syncOrAsyncAction, {
  [SUCCESS]: [],
  [ERROR]: []
});

controller.signal('appMounted', [syncOrAsyncAction, {
  [SUCCESS]: [],
  [ERROR]: []
}]);

So the questions here are:

  1. What should happen when you resolve next({}), but there is a path to go down. Should it throw error? Should it go down the first path? Or a DEFAULT key path?
  2. What about the opposite? You want to go down a path, but it does not exist? Should probably be error?
  3. Is it required to use next()with sync actions? Or should it just move on? Or should the signal stop?

In regards of migration we can still allow returning values and use "resolve/reject" methods, just warn DEPRECATION.

Cerebral with React and stateless function

I'm using Cerebral with React. I'm running into an issue when using Cerebral without a React Class. Instead of using React.createClass I'm using a stateless function to produce the component.

When the event emitter runs update, the Cerebral mixin function _mergeInState is given the wrong reference to "this". "this" is a Cerebral object, and not the component. An error occurs when the script attempts to access this.context.cerebral in _mergeInState:

var cerebral = this.context.cerebral;

From troubleshooting I've found that ReactClass binds this to the component. Which results in _mergeInState keeping the component reference for this.

It looks like the chains of events are:
1.) this.signals.signalName(value) is called
2.) Function literal assigned to signalPath.path[signalPath.key] in createSignalMethod is called
3.) emit('update') on EventEmitter from events module is called

EventEmitter is a dependency module that Cerebral uses.

The particular component that triggers this issue can be found here.

First I want to confirm if this is a bug. If it's not a bug can you explain why not so I know where to look next?

Component update only when relevant state changes?

Hi,
I am exploring cerebral, saw the videos, but did not find information if components get updated only if state they requested is changed or they are updated simply on any state change.

For example, in this case would be nice if component refresh only when on of these three properties in state changes:

@Cerebral({
  isLoading: ['isLoading'],
  user: ['user'],
  error: ['error']  
})

So wondering if something like this is implemented or its planned.

Thank you!
Btw videos you created are very helpful make first steps, well done!

Usage with a SPA

When using this framework with a single page application, how do you handle the unique state and signals used by each page within the application?

The state seems to be fairly easy to solve by nesting it within the app state and using a map to always refer to the current page state.

The signals seem to be the issue. There doesn't seem to be a concept of nested signals or signals attached to nodes within the tree. I was initially looking for some type of cursor like Baobab that I could select and perhaps attach signals to the cursor.

let pageCursor = cerebral.select(['pages', 'landing']);
pageCursor.signal('modifyDashboardClicked', modifyDashboard);

Then I could attach this cursor/cerebral sub-instance to the react component rendering the particular page and all of it's state and signals would be available.

Maybe this is a bad idea or way out of scope, but any guidance would be appreciated.

Idea: splitting view and model related packages

Hi @christianalfoni,

In order to understand how cerebral works I'm implementing a cerebral package backed by tcomb:

cerebral-tcomb

The goal would be to provide an immutable state which is also type checked.

Coming to this issue, all the cerebral-react-* packages share the same code for react. It would be nice to extract that code in a separate package. Something like:

  • cerebral-baobab (model handler)
  • cerebral-tcomb (model handler)
  • cerebral-immutable-store (model handler)
  • cerebral-react (view handler)
  • cerebral-react-native (view handler)
  • ...

Then cerebral-react-baobab would be the mixin of cerebral-react + cerebral-baobab.

Example

// cerebral-baobab package (just a model handler)

function controllerFactory(initialState, defaultArgs, options) {
  ....
}
module.exports = controllerFactory;
// cerebral-react package (just a view handler)

function attachView(controllerFactory) {

  function factory(initialState, defaultArgs, options) {
    var controller = controllerFactory(initialState, defaultArgs, options);
    controller.injectInto = ...
    return controller;
  }

  factory.Mixin = ...

  factory.Decorator = ...

  factory.HOC = ...

  return factory;
}

and then

// cerebral-react-baobab package
var controllerFactory = require('cerebral-baobab');
var attachView = require('cerebral-react');
module.exports = attachView(controllerFactory);

Throw errors on mutation methods in the async state object

You might be confused by the state object not having any mutation methods, so we could still have them, but throw errors instead.

The reason you can not mutate state there is because async actions are not run when remembering state. Only sync actions. But async actions outputs are remembered. This makes Cerebral able to replay signals synchronously, which is really important :-)

New Feature: "Top Level Array"

To allow easy composition of signals, we will make the top level an array.

const $actionGroup = [
  action1,
  action2,
  [action3]
];

controller.signal('somethingHappened', [specialAction].concat($actionGroup));
controller.signal('somethingElseHappened', $actionGroup);

This requires breaking API change though, as the top level will need to be an array to support:

controller.signal('somethingHappened', [ 
  [asyncAction] 
]);

controller.signal('somethingHappened', [
  syncAction
].concat(otherActions));

New Feature Suggestion: Split packages and custom mutation methods

Hi guys!

I just got to say big thanks for fantastic feedback on Cerebral. The latest big contribution was the idea of adding outputs and type checking, by @a-s-o. This makes Cerebral able to understand even more about your flow and help you keep control of how signals run, especially on larger projects and with teams. Also hope the new "output" indication in the debugger is helpful :-)

There are two new great ideas I want to relay here:
#1. Split Cerebral packages into two types

By: @gcanti

Now we have packages like cerebral-react-immutable-store, cerebral-angular-immutable-store and cerebral-react-baobab. What if we had: cerebral-react, cerebral-angular, cerebral-immutable-store and cerebral-baobab?

This would allow us to reuse packages and you would make a more conscious choice on choosing what view layer to use and what model layer to use.

This also leads into the next suggestion.
#2. Custom state mutation methods

By: @pstoica

Baobab and Immutable-Store has very similar APIs, but Immutable-JS is quite different. When thinking of the previous example it would be more natural to make mutations pr. model-layer-package. So in the documentation of each model-layer package it would describe what mutation methods are available to you. That way Cerebral can take the full power of the model-layer and you actually work with the model-layer you choose, not Cerebral default mutations which can feel limiting if you want to use immutable-js for example.

So current packages will keep their current mutation methods, but when using cerebral-immutable-js you might use additional/different methods.

UPDATE: A revised suggestion is to make the implementation of creating mutation hooks very flexible, but by convention the official Cerebral Model Packages will share the same API (set, push, pop etc.). That allows others to create custom controllers with any state mutation methods for their own project or to share with others.

Some syntax suggestions

import Controller from 'cerebral';
import Model from 'cerebral-baobab';
import View from 'cerebral-react';

const state = {...};

const defaultInput = {...};

const controller = Controller({
  view: View,
  model: Model,
  state: state,
  defaultInput: defaultInput
});

export default controller;

So basically when the controller instantiates itself there has to be a common way to connect this stuff together. Needs some more thought, but just wanted to start somewhere.

What are the thoughts on this?

Optional localStorage / debugger

Would be nice to be able to disable use of localStorage for persisting the cerebral state, and also to toggle the debugger, without flipping NODE_ENV to production.

Perhaps something like:

let cerebral = Cerebral(...);
cerebral.persistence = false;

cerebral.debugger(false);

I'd like to add a save button to my debug UI, so I can choose when to persist the cerebral state to localStorage, so a "save now" method on the cerebral would be useful.

Should Cerebral be independent of React?

Cerebral does not really need React, should it be standalone?

Would have to create strategies for injecting the debugger into the different frameworks or create a generic JavaScript version of it.

Type Checking On Actions Fails on False Values

First off, thank you so much for the new type checker for actions. It really does work wonders for productivity. It seems like when I pass a false-y value in, it says I am giving the wrong input. By that I mean "" or 0, even when it is the right type.

Composing state from already composed state

Hey Christian,

I had one main question and a couple smaller ones.

Is it possible for a composed state function to listen to another composed state lookupState. For example, is the following possible?:

import Cerebral from 'cerebral';

let visibleTodosThatStartWithTheLetterA = function () {
  return {
    initialState: [],
    lookupState: ['visibleTodos'],
    get(cerebral, lookupState, refs) {
      return lookupState.visibleTodos.filter(function (visibleTodo) {
        if (visibleTodo starts with A) { //pseudocode
          return true
        }
      })
    }
  };
};

let visibleTodos = function () {
  return {
    initialState: [],
    lookupState: ['todos'],
    get(cerebral, lookupState, refs) {
      return refs.map(function (ref) {
        return lookupState.todos[ref];
      });
    }
  };
};

let cerebral = Cerebral({
  todos: {},
  visibleTodos: visibleTodos,
  visibleTodosThatStartWithTheLetterA: visibleTodosThatStartWithTheLetterA,
  count: 0,
  newTodoTitle: ''
});

Also, what determines what's inside the refs object that is being passed to the get() function? Is it based on what's in the lookupState?

Finally, say your cerebral was nested:
let cerebral = Cerebral({
todos: {
urgentTodos: [],
regularTodos
},
visibleUrgentTodos = visibleUrgentTodos
});

Would this be the proper syntax for accessing the 'urgentTodos' property, where the lookupState is an object?

let visibleUrgentTodos = function() {
  return {
    initialState: [],
    lookupState: {
      urgentTodos: ['todos', 'urgentTodos']
    },
    get(cerebral, lookupState, refs) {
      return refs.map(function(ref) {
        return lookupState.urgentTodos[ref];
      });
    }
  };
};

Thanks a bunch!

Controlling flow of exceptions

Hello Christian,

I have just started using cerebral and I am trying to implement type validation on action inputs and outputs. I am running into some issues with the flow of errors. I am using a helper method t.action (see http://gist.github.com/a-s-o/e35edddf8d61c63e88d9) to create the actions as follows:

   const resolveRoute = t.action({
      inputs: {
         route: t.Str
      },
      resolves: {
         pageTitle: t.Str
      },
      rejects: {
         error: t.Str
      },
      fn: function resolveRoute (args, state, promise) {
         promise.resolve({
            pageTitle: 3      // Let's introduce a bug here; instead of
                              // resolving with a page title string, we will
                              // resolve with a number which gets caught
                              // by the type system
         });
      }
   });

   const setTitle = t.action({
      inputs: {
         pageTitle: t.Str
      },
      fn: function setTitle (args, state) {
         state.set('pageTitle', t.Str(args.pageTitle));
      }
   });

   const displayError = t.action({
      inputs: {
         error: t.Str
      },
      fn: function displayError (args, state) {
         console.log(args);
         state.set('error', args.error);
      }
   });

The signal is defined as follows:

   cerebral.signal('routeChanged', [resolveRoute, {
      resolve: [setTitle],
      reject: [displayError]
   }]);

As a result of the bug in in the resolveRoute action, I get the following errors in the console:

[resolveRoute:resolves] /pageTitle is 3 should be a Str
[setTitle:inputs] /pageTitle is 3 should be a Str
[displayError:inputs] /error is undefined should be a Str

This is great and exactly what I expect but if instead of console.logging the errors from the validator, I throw the validation errors, the only error that gets caught is:

Uncaught (in promise) TypeError: [displayError:inputs] /error is undefined should be a Str
My question is whether there is a way to get access to those exceptions that occur along the way in action handlers?

I suspect this has to do with the way thrown exceptions work in promises but I would expect to see the original [resolveRoute:resolves] /pageTitle is 3 should be a Str. Not having the original error is causing some grief finding where the error started in long chains.

Incorrect path passed to pop and shift hooks

I think is due to this line:

https://github.com/christianalfoni/cerebral/blob/master/src/createActionArgs.js#L13

// args can be (path, value), ([path], value), (value)

It also can be (path) in case of pop / shift

Test case:

var cerebral = require('cerebral');

var controller = cerebral.Controller({
  onPop: function (path) {
    console.log(path); // should output ['a', 'b'] but it outputs [] instead
  },
  onShift: function (path) {
    console.log(path); // should output ['c', 'd'] but it outputs [] instead
  }
});

controller.signal('test', function (input, state, output) {
  state.pop(['a', 'b']);
  state.shift(['c', 'd']);
});

controller.signals.test();

Creating custom state mutation hooks

ImmutableJS has a ton of different data structures which I find pretty useful (Set, OrderedMap, etc). Each data structure has its own special methods.

Does the current Cerebral API support custom state mutators? It seems hardcoded right now: https://github.com/christianalfoni/cerebral/blob/5483fb634b70bdf8833bbda00c024462d7cfa9ef/src/createActionArgs.js#L34

I want to start making an adapter and I think this would be great to have. But I can totally understand if you don't want to fragment the API across packages (write once and keep things swappable?).

Redefined feature: "Creating actions" (conditionals)

Okay, so we got some great input from @a-s-o. Inspired by node-machines. The good thing here is that we keep the really simple syntax that we have now. We rather add a way to define custom paths and do type checking on the actions. I think this will be a life saver in larger projects and especially with 2+ teams ;-)

It is really important not to get fooled by the seemingly complexity here. Signals will work exactly the same, but you get added functionality for custom and typed actions.

Default sync/async action

const noPathAction = function (args, state, next) {
  next({foo: 'bar'}); // Always next, no return, to keep consistency
};

const defaultPathsAction = function (args, state, next) {
  if (conditional) {
    next.success({foo: 'bar'});
  } else {
    next.error({foo: 'bar'});
  }
};

// sync
controller.signal('appMounted', noPathAction);
controller.signal('appMounted', defaultPathsAction, {
  success: [],
  error: []
});

// async
controller.signal('appMounted', [noPathAction]);
controller.signal('appMounted', [defaultPathsAction, {
  success: [],
  error: []
}]);

So "success" and "error" are suggested default paths. Current "resolve" and "reject" should be changed. The reason is to avoid confusion with a promise. Yes, there is an underlying promise on async actions, but the next method chooses a path, it does not reject the actual promise. Any actual errors in the code (thrown errors) will be a rejected promise and will be thrown to console like normal and the signal will stop.

Custom paths

const customPathsAction = function (args, state, next) {
  if (conditional) {
    next.foo({foo: 'bar'});
    next({foo: 'bar}); // Default to foo
  } else {
    next.bar({foo: 'bar'});
  }
};
customPathsAction.defaultOutput = 'foo';
customPathsAction.outputs = ['foo', 'bar'];

// sync
controller.signal('appMounted', customPathsAction, {
  foo: [],
  bar: []
});

// async
controller.signal('appMounted', [customPathsAction, {
  foo: [],
  bar: []
}]);

Type checked actions

import {Types} from 'cerebral-some-package';

const noPathTypedAction = function (args, state, next) {
    next({foo: 'bar'})
};
noPathTypedAction.input = {
    foo: Types.String
};
noPathTypedAction.output = {
    foo: Types.String
};

const defaultPathsTypedAction = function (args, state, next) {
  if (conditional) {
    next.success({foo: 'bar'});
  } else {
    next.error({foo: 'bar'});
  }
};
defaultPathsTypedAction.input = {
    foo: Types.String,
    bar: Types.Bool
};
defaultPathsTypedAction.outputs = {
    success: {
      foo: Types.String
    },
    error: {
      foo: Types.String
    }
};

const customPathsTypedAction = function (args, state, next) {
  if (conditional) {
    next.foo({foo: 'bar'});
  } else {
    next.bar({foo: 'bar'});
  }
};
customPathsTypedAction.input = {
    foo: Types.String
};
customPathsTypedAction.outputs = {
    foo: {
      foo: Types.String
    },
    bar: {
      foo: Types.String
    }
};

Okay, so the thing here is that you commonly get away with default sync and async actions. You might need to reach out for a custom action to define some custom paths, but as you can see the definition is really simple.

In bigger projects and more people working on the project you can bring it up a notch, bringing some type checking into the mix. This will give errors when you refresh the page (compile time) as the signal can be analyzed and verified even before it runs.

PS!
There is not need for top level array, we can use ES6 spread

controller.signal('somethingHappened', syncAction, ...otherActions );
controller.signal('somethingHappened', [asyncAction], ...otherActions );

cerebral on server side

Hey Christian,
thank you for developing cerebral - i like it.
Is it possible to implement it on server side for isomorphic app's?
I've tried different ways, but all without good results.

Question: triggering signals from within signal handlers?

I'm experimenting with cerebral, and have a maybe quite weird use-case. I'm writing a validator for an external data-format parts of our system consume (VAST), and have sort of the following flow in mind:

  • Choose "From URL" or "From XML" (as in: input raw XML)
  • Choose whether to proxy or not
  • Enter either URL or XML (in Codemirror)
  • Click "Start Validation"
  • Fetch remote URL or take XML from input

At this point it gets a bit murky for me. The response (a quite deeply nested object with arrays 'n such) needs to go through the code we use for processing in our client. The function basically behaves like so:

this.VASTClient.get(url, (response) => {
  // traverse the response object with all sorts of ugly loops. not my code :/
});

What I want to do, is if we get a valid response, walk through this logic, but instead of taking the actions our normal client would take, push signals onto cerebral (cerebral.signals.logMessagePushed(node)). Can I just call cerebral.signals.[signalName] from another signal handler? What's your advice here?

My idea was (slight pseudocode):

cerebral.signal('onValidationStarted', getVast, setVast, traverseResponse);

where traverseResponse behaves sort of like:

export default function traverseResponse(cerebral, vastResponse){
   vastResponse.ads.forEach((ad) => {
       cerebral.signals.messageLogged('Entered adNode', ad);
   });
};

play button and export/import for debugger

I think it'd be kind of neat to have some kind of play button in the debugger. I haven't had a look at the source yet, but for this to work, I think it'd need the time stamping of all state changes so they could be replayed at the same distance from each other.

Also, it'd be neat if you could export/import a session for later replay.

Lacking documentation and simple examples

First, thank you for publishing your work!, but here comes some constructive criticism from a beginner:

There is no clear way to begin a simple application, the only boilerplate project i found is outdated and serves to confuse some concepts even more.

There is no sequence in the readme to follow that can make you have a working simple cerebral app. It starts showing a bunch of Cerebral packages, it does not explain their purpose (i figured it to be an adapter to a store that also has some bootstrapping code, none explained). by trying to replicate the code in cerebral-react-immutable-store, on a new simple app only throws errors (using Decorator Syntax).

The video just glosses over some concepts and does not explain how to build a simple cerebral app, it explains some concepts of cerebral that can be grasped but other stuff remains hidden.

The demo on cerebral of todo-mvc has no explanations or pertinent comments, it bootstraps a custom package in a way not mentioned anywhere (using a wrapper class with childContexTypes and getChildContext) ,and that reminds me there is no api documentation, it also uses a StateComponent thing, that i presume has to do with the record function, its neat, but this further obscures what is needed to create a cerebral app.

I am sold on the concept, but a simple boilerplate for cerebral (with a custom package) and other boilerplates for cerebral-react-immutable-store, would help tremendously, preferably explaining all the details.

For some source code is documentation enough, i am unfortunately, i bit more limited.
I will try to read all over this again tomorrow, perhaps i will wrap my head around it eventually, but for now i cant help but be somewhat confused.

More explicit names

I don't know whether this is bike shedding or not, but here I go. ๐Ÿ˜‰ I know this is really a matter of taste and I tend to use long names for everything, but I think it makes everything clearer and with code completion and snippets I think long names are a none issue. I was reading through the introductory blog post and noticed a few things:

let projectRows = function () {
  return {
    // if this value is changing, it should be obvious by the name, as with React components
    initialValue: [],

    // I'm torn on this one. it should be the same for both the mixin and here, so if it's
    // `getCerebralState` in the mixin, it I think should also be `state` here or `dependencies`
    // and then also `getCerebralDependencies`. Or maybe `dependsOn`? In any case I really don't
    // shortened words, as the way people shorten things differs from project to project and you (well, I)
    // never get it right.
    dependsOn: ['projects'],
    get(cerebral, states, ids) {
      return ids.map(id => states.projects[id]);
    }
  };
};

Dependencies is a really long word, but it describes it very well ... hmm, maybe there is an alternative lurking somewhere? But the more I think about it, I think it should be state everywhere.

HOC alternative to the mixin

Internally it could also reuse the mixin. It could look something like this:

function Cerebralize(Component, cerebralState) {
  return React.createClass({
    mixin : [ cerebralMixin ],
    displayName : `Cerebralized${Component.name}`,
    getCerebralState() {
      return cerebralState;
    },
    render() {
      return <Component signals={this.signals} {...this.props} {...this.state}>;
    }
  });
};
// ES5
var MyComponent = React.createClass({ ... });
MyComponent = Cerebralize(MyComponent, [ ... ]);
// ES6
class MyComponent extends React.Component { }
MyComponent = Cerebralize(MyComponent, [ ... ]);
// ES7
@Cerebralize([ ... ])
class MyComponent extends React.Component { }

Data fetching across multiple facets

Hey @christianalfoni,

Cerebral looks fantastic! I don't have to learn Elm after all :)

A question that I raised on your blog site about data fetching inside facets is relevant to this library so I thought I should post it here.

What happens when you have multiple facets that rely on the same data path eg. users - where would you put put the signal call without having to duplicate code throughout your facets?

cerebral.facet('visibleTodos', ['todos', 'authors'], function (cerebral, uuid) {
   let todo = cerebral.get('todos')[uuid].toJS();
    todo.author = cerebral.get('authors')[todo.authorId];

    if (!todo.author) {
      todo.author = {
        id: todo.authorId,
        $isLoading: true
      };
      cerebral.signals.missingAuthor(todo.authorId);
    }

})

cerebral.facet('visibleNotes', ['notes', 'authors'], function (cerebral, uuid) {
    let note = cerebral.get('notes')[uuid].toJS();
    note.author = cerebral.get('authors')[todo.authorId];

    if (!notes.author) {
      notes.author = {
        id: notes.authorId,
        $isLoading: true
      };
      cerebral.signals.missingAuthor(notes.authorId);
    }

})

It would be nice if the data fetching was executed when the actual raw data is being accessed. In this case it is authors path.

I havn't thought this though completely, but what if we could do something like this:

// first argument is data which is returned from the get method.
// the rest of the arguments are the paths you pass to get method.
cerebral.watch('authors', function(data [, uuid, path2...]) {

  if (!data) {
    data = {
      id: uuid, 
      $isLoading: true
    };
    cerebral.signals.missingAuthor(uuid);
  }

  return data
})


// and this is how you would use it inside the facet or wherever
var author = cerebral.get('authors', uuid)

TodoMVC Demo App: Inccorect text insert behavior

Hi Christian,

I think there's a bug in your TodoMVC demo app.

Here are the steps to reproduce this bug.

  1. Open a web browser and navigate to the TodoMVC app, http://www.christianalfoni.com/todomvc
  2. In the text field, enter some text, for example: my task
  3. Place the caret in the middle of the text entered above, e.g. after my
  4. Type some more text, e.g. new
  5. The new text will not be inserted correctly in the text box. Instead of the expected text my new task, I received my ntaskew

Any solution of how to fix this?

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.