Skip to content

Instantly share code, notes, and snippets.

@ricokahler
Created September 15, 2016 01:08
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ricokahler/9debb8f120e95bea568521b3b082a8c8 to your computer and use it in GitHub Desktop.
Save ricokahler/9debb8f120e95bea568521b3b082a8c8 to your computer and use it in GitHub Desktop.
Structured Component for Cycle
/**
* Component module to enforce model update view
*/
import xs, {MemoryStream, Stream} from 'xstream';
import {DOMSource, VNode} from '@cycle/dom';
import {Record} from 'immutable';
export interface ComponentSource { DOM: DOMSource }
export interface Updater<Value, Model> {
from$: Stream<Value>,
by: (model: Model, value: Value) => Model
}
export interface ComponentSink {
model$: MemoryStream<any>,
DOM: MemoryStream<VNode>,
components: {
[key: string]: ComponentSink
}
};
export interface ComponentOptions<Model, Components> {
components?: {
[key: string]: ComponentSink
},
model?: Model,
update?: {
[key: string]: Updater<any, Model>
},
view: (model: Model, components: any) => VNode,
}
interface EventType<T> {
name: string
}
export const on = {
click: { name: 'click' } as EventType<MouseEvent>,
input: { name: 'input' } as EventType<UIEvent>,
submit: { name: 'submit' } as EventType<UIEvent>,
checkboxStateChange: { name: 'CheckboxStateChange' } as EventType<UIEvent>
}
export const to = {
value: (event: any) => event.target.value,
object: (event: any) => {
event.preventDefault();
const inputs = Array.prototype.slice.call(
event.target.querySelectorAll('input')
).filter((elm: any) => elm.name) as HTMLInputElement[];
const keyValues = inputs.map(elm => ({name: elm.name, value: elm.value}));
const inputsObject = keyValues.reduce((inputsObject: any, nameValuePair: any) => {
inputsObject[nameValuePair.name] = nameValuePair.value;
return inputsObject;
}, {});
return inputsObject;
}
}
export const domSelector = (sources: ComponentSource) => (
function dom<T> (
selector: string,
event: EventType<T>,
mapper?: (event: T) => any
) {
return sources.DOM.select(
selector
).events(
event.name
).map(
mapper || ((e: any) => e)
);
}
)
export default function Component<Model, Components> (
options: ComponentOptions<Model, Components>
) {
const { model, update, view } = options;
// get the components objects as a list of component tuples
const componentNames = /*if*/ options.components ? (
Object.keys(options.components)
) : ([]);
const components = componentNames.map(name => ({
name,
dom$: (options.components[name].DOM as xs<VNode>)
}));
const ComponentsRecord = Record((function () {
let emptyComponents: any = {};
componentNames.forEach(name => emptyComponents[name] = null);
return emptyComponents;
}()));
/**
* state record is an immutable type to hold the
* model state and the components view tree.
* these all get stored into one StateRecord.
*/
const ModelRecord = Record({
thisComponent: model, components: new ComponentsRecord()
});
const component$s = components.map(({name, dom$}) => (
dom$.map(dom => (
(model: Immutable.Map<string, any>) => model.setIn(
['components', name],
dom
)
))
));
const updaters = Object.keys(update || []).map(key => update[key]);
const updater$s = updaters.map(updater => {
const { from$, by } = updater;
const updater$s = from$.map(value => (model: Model) => by(model, value));
return updater$s.map(
updater => (
model: Immutable.Map<string, any>
) => model.update('thisComponent',updater)
);
});
const model$ = xs.merge(...updater$s, ...component$s).fold(
(state, update) => update(state),
new ModelRecord()
);
const view$ = model$.map(model => {
const componentArray = componentNames.map(
name => ({
name,
dom: model.getIn(['components', name])
})
);
const components = (function () {
let components: any = {};
componentArray.forEach(({name, dom}) => {
components[name] = dom;
});
return components;
}());
return view(model.get('thisComponent'), components);
});
return {
model$: model$.map(model => model.get('thisComponent')),
DOM: view$,
components: options.components
};
}
import Component, {ComponentSource, domSelector, on, to} from '../Component';
import { div, h1, input, DOMSource } from '@cycle/dom';
export default function HelloComponent(sources: {
DOM: DOMSource
}) {
const dom = domSelector(sources);
return Component({
model: 'World!',
update: {
onInput: {
from$: dom(`.hello`, on.input, to.value),
by: (model, value) => value
}
},
view: (name) => div([
h1([`Hello, ${name}`]),
input(`.hello`, {attrs: {type: 'text', value: name}}),
])
});
}
import xs from 'xstream';
import {DOMSource, makeDOMDriver, div, h1, input, hr} from '@cycle/dom';
import {run} from '@cycle/xstream-run';
import Component from './Component';
import Hello from './components/Hello';
import {domSelector, on, to} from './Component';
import {Record} from 'immutable';
function main(sources: any) {
const dom = domSelector(sources);
const ModelRecord = Record({
clicks: 0,
message: ''
})
const helloNestedComponent = Hello(sources);
const component = Component({
components: { helloNestedComponent },
model: new ModelRecord(),
update: {
onClick: {
from$: dom('body', on.click),
by: model => model.update('clicks', x => x + 1)
},
onInput: {
from$: dom('.message', on.input, to.value),
by: (model, message) => model.set('message', message)
}
},
view: (model, { helloNestedComponent }) => div([
h1(['clicks: ' + model.get('clicks')]),
h1([`message: ${model.get('message')}`]),
input('.message', {attrs: {type: 'text', value: model.get('message')}}),
hr(),
helloNestedComponent
])
});
return {
DOM: component.DOM
}
}
run(main, {
DOM: makeDOMDriver('#will-i-pass')
});
@ricokahler
Copy link
Author

i was playing around with cycle and i wanted to come up with a declarative api the enforces the elm architecture. i have a few problems with my own code. The biggest thing I see wrong is that I only save the view stream when nesting components together and this doesn't really save all the state in one store like redux which can lead to inconsistency when trying to implement things like undo/redo. i'll play with this more :)

@ricokahler
Copy link
Author

(Unfortunately) what I was trying with this Component module didn't really go along with the ideas of cycle so i'm now attempting to make my own frontend framework 😓 . https://github.com/ricokahler/harmony

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment