Skip to content

Instantly share code, notes, and snippets.

@ricokahler
Created September 15, 2016 01:08
Show Gist options
  • 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

(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