ok i have a basic vdom implementation in and lets see if i can write a render function that works
// mount the vdom on to the dom and
// set up the runtime from sources and
// patch the vdom
// ---
// returns an unsubscribe method you can use to unmount
export function mount(element, container) {
const source$ = constructStream(element)
const rootNode = render(element)
container.appendChild(rootNode)
return scan(
source$,
(prevDOM, next) => {
const vDOM = render(element, next)
patch(rootNode, diff(prevDOM, vDOM))
return vDOM
},
rootNode
).subscribe()
}
constructStream:
// traverse all children and collect a stream of all sources
// this means you cant hot swap streams for now which is impt for a real app
function constructStream(rootEl) {
// this is the first ping of data throughout the app
let source$ = Observable.of(INITIALSOURCE)
traverse(source$, source => {
// visit each source and merge with source$
if (source) return source$ = merge(source, source$)
})(rootEl)
return source$
/* alternate approach abandoned for now */
// // this is the first ping of data throughout the app
// let data$ = Observable.of(INITIALSOURCE)
// // start with null view
// let view$ = Observable.of(null)
// return {data$, view$}
}
function traverse(source$, addToStream) {
return function traverseWithStream(element){
const { type, props } = element;
const isDomElement = typeof type === "string";
if (isDomElement) {
// updateDomProperties(dom, [], props); // TODO: add event listeners
const childElements = props.children || [];
childElements.map(traverseWithStream); // recursion
} else {
// Instantiate component element
const instance = {};
const publicInstance = createPublicInstance(element, instance);
addToStream(publicInstance.source(source$));
const childElement = publicInstance.render();
traverseWithStream(childElement);
}
}
}
now i just need to render a bunch of h’s
ended up dipping into vtext and vnode instead of using hyperscript but it workd!
Hello world!
class App extends Component {
source($) {
return scan(
Ticker(), // tick every second
x => x+1, // count up
0 // from zero
)
}
render(state, prevState) {
const elapsed = state === INITIALSOURCE ? 0 : state
return <div> number of seconds elapsed: {elapsed} </div>
}
}
mount(<App />, document.getElementById('app'))
this produces a steadily ticking counter next to hello world!
6pm ok so looking at last nights todo list:
- make every dom el a source: not done yet but doable under my static stream restriction
- figure out if sinks matter? maybe
- render takes observable? not sure what this mmeans haha
now my new wishlist:
- local stream state based on the instance
- trying to show the rough edges based on this new thingy…
3am long detour but i took in jafar husain’s awesome rxjs course on FEM and also explored recompose and reasonreact from egghead. i feel very close to something here.
Reason react looks like this:
type state = {count: int};
type action =
| Increment
| Decrement;
let component = ReasonReact.reducerComponent("Counter");
let string = ReasonReact.stringToElement;
let make = _children => {
...component,
initialState: () => {count: 0},
reducer: (action, state) =>
switch action {
| Increment => ReasonReact.Update({count: state.count + 1})
| Decrement => ReasonReact.Update({count: state.count - 1})
},
render: ({send, state}) => {
let msg = "Current Count: " ++ string_of_int(state.count);
<div>
<h2> (string(msg)) </h2>
<button onClick=(_event => send(Increment))> (string("+")) </button>
<button onClick=(_event => send(Decrement))> (string("-")) </button>
</div>;
}
};
so having the send
function is a pretty good idea i should steal.
theres an alternative using self.handle
…
let handleClick = (_event, _self) => Js.log("click!");
let make = (~message, _children) => {
...component,
render: self =>
<div onClick=(self.handle(handleClick))>
(ReasonReact.stringToElement(message))
</div>
};
i can adapt this idea.
recompose
also has a nice section: https://github.com/acdlite/recompose/blob/v0.26.0/docs/API.md#componentfromstream
with a createEventHandler
i should probably use:
const Counter = componentFromStream(props$ => {
const { handler: increment, stream: increment$ } = createEventHandler()
const { handler: decrement, stream: decrement$ } = createEventHandler()
const count$ = Observable.merge(
increment$.mapTo(1),
decrement$.mapTo(-1)
)
.startWith(0)
.scan((count, n) => count + n, 0)
return props$.combineLatest(
count$,
(props, count) =>
<div {...props}>
Count: {count}
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
})
4am i took componentFromStream and adapted it for my use now to implement:
class Counter extends Component {
constructor() {
this.$ = {
increment: createEventHandler(),
decrement: createEventHandler()
}
}
source($) {
const {increment, decrement} = this.$
const state$ = scan(
startWith(
merge(
mapToConstant(increment.$, 1),
mapToConstant(decrement.$, -1)
),
0
),
(acc, n) => acc + n, 0 // scan args
)
return state$
}
render(state) {
const {increment, decrement} = this.$
return <div {...props}>
Count: {state}
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
}
}
4.15 ok made a basic createHandler()
export function createHandler() {
const emitter = createChangeEmitter()
let handler = emitter.emit
handler.$ = new Observable(observer => {
return emitter.listen(value => observer.next(value))
})
return handler
}
now to have persistent instances
6.40am woohoo i have a working counter app!
class App extends Component {
increment = createHandler(() => 1)
decrement = createHandler(() => -1)
source($) {
const source$ = merge(this.increment.$, this.decrement.$)
const reducer = (acc, n) => acc + n
return scan(startWith(source$, 0), reducer, 0)
}
render(state) {
return <div>
Count: {state}
<button onclick={this.increment}>+</button>
<button onclick={this.decrement}>-</button>
</div>
}
}
i would have cracked persistent isntances much earlier but i lost like an hour to a bug due to shadowing of an instance
variable .
ok so now i need to make my stream into a stream of streams so that my state can be local. idk if that is overengineering already.