0

just trying come silly stuff and playing around with Cycle.js. and running into problem. Basically I just have a button. When you click it it's suppose to navigate the location to a random hash and display it. Almost like a stupid router w/o predefined routes. Ie. routes are dynamic. Again this isn't anything practical I am just messing with some stuff and trying to learn Cycle.js. But the code below crashes after I click "Add" button. However the location is updated. If I actually just navigate to "#/asdf" it displays the correct content with "Hash: #/asdf". Not sure why the flow is crashing with error:

render-dom.js:242 TypeError: Cannot read property 'subscribe' of undefined(…)

import Rx from 'rx';
import Cycle from '@cycle/core';
import { div, p, button, makeDOMDriver } from '@cycle/dom';
import { createHashHistory } from 'history';
import ranomdstring from 'randomstring';

const history = createHashHistory({ queryKey: false });

function CreateButton({ DOM }) {
  const create$ = DOM.select('.create-button').events('click')
    .map(() => {
      return ranomdstring.generate(10);
    }).startWith(null);

  const vtree$ = create$.map(rs => rs ?
    history.push(`/${rs}`) :
    button('.create-button .btn .btn-default', 'Add')
  );

  return { DOM: vtree$ };
}

function main(sources) {
  const hash = location.hash;
  const DOM = sources.DOM;

  const vtree$ = hash ?
    Rx.Observable.of(
      div([
        p(`Hash: ${hash}`)
      ])
    ) :
    CreateButton({ DOM }).DOM;

  return {
    DOM: vtree$
  };
}

Cycle.run(main, {
  DOM: makeDOMDriver('#main-container')
});

Thank you for the help

Bojan
  • 137
  • 6

2 Answers2

5

I would further suggest using @cycle/history to do your route changing (Only showing relevant parts)

import {makeHistoryDriver} from '@cycle/history'
import {createHashHistory} from 'history'

function main(sources) {
  ...
  return {history: Rx.Observable.just('/some/route') } // a stream of urls
}

const history = createHashHistory({ queryKey: false })
Cycle.run(main, {
  DOM: makeDOMDriver('#main-container'),
  history: makeHistoryDriver(history),
})
TylorS
  • 139
  • 7
  • Hello, thanks for the feedback. Sorry I feel really dumb, but I couldn't get it working with @cycle/history; I tried different things and read the documentation. How do I connect the history driver with the button's change events? Am I suppose to combine the two streams? – Bojan Mar 29 '16 at 01:48
  • Hmm I think I finally figured it out thanks for the help. – Bojan Mar 29 '16 at 12:42
2

On your function CreateButton you are mapping your clicks to history.push() instead of mapping it to a vtree which causes the error:

function CreateButton({ DOM }) {
  ...
  const vtree$ = create$.map(rs => rs
    ? history.push(`/${rs}`) // <-- not a vtree
    : button('.create-button .btn .btn-default', 'Add')
  );
  ...
}

Instead you could use the do operator to perform the hashchange:

function CreateButton({ DOM }) {
  const create$ = 
    ...
    .do(history.push(`/${rs}`)); // <-- here

  const vtree$ = Observable.of(
    button('.create-button .btn .btn-default', 'Add')
  );
  ...
}

However in functional programming you should not perform side effects on you app logic, every function must remain pure. Instead, all side effects should be handled by drivers. To learn more take a look at the drivers section on Cycle's documentation

To see a working driver jump at the end of the message.


Moreover on your main function you were not using streams to render your vtree. It would have not been reactive to locationHash changes because vtree$ = hash ? ... : ... is only evaluated once on app bootstrapping (when the main function is evaluated and "wires" every streams together).

An improvement will be to declare your main's vtree$ as following while keeping the same logic:

const vtree$ = hash$.map((hash) => hash ? ... : ...)

Here is a complete solution with a small locationHash driver:

import Rx from 'rx';
import Cycle from '@cycle/core';
import { div, p, button, makeDOMDriver } from '@cycle/dom';
import { createHashHistory } from 'history';
import randomstring from 'randomstring';


function makeLocationHashDriver (params) {
  const history = createHashHistory(params);

  return (routeChange$) => {
    routeChange$
      .filter(hash => {
        const currentHash = location.hash.replace(/^#?\//g, '')
        return hash && hash !== currentHash
      })
      .subscribe(hash => history.push(`/${hash}`));

    return Rx.Observable.fromEvent(window, 'hashchange')
      .startWith({})
      .map(_ => location.hash);
  }
}

function CreateButton({ DOM }) {
  const create$ = DOM.select('.create-button').events('click')
    .map(() => randomstring.generate(10))
    .startWith(null);

  const vtree$ = Rx.Observable.of(
    button('.create-button .btn .btn-default', 'Add')
  );

  return { DOM: vtree$, routeChange$: create$ };
}

function main({ DOM, hash }) {
  const button = CreateButton({ DOM })
  const vtree$ = hash.map(hash => hash
    ? Rx.Observable.of(
        div([
          p(`Hash: ${hash}`)
        ])
      )
    : button.DOM
  )

  return {
    DOM: vtree$,
    hash: button.routeChange$
  };
}

Cycle.run(main, {
  DOM: makeDOMDriver('#main-container'),
  hash: makeLocationHashDriver({ queryKey: false })
});

PS: there is a typo in your randomstring function name, I fixed it in my example.

Fuunnx
  • 61
  • 5