Comparing version
@@ -13,5 +13,4 @@ { | ||
"typescript": "1.8.7", | ||
"dompteuse": "0.3.5", | ||
"dompteuse": "0.4.0", | ||
"snabbdom": "0.4.2", | ||
"most": "0.19.6", | ||
"abyssa": "7.2.7", | ||
@@ -18,0 +17,0 @@ "immupdate": "0.2.0", |
import { api as router } from 'abyssa' | ||
import { Component, h, ConnectParams } from 'dompteuse' | ||
import { Stream } from 'most' | ||
import appState, { incrementBlue } from './appState' | ||
import appStore, { IncrementBlue } from './appStore' | ||
import { contentAnimation } from './util/animation' | ||
@@ -11,5 +10,5 @@ import index from './index' | ||
export default Component({ | ||
export default Component<void, State>({ | ||
key: 'app', | ||
initState: readGlobalState, | ||
initState, | ||
connect, | ||
@@ -26,11 +25,17 @@ render | ||
return { | ||
count: appState.value.blue.count, | ||
route: appState.value.route.fullName | ||
count: appStore.state().blue.count, | ||
route: appStore.state().route.fullName | ||
} | ||
} | ||
function initState() { | ||
return readGlobalState() | ||
} | ||
function connect({ on }: ConnectParams<void, State>) { | ||
on(appState, readGlobalState) | ||
on(appStore.state, readGlobalState) | ||
} | ||
function render(props: void, state: State) { | ||
@@ -52,2 +57,2 @@ return h('div', [ | ||
//setInterval(appState.send.bind(null, incrementBlue()), 2500) | ||
//setInterval(appStore.send.bind(null, IncrementBlue()), 2500) |
import { api as router } from 'abyssa' | ||
import { Component, h, ConnectParams, Message, Messages } from 'dompteuse' | ||
import * as most from 'most' | ||
import { Stream } from 'most' | ||
import { Component, h, ConnectParams, Message, Messages, Observable } from 'dompteuse' | ||
import mergeObs from 'dompteuse/lib/observable/merge' | ||
import { contentAnimation } from './util/animation' | ||
import green from './green' | ||
import appState, { incrementBlue } from './appState' | ||
import appStore, { AppState, IncrementBlue } from './appStore' | ||
import { merge } from './util/obj' | ||
import select from './util/select' | ||
import observeAjax from './util/ajax' | ||
import * as promise from './util/promise' | ||
export default function() { | ||
return Component({ | ||
key: 'blue', | ||
initState, | ||
connect, | ||
render | ||
}) | ||
return Component<void, State>({ key: 'blue', initState, connect, render }) | ||
} | ||
const Increment = Message('increment') | ||
const UserChange = Message<string>('userChange') | ||
const RefreshSelect = Message('refreshSelectList') | ||
interface State { | ||
@@ -38,48 +29,58 @@ count: number | ||
function initState() { | ||
return merge({ users: [], loading: true }, readGlobalState()) | ||
return mergeGlobalState({ | ||
users: [], | ||
loading: false, | ||
selectedUser: undefined | ||
}, appStore.state()) | ||
} | ||
function readGlobalState() { | ||
return { | ||
count: appState.value.blue.count, | ||
route: appState.value.route.fullName, | ||
id: appState.value.route.params['id'] | ||
} | ||
function mergeGlobalState<S>(partialState: S, appState: AppState) { | ||
return merge(partialState, { | ||
count: appState.blue.count, | ||
route: appState.route.fullName, | ||
id: appState.route.params['id'] | ||
}) | ||
} | ||
function connect({ on, messages }: ConnectParams<void, State>) { | ||
on(Increment, _ => appState.send(incrementBlue())) | ||
on(appState, state => merge(state, readGlobalState())) | ||
const Increment = Message('Increment') | ||
const UserChange = Message<string>('UserChange') | ||
const RefreshSelect = Message('RefreshSelect') | ||
function connect({ on, props, msg }: ConnectParams<void, State>) { | ||
on(Increment, _ => appStore.send(IncrementBlue())) | ||
on(appStore.state, mergeGlobalState) | ||
on(UserChange, (state, user) => merge(state, { selectedUser: user })) | ||
const [userData, loading] = getUserData(messages) | ||
on(userData, (state, users) => merge(state, { users })) | ||
on(loading, (state, loading) => merge(state, { loading })) | ||
} | ||
const ajax = observeAjax({ | ||
name: 'Users', | ||
callNow: true, | ||
trigger: msg.listen(RefreshSelect), | ||
ajax: getUserData | ||
}) | ||
function getUserData(messages: Messages): [Stream<string[]>, Stream<boolean>] { | ||
on(ajax.data, (state, users) => merge(state, { users })) | ||
function getSomeUsers() { | ||
interface User { | ||
name: { first: string, last: string } | ||
} | ||
on(ajax.error, (state, err) => merge(state, { users: [] })) | ||
return most.fromPromise(fetch('https://randomuser.me/api/?results=10') | ||
.then(res => res.json()) | ||
.then(json => (json.results as Array<User>).map((user: any) => | ||
`${user.name.first} ${user.name.last}`) | ||
) | ||
).delay(2000) | ||
on(ajax.loading, (state, loading) => merge(state, { loading })) | ||
} | ||
function getUserData() { | ||
interface User { | ||
name: { first: string, last: string } | ||
} | ||
const refreshes = messages.listen(RefreshSelect) | ||
const userData = refreshes.map(getSomeUsers).startWith(getSomeUsers()).switch().multicast() | ||
return promise.delay(2000).then(x => fetch('https://randomuser.me/api/?results=10') | ||
.then(res => res.json()) | ||
.then(json => (json.results as Array<User>).map(user => | ||
`${user.name.first} ${user.name.last}`) | ||
)) | ||
} | ||
const loading = most.merge(refreshes.constant(true), userData.constant(false)) | ||
return [userData, loading] | ||
} | ||
function render(props: void, state: State) { | ||
@@ -86,0 +87,0 @@ const { id, route } = state |
import { Component, h, Message, ConnectParams } from 'dompteuse' | ||
import update from 'immupdate' | ||
import { Stream } from 'most' | ||
import appState from './appState' | ||
import appStore from './appStore' | ||
import { merge } from './util/obj' | ||
@@ -11,12 +10,5 @@ import popup, * as Popup from './util/popup' | ||
export default function() { | ||
return Component({ | ||
key: 'green', | ||
initState, | ||
connect, | ||
render | ||
}) | ||
return Component({ key: 'green', initState, connect, render }) | ||
} | ||
const InputChanged = Message<Event>('inputChanged') | ||
const ShowPopup = Message('showPopup') | ||
@@ -31,3 +23,3 @@ interface State { | ||
return { | ||
id: appState.value.route.params['id'], | ||
id: appStore.state().route.params['id'], | ||
form: {}, | ||
@@ -38,14 +30,17 @@ popupOpened: false | ||
function connect({ on, messages }: ConnectParams<void, State>) { | ||
const formUpdate = messages.listen(InputChanged).map(evt => { | ||
const InputChanged = Message<Event>('InputChanged') | ||
const ShowPopup = Message('ShowPopup') | ||
function connect({ on }: ConnectParams<void, State>) { | ||
on(InputChanged, (state, evt) => { | ||
const { name, value } = evt.target as HTMLInputElement | ||
return { [name]: value.substr(0, 4) } | ||
const formPatch = { [name]: value.substr(0, 4) } | ||
update(state, { form: formPatch }) | ||
}) | ||
on(formUpdate, (state, patch) => | ||
update(state, { form: patch }) | ||
) | ||
on(appState, (state, appState) => | ||
on(appStore.state, (state, appState) => | ||
merge(state, { id: appState.route.params['id'] }) | ||
@@ -58,2 +53,3 @@ ) | ||
function render(props: void, state: State) { | ||
@@ -60,0 +56,0 @@ const { id, form, popupOpened } = state |
import { log } from 'dompteuse' | ||
log.render = log.stream = true | ||
log.render = true | ||
log.message = true |
@@ -8,4 +8,2 @@ import './logger' | ||
declare var require: any | ||
export const snabbdomModules = [ | ||
@@ -12,0 +10,0 @@ require('snabbdom/modules/class'), |
import { h, Component, Vnode, Message, ConnectParams, patch } from 'dompteuse' | ||
import { h, Component, Vnode, Message, NoArgMessage, ConnectParams, patch } from 'dompteuse' | ||
import { TweenLite } from './gsap' | ||
@@ -7,20 +7,12 @@ import { findParentByClass } from './dom' | ||
// Popups are rendered in their own top-level container for clean separation of layers. | ||
let popupContainer = document.getElementById('popups') | ||
export const Close = Message('close') | ||
const OverlayClick = Message<Event>('OverlayClick') | ||
interface Props { | ||
content: Array<Vnode> | ||
onClose: Function | ||
onClose: NoArgMessage | ||
} | ||
export default function(props: Props) { | ||
return Component({ | ||
key: 'popup', | ||
props, | ||
initState, | ||
connect, | ||
render | ||
}) | ||
return Component<Props, void>({ key: 'popup', props, initState, connect, render }) | ||
} | ||
@@ -32,14 +24,21 @@ | ||
export const Close = Message('Close') | ||
const OverlayClick = Message<Event>('OverlayClick') | ||
// Listen for messages inside the popup container, and redispatch at the Popup launcher level. | ||
function connect({ on, props, messages }: ConnectParams<Props, {}>) { | ||
function connect({ on, props, msg }: ConnectParams<Props, void>) { | ||
messages.listenAt('#popups', Close).forEach(_ => | ||
messages.send(props().onClose())) | ||
on(msg.listenAt('#popups', Close), () => { | ||
msg.sendToParent(props().onClose()) | ||
}) | ||
messages.listenAt('#popups', OverlayClick).forEach(evt => { | ||
on(msg.listenAt('#popups', OverlayClick), (state, evt) => { | ||
if (!findParentByClass('popup', evt.target as Element)) | ||
messages.send(props().onClose()) | ||
msg.sendToParent(props().onClose()) | ||
}) | ||
} | ||
function render(props: Props) { | ||
@@ -46,0 +45,0 @@ const { content } = props |
import update from 'immupdate' | ||
import { Component, h, Message, ConnectParams, Vnode } from 'dompteuse' | ||
import { Stream } from 'most' | ||
@@ -10,22 +9,11 @@ import { TweenLite } from './gsap' | ||
export default function<T>(props?: Props<T>) { | ||
return Component({ | ||
key: 'select', | ||
props, | ||
defaultProps, | ||
initState, | ||
connect, | ||
render | ||
}) | ||
return Component<Props<T>, State>({ key: 'select', props, defaultProps, initState, connect, render }) | ||
} | ||
const Open = Message('open') | ||
const Close = Message('close') | ||
const ItemSelected = Message<any>('itemSelected') | ||
interface Props<T> { | ||
items: Array<T> | ||
selectedItem: T | ||
onChange: Function | ||
onChange: Message<T> | ||
itemRenderer?: (item: T) => string | ||
loading: boolean | ||
@@ -35,3 +23,4 @@ } | ||
const defaultProps: any = { | ||
items: [] | ||
items: [], | ||
itemRenderer: (item: any) => item.toString() | ||
} | ||
@@ -47,15 +36,21 @@ | ||
function connect({ on, props, messages }: ConnectParams<Props<any>, State>) { | ||
const Open = Message('Open') | ||
const Close = Message('Close') | ||
const ItemSelected = Message<any>('ItemSelected') | ||
function connect({ on, props, msg }: ConnectParams<Props<any>, State>) { | ||
on(Open, state => merge(state, { opened: true })) | ||
on(Close, state => merge(state, { opened: false })) | ||
on(ItemSelected, (state, item) => messages.send(props().onChange(item))) | ||
on(ItemSelected, (state, item) => msg.sendToParent(props().onChange(item))) | ||
} | ||
function render(props: Props<any>, state: State) { | ||
const { items, selectedItem, loading } = props | ||
const { items, selectedItem, loading, itemRenderer } = props | ||
const { opened } = state | ||
const text = (!loading && items.indexOf(selectedItem) > -1) ? selectedItem : '' | ||
const dropdownEl = getDropdownEl(items, opened, loading) | ||
const dropdownEl = getDropdownEl(props, opened) | ||
@@ -74,5 +69,7 @@ return ( | ||
function getDropdownEl(items: Array<any>, opened: boolean, loading: boolean) { | ||
function getDropdownEl(props: Props<any>, opened: boolean) { | ||
const { items, itemRenderer, loading } = props | ||
const itemEls = opened && !loading | ||
? items.map(renderItem) | ||
? items.map(itemRenderer).map(renderItem) | ||
: undefined | ||
@@ -92,3 +89,3 @@ | ||
function renderItem(item: any) { | ||
return h('li', { events: { onMouseDown: ItemSelected.with(item) } }, String(item)) | ||
return h('li', { events: { onMouseDown: ItemSelected.with(item) } }, item) | ||
} | ||
@@ -95,0 +92,0 @@ |
@@ -16,3 +16,3 @@ { | ||
"src/app.ts", | ||
"src/appState.ts", | ||
"src/appStore.ts", | ||
"src/blue.ts", | ||
@@ -23,2 +23,3 @@ "src/green.ts", | ||
"src/main.ts", | ||
"src/util/ajax.ts", | ||
"src/util/animation.ts", | ||
@@ -30,7 +31,9 @@ "src/util/dom.ts", | ||
"src/util/popup.ts", | ||
"src/util/promise.ts", | ||
"src/util/select.ts", | ||
"typings/es6shim.d.ts", | ||
"typings/fetch.d.ts", | ||
"typings/globals.d.ts", | ||
"typings/gsap.d.ts" | ||
] | ||
} |
@@ -10,12 +10,6 @@ 'use strict'; | ||
var _most = require('most'); | ||
var _most2 = _interopRequireDefault(_most); | ||
var _render = require('./render'); | ||
var _shallowEqual = require('./shallowEqual'); | ||
var _util = require('./util'); | ||
var _shallowEqual2 = _interopRequireDefault(_shallowEqual); | ||
var _messages = require('./messages'); | ||
@@ -25,2 +19,10 @@ | ||
var _observable = require('./observable'); | ||
var _observable2 = _interopRequireDefault(_observable); | ||
var _log = require('./log'); | ||
var _log2 = _interopRequireDefault(_log); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -51,5 +53,5 @@ | ||
// An empty placeholder is returned, and that's all our parent is going to see. | ||
// Components handle their own internal rendering. | ||
// Each component handles its own internal rendering. | ||
return (0, _h2.default)('div', compProps); | ||
}; | ||
} | ||
@@ -63,4 +65,6 @@ // Called when the component is created but isn't yet attached to the DOM | ||
var connected = false; | ||
// Internal callbacks | ||
component.lifecycle = { | ||
@@ -71,9 +75,4 @@ inserted: inserted, | ||
// A stream which only produces one value at component destruction time | ||
var componentDestruction = _most2.default.create(function (add) { | ||
component.lifecycle.destroyed = add; | ||
}); | ||
var messages = new _messages2.default(); | ||
var messages = new _messages2.default(componentDestruction); | ||
component.state = initState(props); | ||
@@ -83,17 +82,25 @@ component.elm = vnode.elm; | ||
component.messages = messages; | ||
component.subscriptions = []; | ||
// First render: | ||
// Create and insert the component's content | ||
// while its parent is still unattached for better perfs. | ||
(0, _render.renderComponentSync)(component); | ||
component.placeholder.elm = component.vnode.elm; | ||
component.placeholder.elm.__comp__ = component; | ||
var propsObservable = _observable2.default.create(function (add) { | ||
add(component.props); | ||
component.lifecycle.propsChanged = add; | ||
}, { replay: false }).named('props'); | ||
// Subsequent renders following a state update | ||
var onStream = function onStream(streamOrMessage, fn) { | ||
// Eagerly subscribe so that the observable get its first value and we honour | ||
// the ObservableWithInitialValue interface contract. | ||
propsObservable.subscribe(function (x) { | ||
return x; | ||
}); | ||
var stream = streamOrMessage._isMessage ? messages.listen(streamOrMessage) : streamOrMessage.until(componentDestruction); | ||
// Message | Observable registration function | ||
var onObservable = function onObservable(observableOrMessage, fn) { | ||
stream.observe(function (val) { | ||
var observable = observableOrMessage._isMessage ? messages.listen(observableOrMessage) : observableOrMessage; | ||
var unsubscribe = observable.subscribe(function (val, name) { | ||
var oldState = component.state; | ||
if ((0, _log.shouldLog)(_log2.default.message, component.key)) console.log('%c' + name + ' %creceived by %c' + component.key, 'color: #C963C1', 'color: black', 'font-weight: bold', 'with payload', val); | ||
var newState = fn(oldState, val); | ||
@@ -105,17 +112,33 @@ | ||
if (!(0, _shallowEqual2.default)(oldState, newState)) (0, _render.renderComponentAsync)(component); | ||
var shouldRender = | ||
// synchronous observables triggering before the very first render | ||
connected && | ||
// the props observable triggered, a synchronous render is made right after so skip | ||
!component.lifecycle.propsChanging && | ||
// null update | ||
!(0, _util.shallowEqual)(oldState, newState); | ||
if (shouldRender) (0, _render.renderComponentAsync)(component); | ||
}); | ||
return stream; | ||
component.subscriptions.push(unsubscribe); | ||
}; | ||
var connectParams = { | ||
on: onStream, | ||
props: function props() { | ||
return component.props; | ||
}, | ||
messages: messages | ||
on: onObservable, | ||
props: propsObservable, | ||
msg: messages | ||
}; | ||
connect(connectParams); | ||
connected = true; | ||
// First render | ||
// Create and insert the component's content | ||
// while its parent is still unattached for better perfs. | ||
(0, _render.renderComponentSync)(component); | ||
component.placeholder.elm = component.vnode.elm; | ||
component.placeholder.elm.__comp__ = component; | ||
// The component is now attached to the document, activate the messages | ||
messages._activate(component.vnode.elm); | ||
@@ -125,3 +148,3 @@ } | ||
// Store the component depth once it's attached to the DOM so we can render | ||
// component hierarchies in a predictive manner. | ||
// component hierarchies in a predictive (top -> down) manner. | ||
function inserted(component) { | ||
@@ -140,3 +163,3 @@ component.depth = getDepth(component.vnode.elm); | ||
// Update the original component with any property that may have changed on this render | ||
// Update the original component with any property that may have changed during this render pass | ||
component.props = newProps; | ||
@@ -149,8 +172,13 @@ component.placeholder = vnode; | ||
// in the render context of our parent | ||
if (!(0, _shallowEqual2.default)(oldProps, newProps)) (0, _render.renderComponentSync)(component); | ||
if (!(0, _util.shallowEqual)(oldProps, newProps)) { | ||
component.lifecycle.propsChanging = true; | ||
component.lifecycle.propsChanged(newProps); | ||
component.lifecycle.propsChanging = false; | ||
(0, _render.renderComponentSync)(component); | ||
} | ||
} | ||
function rendered(component, newVnode) { | ||
var i = void 0; | ||
// Store the new vnode inside the component so we can diff it next render | ||
@@ -161,3 +189,4 @@ component.vnode = newVnode; | ||
// as the placeholder is all our parent vnode knows about. | ||
if ((i = newVnode.data.hook) && (i = i.remove)) component.placeholder.data.hook.remove = i; | ||
var hook = newVnode.data.hook && newVnode.data.hook.remove; | ||
if (hook) component.placeholder.data.hook.remove = hook; | ||
} | ||
@@ -171,3 +200,6 @@ | ||
comp.destroyed = true; | ||
comp.lifecycle.destroyed(); | ||
for (var i = 0; i < comp.subscriptions.length; i++) { | ||
comp.subscriptions[i](); | ||
} | ||
} | ||
@@ -174,0 +206,0 @@ |
@@ -5,29 +5,34 @@ | ||
export function startApp<S>(options: { | ||
app: Vnode; | ||
snabbdomModules: any[]; | ||
elm: Element; | ||
}): void; | ||
app: Vnode | ||
snabbdomModules: any[] | ||
elm: Element | ||
}): void | ||
// Observables | ||
import { ObservableObject, Observable, ObservableWithInitialValue } from './observable' | ||
export var Observable: ObservableObject | ||
// Components | ||
interface StreamSub<S> { | ||
<A>(stream: Stream<A>, cb: (state: S, value: A) => S|void): Stream<A>; | ||
(message: NoArgMessage, cb: (state: S) => S|void): Stream<void>; | ||
<P>(message: Message<P>, cb: (state: S, payload: P) => S|void): Stream<P>; | ||
interface RegisterMessages<S> { | ||
<T>(observable: Observable<T>, handler: (state: S, value: T) => S|void): void | ||
(message: NoArgMessage, handler: (state: S) => S|void): void | ||
<P>(message: Message<P>, handler: (state: S, payload: P) => S|void): void | ||
} | ||
export interface ConnectParams<P, S> { | ||
on: StreamSub<S>; | ||
props: () => P; | ||
messages: Messages; | ||
on: RegisterMessages<S> | ||
props: ObservableWithInitialValue<P> | ||
msg: Messages | ||
} | ||
export function Component<P, S>(options: { | ||
key: string; | ||
props?: P; | ||
defaultProps?: any; // :-( https://github.com/Microsoft/TypeScript/issues/4889 | ||
initState: (props: P) => S; | ||
connect: (params: ConnectParams<P, S>) => void; | ||
render: (props: P, state: S) => Vnode; | ||
}): Vnode; | ||
key: string | ||
props?: P | ||
defaultProps?: any // :-( https://github.com/Microsoft/TypeScript/issues/4889 | ||
initState: (initProps: P) => S | ||
connect: (params: ConnectParams<P, S>) => void | ||
render: (props: P, state: S) => Vnode | ||
}): Vnode | ||
@@ -37,64 +42,40 @@ // dompteuse internals | ||
export var log: { | ||
render: boolean; | ||
stream: boolean; | ||
render: boolean | string | ||
message: boolean | string | ||
} | ||
// Messages & Events | ||
// Messages | ||
export interface NoArgMessage { | ||
(): MessagePayload<void>; | ||
(): MessagePayload<void> | ||
} | ||
export interface Message<P> { | ||
(payload: P): MessagePayload<P>; | ||
(payload: P): MessagePayload<P> | ||
with(payload: P): [Message<P>, P] | ||
} | ||
export function Message(name: string): NoArgMessage; | ||
export function Message<P>(name: string): Message<P>; | ||
export function Message(name: string): NoArgMessage | ||
export function Message<P>(name: string): Message<P> | ||
interface MessagePayload<P> { | ||
_id: number; | ||
_name: string; | ||
payload: P; | ||
_id: number | ||
_name: string | ||
payload: P | ||
} | ||
interface Messages { | ||
listen<P>(message: Message<P>): Stream<P>; | ||
listen(message: NoArgMessage): Stream<void>; | ||
listenAt<P>(selector: string, message: Message<P>): Stream<P>; | ||
listenAt(selector: string, message: NoArgMessage): Stream<void>; | ||
send<P>(payload: MessagePayload<P>): void; | ||
listen<P>(message: Message<P>): Observable<P> | ||
listen(message: NoArgMessage): Observable<void> | ||
listenAt<P>(selector: string, message: Message<P>): Observable<P> | ||
listenAt(selector: string, message: NoArgMessage): Observable<void> | ||
send<P>(payload: MessagePayload<P>): void | ||
sendToParent<P>(payload: MessagePayload<P>): void | ||
} | ||
export var Events: { | ||
listenAt(node: Element, targetSelector: string, eventName: string): Stream<Event> | ||
} | ||
// Global stream | ||
interface OnMessage<S> { | ||
(message: NoArgMessage, handler: (state: S) => S): void; | ||
<P>(message: Message<P>, handler: (state: S, payload: P) => S): void; | ||
} | ||
type GlobalStream<S> = Stream<S> & { | ||
value: S | ||
send: <P>(payload: MessagePayload<P>) => void | ||
} | ||
export function GlobalStream<S>( | ||
initialState: S, | ||
registerHandlers: (on: OnMessage<S>) => void): GlobalStream<S>; | ||
// most | ||
import { Stream } from 'most'; | ||
// snabbdom | ||
export type PatchFunction = (target: Element | Vnode, vnode: Vnode) => Vnode; | ||
export type PatchFunction = (target: Element | Vnode, vnode: Vnode) => Vnode | ||
export var snabbdom: { init: (modules: any[]) => PatchFunction }; | ||
export var snabbdom: { init: (modules: any[]) => PatchFunction } | ||
@@ -105,35 +86,35 @@ | ||
interface VnodeData { | ||
[s: string]: any; | ||
hook?: Hooks; | ||
events?: { [s: string]: EventHandler }; | ||
[s: string]: any | ||
hook?: Hooks | ||
events?: { [s: string]: EventHandler } | ||
} | ||
export interface Vnode { | ||
sel: string; | ||
data: VnodeData; | ||
children?: Array<Vnode>; | ||
text?: string; | ||
elm?: HTMLElement; | ||
key?: string; | ||
sel: string | ||
data: VnodeData | ||
children?: Array<Vnode> | ||
text?: string | ||
elm?: HTMLElement | ||
key?: string | ||
} | ||
type Node = Vnode | string; | ||
type Node = Vnode | string | ||
interface Hooks { | ||
pre?: () => void; | ||
init?: (node: Vnode) => void; | ||
create?: (emptyNode: any, node: Vnode) => void; | ||
insert?: (node: Vnode) => void; | ||
prepatch?: (oldVnode: Vnode, node: Vnode) => void; | ||
update?: (oldVnode: Vnode, node: Vnode) => void; | ||
postpatch?: (oldVnode: Vnode, node: Vnode) => void; | ||
destroy?: (node: Vnode) => void; | ||
remove?: (node: Vnode, cb: () => void) => void; | ||
post?: () => void; | ||
pre?: () => void | ||
init?: (node: Vnode) => void | ||
create?: (emptyNode: any, node: Vnode) => void | ||
insert?: (node: Vnode) => void | ||
prepatch?: (oldVnode: Vnode, node: Vnode) => void | ||
update?: (oldVnode: Vnode, node: Vnode) => void | ||
postpatch?: (oldVnode: Vnode, node: Vnode) => void | ||
destroy?: (node: Vnode) => void | ||
remove?: (node: Vnode, cb: () => void) => void | ||
post?: () => void | ||
} | ||
export function h(sel: string): Vnode; | ||
export function h(sel: string, dataOrChildren: VnodeData | Array<Node> | string): Vnode; | ||
export function h(sel: string, data: VnodeData, children: Array<Node> | string): Vnode; | ||
export function h(sel: string): Vnode | ||
export function h(sel: string, dataOrChildren: VnodeData | Array<Node> | string): Vnode | ||
export function h(sel: string, data: VnodeData, children: Array<Node> | string): Vnode | ||
export var patch: PatchFunction; | ||
export var patch: PatchFunction |
'use strict'; | ||
exports.__esModule = true; | ||
exports.h = exports.snabbdom = exports.patch = exports.GlobalStream = exports.log = exports.startApp = exports.Events = exports.Message = exports.Component = undefined; | ||
exports.h = exports.snabbdom = exports.patch = exports.log = exports.startApp = exports.Message = exports.Component = undefined; | ||
@@ -26,8 +26,4 @@ var _snabbdom = require('snabbdom'); | ||
var _globalStream = require('./globalStream'); | ||
var _events = require('./events'); | ||
var _events2 = _interopRequireDefault(_events); | ||
var _log = require('./log'); | ||
@@ -53,6 +49,4 @@ | ||
exports.Message = _message2.default; | ||
exports.Events = _events2.default; | ||
exports.startApp = startApp; | ||
exports.log = _log2.default; | ||
exports.GlobalStream = _globalStream.GlobalStream; | ||
exports.patch = patch; | ||
@@ -59,0 +53,0 @@ exports. |
@@ -6,6 +6,2 @@ 'use strict'; | ||
var _most = require('most'); | ||
var _most2 = _interopRequireDefault(_most); | ||
var _util = require('./util'); | ||
@@ -15,4 +11,2 @@ | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
/* snabbdom module extension used to register Messages as event listeners */ | ||
@@ -36,3 +30,3 @@ | ||
(0, _messages._sendToNode)(evt.target, msg(arg)); | ||
(0, _messages._sendToElement)(evt.target, msg(arg)); | ||
}; | ||
@@ -55,38 +49,2 @@ }; | ||
update: updateEventListeners | ||
}; | ||
/* Listens to a DOM Event using delegation */ | ||
exports.default = { | ||
listenAt: function listenAt(el, sel, name) { | ||
return _most2.default.create(function (add) { | ||
var listener = function listener(evt) { | ||
if (targetMatches(evt.target, sel, el)) add(evt); | ||
}; | ||
var useCapture = name in nonBubblingEvents; | ||
el.addEventListener(name, listener, useCapture); | ||
return function unsub() { | ||
el.removeEventListener(name, listener, useCapture); | ||
}; | ||
}); | ||
} | ||
}; | ||
var proto = Element.prototype; | ||
var nativeMatches = proto.matches || proto.matchesSelector || proto.webkitMatchesSelector || proto.mozMatchesSelector || proto.msMatchesSelector || proto.oMatchesSelector; | ||
function matches(el, selector) { | ||
return nativeMatches.call(el, selector); | ||
} | ||
var nonBubblingEvents = (0, _util.Set)('load', 'unload', 'focus', 'blur', 'mouseenter', 'mouseleave', 'submit', 'change', 'reset', 'timeupdate', 'playing', 'waiting', 'seeking', 'seeked', 'ended', 'loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'durationchange', 'play', 'pause', 'ratechange', 'volumechange', 'suspend', 'emptied', 'stalled'); | ||
function targetMatches(target, selector, root) { | ||
for (var el = target; el && el !== root; el = el.parentElement) { | ||
if (matches(el, selector)) return true; | ||
} | ||
return false; | ||
} | ||
}; |
"use strict"; | ||
exports.__esModule = true; | ||
exports.shouldLog = shouldLog; | ||
exports.default = { | ||
render: false, | ||
stream: false | ||
}; | ||
message: false | ||
}; | ||
function shouldLog(log, key) { | ||
return log === true || log === key; | ||
} |
@@ -25,3 +25,3 @@ "use strict"; | ||
// Allows Actions to be used as Object keys with the correct behavior | ||
// Allows Messages to be used as Object keys with the correct behavior | ||
message.toString = function () { | ||
@@ -28,0 +28,0 @@ return _id; |
@@ -5,17 +5,11 @@ 'use strict'; | ||
exports.default = Messages; | ||
exports._sendToNode = _sendToNode; | ||
exports._sendToElement = _sendToElement; | ||
var _most = require('most'); | ||
var _observable = require('./observable'); | ||
var _most2 = _interopRequireDefault(_most); | ||
var _observable2 = _interopRequireDefault(_observable); | ||
var _log = require('./log'); | ||
var _log2 = _interopRequireDefault(_log); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
function Messages(componentDestruction) { | ||
this.componentDestruction = componentDestruction; | ||
}; | ||
function Messages() {} | ||
@@ -27,6 +21,6 @@ Messages.prototype.listen = function (messageType) { | ||
return _most2.default.create(function (add) { | ||
return _observable2.default.create(function (add) { | ||
var sub = { | ||
messageType: messageType, | ||
streamAdd: add | ||
observableAdd: add | ||
}; | ||
@@ -39,7 +33,7 @@ | ||
}; | ||
}).until(this.componentDestruction); | ||
}).named(messageType._name); | ||
}; | ||
Messages.prototype.listenAt = function (nodeSelector, messageType) { | ||
return _most2.default.create(function (add) { | ||
return _observable2.default.create(function (add) { | ||
var el = document.querySelector(nodeSelector); | ||
@@ -52,3 +46,3 @@ if (!el) return; | ||
messageType: messageType, | ||
streamAdd: add | ||
observableAdd: add | ||
}; | ||
@@ -62,9 +56,12 @@ | ||
}; | ||
}).until(this.componentDestruction); | ||
}).named(messageType._name); | ||
}; | ||
Messages.prototype.send = function (msg) { | ||
this._receive(msg); | ||
}; | ||
Messages.prototype.sendToParent = function (msg) { | ||
if (!this.el) throw new Error('Messages.send cannot be called synchronously in connect()'); | ||
_sendToNode(this.el, msg); | ||
_sendToElement(this.el.parentElement, msg); | ||
}; | ||
@@ -82,26 +79,19 @@ | ||
var sub = subs[i]; | ||
if (sub.messageType._id === msg._id) sub.streamAdd(msg.payload); | ||
if (sub.messageType._id === msg._id) sub.observableAdd(msg.payload); | ||
} | ||
}; | ||
function _sendToNode(node, msg) { | ||
var parentEl = node.parentElement; | ||
while (parentEl) { | ||
function _sendToElement(el, msg) { | ||
while (el) { | ||
// Classic component's listen | ||
if (parentEl.__comp__) { | ||
if (_log2.default) console.log('%c' + msg._name, 'color: #FAACF3', 'sent locally to', parentEl, 'with payload ', msg.payload); | ||
return parentEl.__comp__.messages._receive(msg); | ||
} | ||
if (el.__comp__) return el.__comp__.messages._receive(msg); | ||
// listenAt | ||
else if (parentEl.__subs__) return parentEl.__subs__.filter(function (sub) { | ||
else if (el.__subs__) return el.__subs__.filter(function (sub) { | ||
return sub.messageType._id === msg._id; | ||
}).forEach(function (sub) { | ||
if (_log2.default) console.log('%c' + msg._name, 'color: #FAACF3', 'sent locally to', parentEl, 'with payload ', msg.payload); | ||
sub.streamAdd(msg.payload); | ||
return sub.observableAdd(msg.payload); | ||
}); | ||
parentEl = parentEl.parentElement; | ||
el = el.parentElement; | ||
} | ||
}; | ||
} |
@@ -59,7 +59,7 @@ 'use strict'; | ||
if (!nextRender) nextRender = requestAnimationFrame(renderNow); | ||
}; | ||
} | ||
function renderComponentSync(component) { | ||
renderComponent(component, true); | ||
}; | ||
} | ||
@@ -94,5 +94,5 @@ function renderComponent(component, checkRenderQueue) { | ||
if (_log2.default.render) { | ||
if ((0, _log.shouldLog)(_log2.default.render, component.key)) { | ||
var renderTime = Math.round((performance.now() - beforeRender) * 100) / 100; | ||
console.log('Render component %c' + component.key, 'font-weight: bold', renderTime + ' ms', props, state); | ||
console.log('Render component %c' + component.key, 'font-weight: bold', renderTime + ' ms', '| props: ', props, '| state: ', state); | ||
} | ||
@@ -136,3 +136,3 @@ | ||
function logEndRender() { | ||
if (_log2.default.render) console.log('%cRender - end\n\n', 'color: orange'); | ||
if (_log2.default.render) console.log('%cRender - end\n\n\n', 'color: orange'); | ||
} |
@@ -5,2 +5,3 @@ "use strict"; | ||
exports.Set = Set; | ||
exports.shallowEqual = shallowEqual; | ||
function Set() { | ||
@@ -12,2 +13,24 @@ var set = {}; | ||
return set; | ||
}; | ||
} | ||
/* Efficient shallow comparison of two objects */ | ||
function shallowEqual(objA, objB) { | ||
if (objA === objB) return true; | ||
var keysA = Object.keys(objA); | ||
var keysB = Object.keys(objB); | ||
// Test for A's keys different from B's. | ||
for (var i = 0; i < keysA.length; i++) { | ||
if (objA[keysA[i]] !== objB[keysA[i]]) return false; | ||
} | ||
// Test for B's keys different from A's. | ||
// Handles the case where B has a property that A doesn't. | ||
for (var i = 0; i < keysB.length; i++) { | ||
if (objA[keysB[i]] !== objB[keysB[i]]) return false; | ||
} | ||
return true; | ||
} |
{ | ||
"name": "dompteuse", | ||
"version": "0.3.5", | ||
"version": "0.4.0", | ||
"main": "lib/dompteuse", | ||
"typings": "lib/dompteuse.d.ts", | ||
"description": "Virtual dom, streams and isolated components", | ||
"keywords": ["virtual dom", "vdom", "model", "stream", "component"], | ||
"description": "Virtual dom, observables / streams and isolated components", | ||
"keywords": ["virtual dom", "vdom", "model", "stream", "observable", "component", "framework"], | ||
"homepage": "https://github.com/AlexGalays/dompteuse/", | ||
@@ -29,5 +29,8 @@ | ||
"scripts": { | ||
"copy-typings": "cp typings/dompteuse.d.ts lib", | ||
"build": "npm run copy-typings && node node_modules/babel-cli/bin/babel.js src --out-dir lib" | ||
"copy-typings": "cd src && find . -name '*.d.ts' | cpio -pdm ../lib", | ||
"babel": "node node_modules/babel-cli/bin/babel.js src --out-dir lib", | ||
"copy-lib-to-example": "cp -rf lib example/node_modules/dompteuse", | ||
"build": "npm run copy-typings && npm run babel", | ||
"example-dev": "npm run build && npm run copy-lib-to-example" | ||
} | ||
} |
476
README.md
@@ -7,48 +7,272 @@ # dompteuse | ||
- Fast thanks to [snabbdom](https://github.com/paldepind/snabbdom), [most](https://github.com/cujojs/most), component isolation and async RAF rendering | ||
- Global and local states use streams for greater composition | ||
- Fast, thanks to [snabbdom](https://github.com/paldepind/snabbdom), aggressive component rendering isolation and async RAF rendering | ||
- Global and local state can optionally use Observables for greater composition | ||
- No JS `class` / `this` nonsense | ||
- Tiny size in KB | ||
- Comes with useful logs | ||
- Very typesafe / typescript friendly | ||
- First class support for typescript (very typesafe) | ||
# Content | ||
* [Componentization](#componentization) | ||
* [Global streams](#globalStreams) | ||
* [Api](#api) | ||
* [Example](https://github.com/AlexGalays/dompteuse/tree/master/example/src) | ||
* [Components: step by step guide](#componentization) | ||
* [Observables](#observables) | ||
* [Global stores](#globalStores) | ||
* [API](#api) | ||
* [Component](#api-component) | ||
* [Global store](#api-globalStore) | ||
* [h](#api-h) | ||
* [startApp](#api-startApp) | ||
* [Message](#api-message) | ||
* [patch](#api-patch) | ||
* [logging](#api-logging) | ||
* [Full TS Example](https://github.com/AlexGalays/dompteuse/tree/master/example/src) | ||
<a name="componentization"></a> | ||
# Componentization | ||
# Components: step by step guide | ||
`dompteuse` adds the concept of encapsulated components to pure functional virtual dom. | ||
Standard Virtual nodes and components are composed to build a Vnode tree that can scale in size and complexity. | ||
`dompteuse` adds the concept of encapsulated components to `snabbdom`'s pure functional virtual dom. | ||
Standard Virtual nodes and components are composed to build a `Vnode` tree that can scale in size and complexity. | ||
A `Vnode` is what you get when calling `snabbdom`'s `h` function for instance. | ||
A component is simply a function that takes an option object as an argument and returns a Vnode ready to be used inside its parent children. | ||
A component is simply a function that takes an option object as an argument and returns a `Vnode` ready to be used inside its parent children, i.e, this is a valid array of `Vnodes`: | ||
Note: typescript will be used in the examples, javascript devs can ignore the types annotations. | ||
```javascript | ||
[ | ||
h('div'), | ||
myComponent({ someProp: 33 }), | ||
h('p', 'hello') | ||
] | ||
``` | ||
Note: typescript will be used in the examples, however the library also works just fine with javascript. | ||
Here is the simplest component definition one can write: | ||
```javascript | ||
import { Component } from 'dompteuse' | ||
import { Component, h } from 'dompteuse' | ||
export default function(props?: Props) { | ||
return Component({ | ||
key: 'Select', | ||
props, | ||
defaultProps, | ||
initState, | ||
connect, | ||
render | ||
}) | ||
export default function() { | ||
return Component({ key: 'Button', initState, connect, render }) | ||
} | ||
function initState() { return {} } | ||
function connect() {} | ||
function render() { | ||
return h('button') | ||
} | ||
``` | ||
Let's look at the option object properties: | ||
Now, that isn't terribly useful because we really want our component to be stateful, else we would just use a regular `Vnode` object. | ||
## key | ||
Let's add some state, and make it change over time: | ||
```javascript | ||
import { Component, h, Message, ConnectParams } from 'dompteuse' | ||
import { merge } from './util/object' // Fictitious | ||
export default function() { | ||
return Component<{}, State>({ key: 'Button', initState, connect, render }) | ||
} | ||
interface State { | ||
text: string | ||
} | ||
function initState() { | ||
return { text: '' } | ||
} | ||
const Click = Message('Click') | ||
function connect({ on }: ConnectParams<{}, State>) { | ||
on(Click, state => ({ text: 'clicked' })) | ||
} | ||
function render(props: {}, state: State) { | ||
return h('button', { events: { onClick: Click } }, state.text) | ||
} | ||
``` | ||
Now we created a `Message` named Click that is locally sent to our component whenever the user click on the button. | ||
We handle that message in `connect` and return the new state of our component. The component will then redraw with that new state. | ||
Using explicit Messages instead of callbacks to update our state brings consistency with other kinds of (external) state management and make state debugging easier since messages can be logged (see [logging](#api-logging)). | ||
In the above code, `on(Click)` is in fact a shortcut for `on(msg.listen(Click))`. | ||
Here's the longer form: | ||
```javascript | ||
function connect({ on, msg }: ConnectParams<{}, State>) { | ||
on(msg.listen(Click), state => ({ text: 'clicked' })) | ||
} | ||
``` | ||
What `msg.listen(Click)` returns is an [Observable](#observables) that emits new values (the payload of each message) | ||
every time the message is sent. | ||
This is very useful because observables can be composed easily: | ||
```javascript | ||
import debounce from 'dompteuse/lib/observable/debounce' | ||
function connect({ on, msg }: ConnectParams<{}, State>) { | ||
const clicks = debounce(1000, msg.listen(Click)) | ||
on(clicks, state => ({ text: 'clicked' })) | ||
} | ||
``` | ||
Now, the state is only updated if we stopped clicking for 1 second. | ||
Our component now has an internal state and we know how to update it. But it's also completely opaque from the outside! | ||
In a tree of `Vnodes`, parents must be able to influence the rendering of their children. For that purpose, we introduce props: | ||
```javascript | ||
import { Component, h, Message, ConnectParams } from 'dompteuse' | ||
import { merge } from './util/object' // Fictitious | ||
export default function(props: Props) { | ||
return Component<{}, State>({ key: 'Button', props, initState, connect, render }) | ||
} | ||
interface Props { | ||
defaultText: string | ||
paragraph: string | ||
} | ||
interface State { | ||
text: string | ||
} | ||
function initState(initProps: Props) { | ||
return { text: initProps.defaultText } | ||
} | ||
const Click = Message('Click') | ||
function connect({ on }: ConnectParams<Props, State>) { | ||
on(Click, state => ({ text: 'clicked' })) | ||
} | ||
function render(props: Props, state: State) { | ||
return ( | ||
h('div', [ | ||
h('button', { events: { onClick: Click } }, state.text), | ||
h('p', props.paragraph) | ||
]) | ||
) | ||
} | ||
``` | ||
Now our parent can render the component with more control: It can set the default text that should be displayed initially, but also | ||
directly set the paragraph text of the `p` tag. | ||
When composing components, you must choose which component should own which piece of state. Disregarding global state (which has a use, see [Global store](#globalStores)) for a second, local state can reside in a component or any of its parent hierarchy. | ||
Let's see how we can move the previous button `text` state one level up, so that the component parent can directly set it: | ||
```javascript | ||
import { Component, h, Message, ConnectParams } from 'dompteuse' | ||
import { merge } from './util/object' // Fictitious | ||
export default function(props: Props) { | ||
return Component<{}, State>({ key: 'Button', props, initState, connect, render }) | ||
} | ||
interface Props { | ||
text: string | ||
paragraph: string | ||
onClick: Message<void> | ||
} | ||
interface State {} | ||
function initState() { | ||
return {} | ||
} | ||
const Click = Message('Click') | ||
function connect({ on, props, msg }: ConnectParams<Props, State>) { | ||
on(Click, () => msg.sendToParent(props().onClick())) | ||
} | ||
function render(props: Props, state: State) { | ||
return ( | ||
h('div', [ | ||
h('button', { events: { onClick: Click } }, props.text), | ||
h('p', props.paragraph) | ||
]) | ||
) | ||
} | ||
``` | ||
We now delegate and send a message to our direct parent component so that it can, in turn, listen to that message from its `connect` function and update its own state. | ||
At this point, the component is no longer stateful and providing it didn't have any other state, should be refactored to a simple | ||
function returning a `Vnode` or Array of `Vnodes`. | ||
<a name="observables"></a> | ||
# Observables | ||
`dompteuse` comes with an implementation of observables (also known as streams) so that components can more easily declare | ||
how their state should change based on user input and any other observable changes in the application. | ||
Observables are an optional abstraction: If you are more confident with just sending messages around, you can do that too. | ||
The characteristics of this observable implementation are: | ||
* Tiny abstraction, fast | ||
* Has a functional style: all combinators are standalone functions that won't be compiled in your code if you don't import them | ||
* Multicast: All observables are aware that multiple subscribers may be interested | ||
* The last value of an observable can be read by invoking the observable as a function | ||
* Synchronous: Easier to reason about and friendlier stack traces | ||
* No error handling/swallowing: No need for it since this observable implementation is synchronous | ||
* No notion of an observable's end/completion for simplicity sake and since we have just two kinds of observables: never ending ones, and ones that are tied to a component's lifecycle | ||
* Lazy resource management: An observable only activate if there is at least one subscriber | ||
* If the observable already hold a value, any subscribe function will be called immediately upon registration (two exceptions being Global stores and props observables) | ||
All combinators can be found under `lib/observable`, for instance to import `debounce`: | ||
```javascript | ||
import debounce from 'dompteuse/lib/observable/debounce' | ||
``` | ||
To see observables in action, check the [example's ajax abstraction](https://github.com/AlexGalays/dompteuse/tree/master/example/src/util/ajax.ts) and [its usage](https://github.com/AlexGalays/dompteuse/tree/master/example/src/blue.ts) | ||
<a name="api"></a> | ||
# API | ||
<a name="api-component"></a> | ||
## Component | ||
The `Component` factory function takes an object with the following properties: | ||
### key | ||
Mandatory `String` | ||
This is the standard Virtual node key used to uniquely identify this Vnode. It is also used for logging purposes, so it is usually just the name of the component. | ||
This is the standard Virtual DOM `key` used in the diffing algorithm to uniquely identify this `Vnode`. | ||
It is also used for logging purposes, so it is usually just the name of the component. | ||
## props | ||
### props | ||
@@ -58,11 +282,11 @@ Optional `Object` | ||
Typically props either represent state that is maintained outside the component or properties used to tweak the component's behavior. | ||
The `render` function will be called if the props object changed shallowly, hence it's a good practice to use a flat object. | ||
The `render` function will be called if the props object changed shallowly (any of its property references changed), hence it's a good practice to use a flat object. | ||
Note: props and state are separated exactly like in `React` as it works great. The same design best practices apply. | ||
## defaultProps | ||
### defaultProps | ||
Optional `Object` (upper type of props) | ||
An object with part of the prop keys that should be used if the parent do not specify all the props. | ||
An object with some of the prop keys that should be used if the parent do not specify all the props keys. | ||
## initState | ||
### initState | ||
@@ -72,5 +296,5 @@ Mandatory `Object` | ||
## connect | ||
### connect | ||
Mandatory `function({ on, messages, props }: ConnectParams<Props, State>): void` | ||
Mandatory `function({ on, msg, props }: ConnectParams<Props, State>): void` | ||
Connects the component to the app and computes the local state of the component. | ||
@@ -81,41 +305,36 @@ `connect` is called only once when the component is mounted. | ||
- `on` registers a Stream that modifies the component local state. The stream will be automatically unsubscribed from when | ||
the component is unmounted. | ||
- `messages` is the interface used to `listen` to custom Messages sent by direct component children or `send` a message to our direct component parent. Streams created this way will also be unsubscribed from when the component is unmounted. | ||
- `props` A props accessor function. Used to read props inside connect. | ||
- `on` registers a `Message` or `Observable` that modifies the component local state. | ||
The Observable will be automatically unsubscribed from when the component is unmounted. | ||
Returning the current state or `undefined` in an `on` handler will skip rendering and can be used to do side effects. | ||
`connect` arguably has more interesting characteristics than the imperative approach `React` use (i.e `setState`): | ||
Full interface: | ||
- Streams are composable, callbacks are not. Doing things like throttling or only listening to the very last ajax action fired | ||
is a recurrent, non abstractable pain with imperative callback/setState. | ||
```javascript | ||
<T>(observable: Observable<T>, handler: (state: S, value: T) => S|void): void | ||
(message: NoArgMessage, handler: (state: S) => S|void): void | ||
<P>(message: Message<P>, handler: (state: S, payload: P) => S|void): void | ||
``` | ||
- Callback references often change over time (most likely from using partial application) and we can no longer apply streamlined performance optimizations because some props truly represent data while other props are callbacks that may or may not purposely change. By using simple `Messages` instead of bound functions/closures, this issue is avoided. | ||
- `msg` is the interface used to send and listen messages. | ||
Example: | ||
Full interface: | ||
```javascript | ||
import { Message, ConnectParams } from 'dompteuse' | ||
listen<P>(message: Message<P>): Observable<P> | ||
listen(message: NoArgMessage): Observable<void> | ||
listenAt<P>(selector: string, message: Message<P>): Observable<P> | ||
listenAt(selector: string, message: NoArgMessage): Observable<void> | ||
send<P>(payload: MessagePayload<P>): void | ||
sendToParent<P>(payload: MessagePayload<P>): void | ||
``` | ||
// Message used to communicate with our children in a cohesive manner | ||
const TriggerClick = Message('triggerClick') | ||
- `props` An Observable with a new value every time the props passed by our parent changed. | ||
function connect({ on, props, messages }: ConnectParams<Props, State>) { | ||
// Subscribe to the stream of button clicks and update our state every time it changes | ||
on(messages.listen(TriggerClick), state => { | ||
const opened = !state.opened | ||
Just like with props, a redraw will only get scheduled if the state object changed shallowly. | ||
// Any 'on' handler must return the new component state | ||
return merge(state, { opened }) | ||
}) | ||
} | ||
``` | ||
`on` can listen to any kind of `most` stream. See [global streams](#globalStreams). | ||
Just like with props, a redraw will only get scheduled if the state object changed shallowly so returning the current state | ||
in `on()` will skip rendering. | ||
### render | ||
## render | ||
Mandatory `function(props: Props, state: State): Vnode` | ||
Mandatory `function(props: Props, state: State): VNode` | ||
Returns the Vnode tree based on the props and state. | ||
@@ -132,3 +351,3 @@ | ||
const ButtonClick = Message('buttonClick') | ||
const ButtonClick = Message<number>('ButtonClick') | ||
@@ -138,15 +357,20 @@ function render(props: void, state: State) { | ||
return h('div#text', [ | ||
h('h1', 'Hello'), | ||
h('p', text), | ||
h('button', { events: { onClick: ButtonClick } }) | ||
]) | ||
return ( | ||
h('div#text', [ | ||
h('h1', 'Hello'), | ||
h('p', text), | ||
h('button', { events: { onClick: ButtonClick.with(33) } }) | ||
]) | ||
) | ||
} | ||
``` | ||
<a name="globalStreams"></a> | ||
# Global streams | ||
A construct is provided to easily build push-based global streams in a typesafe fashion. This is entirely optional. | ||
<a name="globalStores"></a> | ||
## Global stores | ||
A construct is provided to easily build push-based global observables in a type-safe manner. This is entirely optional. | ||
First, a note on local versus global state: | ||
You typically want to keep very transient state as local as possible so that it remains encapsulated in a component and do not leak up. | ||
@@ -162,3 +386,3 @@ <br /> | ||
That leaves global state, which can be updated from anywhere and is read from multiple screens. | ||
That leaves global state, which can be updated from anywhere and is accessed from multiple screens. | ||
<br /> | ||
@@ -173,4 +397,5 @@ **Example of typical global state** | ||
import { Message, GlobalStream } from 'dompteuse' | ||
import merge from './util/obj/merge' | ||
import { Message } from 'dompteuse' | ||
import GlobalStore from 'dompteuse/lib/store' | ||
import merge from './util/obj/merge' // Fictitious | ||
@@ -180,2 +405,3 @@ | ||
interface UserState { | ||
@@ -187,3 +413,3 @@ name: string | ||
// This exports a stream ready to be used in a component's connect function | ||
// This exports a store containing an observable ready to be used in a component's connect function | ||
export default GlobalStream<UserState>(initialState, on => { | ||
@@ -197,3 +423,3 @@ on(setUserName, (state, name) => | ||
// Subscribe to it in a component's connect | ||
import userState from './userState' | ||
import userStore from './userStore' | ||
@@ -203,8 +429,8 @@ // Provide an initial value | ||
return { | ||
userName: userState.value.name | ||
userName: userStore.state().name | ||
} | ||
} | ||
connect(on: StreamSub<State>, events: Events) { | ||
on(userState, (state, user) => { | ||
function connect({ on }: ConnectParams<{}, State>) { | ||
on(userStore.state, (state, user) => { | ||
// 'Copy' the global user name into our local component state to make it available to `render` | ||
@@ -217,11 +443,9 @@ return merge(state, { userName: user.name }) | ||
// Then anywhere else, import the stream and the message | ||
stream.send(setUserName('Monique')) | ||
userStore.send(setUserName('Monique')) | ||
``` | ||
<a name="api"></a> | ||
# Api | ||
<a name="api-h"></a> | ||
## h | ||
Creates a Vnode | ||
Creates a `Vnode` | ||
This is proxied to [snabbdom's h](https://github.com/paldepind/snabbdom/blob/master/h.js) so we can add our type definitions | ||
@@ -234,3 +458,3 @@ transparently. | ||
``` | ||
On top of the `snabbdom` modules you pass to `startApp`, an extra module is installed by `dompteuse`: `events`. | ||
On top of the `snabbdom` modules you may feed to `startApp`, an extra module is always installed by `dompteuse`: `events`. | ||
@@ -241,3 +465,3 @@ ```javascript | ||
const SomeMessage = Message<Event>('someMessage') | ||
const SomeMessage = Message<Event>('SomeMessage') | ||
@@ -249,3 +473,3 @@ // Send a message to the enclosing component on click | ||
// This is more efficient than creating a closure on every render. | ||
const AnotherMessage = Message<{x: number}>('anotherMessage') | ||
const AnotherMessage = Message<{x: number}>('AnotherMessage') | ||
@@ -255,2 +479,3 @@ h('div', { events: { onClick: AnotherMessage.with({ x: 3 }) } }) | ||
<a name="api-startApp"></a> | ||
## startApp | ||
@@ -265,3 +490,3 @@ | ||
snabbdomModules: any[] // The snabbdom modules that should be active during patching | ||
}): void; | ||
}): void | ||
``` | ||
@@ -274,3 +499,3 @@ | ||
declare var require: any | ||
const snabbdomModules = [ | ||
@@ -286,5 +511,7 @@ require('snabbdom/modules/class'), | ||
``` | ||
<a name="api-message"></a> | ||
## Message | ||
Create a custom application message used to either communicate between components or push to a [GlobalStream](#globalStreams). | ||
Create a custom application message used to either communicate between components or send to a [GlobalStore](#globalStores). | ||
```javascript | ||
@@ -294,80 +521,41 @@ import { Message } from 'dompteuse' | ||
// Message taking no arguments | ||
const increment = Message('increment') | ||
const Increment = Message('Increment') | ||
// Message taking one argument | ||
const incrementBy = Message<number>('incrementBy') | ||
const IncrementBy = Message<number>('IncrementBy') | ||
``` | ||
## ConnectParams | ||
### StreamSub | ||
<a name="api-patch"></a> | ||
## patch | ||
Used to subscribe to a stream and update the component state. | ||
If nothing is returned (undefined) then the component state is not modified. | ||
The `snabbdom` patch function that dompteuse uses. It is made available after the call to `startApp`. | ||
This can be used to create some advanced components with their own internal patching needs. | ||
Signature: | ||
```javascript | ||
on<A>(stream: Stream<A>, cb: (state: S, value: A) => S|void): Stream<A> | ||
on(message: NoArgMessage, cb: (state: S) => S|void): Stream<void> | ||
// Shortcut for on(messages.listen(MyMessage)) | ||
on<P>(message: Message<P>, cb: (state: S, payload: P) => S|void): Stream<P> | ||
import { patch } from 'dompteuse' | ||
``` | ||
### Messages | ||
```javascript | ||
// Listen for messages coming from immediate Vnodes or component children | ||
messages.listen<P>(message: Message<P>): Stream<P> | ||
<a name="api-logging"></a> | ||
## logging | ||
// Listen for a Message at the first Element found using the given CSS selector. | ||
// This should be rarely used, and is mostly useful for components | ||
// that attach their content to a remote DOM Element, like popups. | ||
messages.listenAt<P>(selector: string, message: Message<P>): Stream<P>; | ||
`dompteuse` has useful logging to help you debug or visualize the data flows. | ||
// Sends a message to the nearest parent component | ||
messages.send<P>(message: MessagePayload<P>): void | ||
``` | ||
By default, nothing is logged, but this can be changed: | ||
Example: | ||
```javascript | ||
import { ConnectParams, Message } from 'dompteuse' | ||
import { Opened } from './someComponent' | ||
import { log } from 'dompteuse' | ||
const Increment = Message('increment') | ||
connect({ on, messages }: ConnectParams<Props, State>) { | ||
// Listen to the Opened even sent by 'someComponent' | ||
const openStream = messages.listen(Opened) | ||
// This is equivalent to above | ||
const openStream2 = on(Opened, state => state) | ||
// Send a message. Our direct parent can react to it. | ||
messages.send(Increment()) | ||
} | ||
log.render = true | ||
log.message = true | ||
``` | ||
## Events | ||
Additionally, you can fine tune which component get logged using that component's `key`: | ||
Builds a Stream of DOM Events using Event delegation. | ||
```javascript | ||
listenAt(node: Element, targetSelector: string, eventName: string): Stream<Event> | ||
log.render = 'select' | ||
log.message = 'popup' | ||
``` | ||
```javascript | ||
import { Events } from 'dompteuse' | ||
const stream = Events.listenAt(document.body, '.button', 'click') | ||
``` | ||
## patch | ||
The `snabbdom` patch function that dompteuse uses. Only available after the call to `startApp` | ||
```javascript | ||
import { patch } from 'dompteuse' | ||
``` | ||
You will want to change the log values as early as possible in your program. |
@@ -1,16 +0,17 @@ | ||
import h from 'snabbdom/h'; | ||
import most from 'most'; | ||
import h from 'snabbdom/h' | ||
import { renderComponentSync, renderComponentAsync } from './render' | ||
import { shallowEqual } from './util' | ||
import Messages from './messages' | ||
import Observable from './observable' | ||
import log, { shouldLog } from './log' | ||
import { renderComponentSync, renderComponentAsync } from './render'; | ||
import shallowEqual from './shallowEqual'; | ||
import Messages from './messages'; | ||
const empty = {}; | ||
const empty = {} | ||
export default function Component(options) { | ||
const { key, props = empty, defaultProps, initState, connect, render } = options; | ||
const { key, props = empty, defaultProps, initState, connect, render } = options | ||
if (defaultProps) | ||
Object.keys(defaultProps).forEach(key => { | ||
if (props[key] === undefined) props[key] = defaultProps[key]}); | ||
if (props[key] === undefined) props[key] = defaultProps[key]}) | ||
@@ -21,14 +22,16 @@ const compProps = { | ||
component: { props, initState, connect, render, key } | ||
}; | ||
} | ||
// An empty placeholder is returned, and that's all our parent is going to see. | ||
// Components handle their own internal rendering. | ||
return h('div', compProps); | ||
}; | ||
// Each component handles its own internal rendering. | ||
return h('div', compProps) | ||
} | ||
// Called when the component is created but isn't yet attached to the DOM | ||
function create(_, vnode) { | ||
const { component } = vnode.data; | ||
const { props, initState, connect } = component; | ||
const { component } = vnode.data | ||
const { props, initState, connect } = component | ||
let connected = false | ||
// Internal callbacks | ||
@@ -38,59 +41,81 @@ component.lifecycle = { | ||
rendered | ||
}; | ||
} | ||
// A stream which only produces one value at component destruction time | ||
const componentDestruction = most.create(add => { | ||
component.lifecycle.destroyed = add; | ||
}); | ||
const messages = new Messages() | ||
const messages = new Messages(componentDestruction); | ||
component.state = initState(props) | ||
component.elm = vnode.elm | ||
component.placeholder = vnode | ||
component.messages = messages | ||
component.subscriptions = [] | ||
component.state = initState(props); | ||
component.elm = vnode.elm; | ||
component.placeholder = vnode; | ||
component.messages = messages; | ||
const propsObservable = Observable.create(add => { | ||
add(component.props) | ||
component.lifecycle.propsChanged = add | ||
}, { replay: false }).named('props') | ||
// First render: | ||
// Create and insert the component's content | ||
// while its parent is still unattached for better perfs. | ||
renderComponentSync(component); | ||
component.placeholder.elm = component.vnode.elm; | ||
component.placeholder.elm.__comp__ = component; | ||
// Eagerly subscribe so that the observable get its first value and we honour | ||
// the ObservableWithInitialValue interface contract. | ||
propsObservable.subscribe(x => x) | ||
// Subsequent renders following a state update | ||
const onStream = function(streamOrMessage, fn) { | ||
// Message | Observable registration function | ||
const onObservable = function(observableOrMessage, fn) { | ||
const stream = streamOrMessage._isMessage | ||
? messages.listen(streamOrMessage) | ||
: streamOrMessage.until(componentDestruction); | ||
const observable = observableOrMessage._isMessage | ||
? messages.listen(observableOrMessage) | ||
: observableOrMessage | ||
stream.observe(val => { | ||
const oldState = component.state; | ||
const newState = fn(oldState, val); | ||
const unsubscribe = observable.subscribe((val, name) => { | ||
const oldState = component.state | ||
if (newState === undefined) return; | ||
if (shouldLog(log.message, component.key)) | ||
console.log(`%c${name} %creceived by %c${component.key}`, | ||
'color: #C963C1', 'color: black', | ||
'font-weight: bold', 'with payload', val) | ||
component.state = newState; | ||
const newState = fn(oldState, val) | ||
if (!shallowEqual(oldState, newState)) | ||
renderComponentAsync(component); | ||
}); | ||
if (newState === undefined) return | ||
return stream; | ||
}; | ||
component.state = newState | ||
const shouldRender = | ||
// synchronous observables triggering before the very first render | ||
connected && | ||
// the props observable triggered, a synchronous render is made right after so skip | ||
!component.lifecycle.propsChanging && | ||
// null update | ||
!shallowEqual(oldState, newState) | ||
if (shouldRender) | ||
renderComponentAsync(component) | ||
}) | ||
component.subscriptions.push(unsubscribe) | ||
} | ||
const connectParams = { | ||
on: onStream, | ||
props: () => component.props, | ||
messages | ||
}; | ||
on: onObservable, | ||
props: propsObservable, | ||
msg: messages | ||
} | ||
connect(connectParams); | ||
messages._activate(component.vnode.elm); | ||
connect(connectParams) | ||
connected = true | ||
// First render | ||
// Create and insert the component's content | ||
// while its parent is still unattached for better perfs. | ||
renderComponentSync(component) | ||
component.placeholder.elm = component.vnode.elm | ||
component.placeholder.elm.__comp__ = component | ||
// The component is now attached to the document, activate the messages | ||
messages._activate(component.vnode.elm) | ||
} | ||
// Store the component depth once it's attached to the DOM so we can render | ||
// component hierarchies in a predictive manner. | ||
// component hierarchies in a predictive (top -> down) manner. | ||
function inserted(component) { | ||
component.depth = getDepth(component.vnode.elm); | ||
component.depth = getDepth(component.vnode.elm) | ||
} | ||
@@ -100,40 +125,47 @@ | ||
function postpatch(oldVnode, vnode) { | ||
const oldData = oldVnode.data; | ||
const newData = vnode.data; | ||
const oldData = oldVnode.data | ||
const newData = vnode.data | ||
const component = oldData.component; | ||
const oldProps = component.props; | ||
const newProps = newData.component.props; | ||
const component = oldData.component | ||
const oldProps = component.props | ||
const newProps = newData.component.props | ||
// Update the original component with any property that may have changed on this render | ||
component.props = newProps; | ||
component.placeholder = vnode; | ||
// Update the original component with any property that may have changed during this render pass | ||
component.props = newProps | ||
component.placeholder = vnode | ||
newData.component = component; | ||
newData.component = component | ||
// If the props changed, render immediately as we are already | ||
// in the render context of our parent | ||
if (!shallowEqual(oldProps, newProps)) | ||
renderComponentSync(component); | ||
if (!shallowEqual(oldProps, newProps)) { | ||
component.lifecycle.propsChanging = true | ||
component.lifecycle.propsChanged(newProps) | ||
component.lifecycle.propsChanging = false | ||
renderComponentSync(component) | ||
} | ||
} | ||
function rendered(component, newVnode) { | ||
let i; | ||
// Store the new vnode inside the component so we can diff it next render | ||
component.vnode = newVnode; | ||
component.vnode = newVnode | ||
// Lift any 'remove' hook to our placeholder vnode for it to be called | ||
// as the placeholder is all our parent vnode knows about. | ||
if ((i = newVnode.data.hook) && (i = i.remove)) | ||
component.placeholder.data.hook.remove = i; | ||
const hook = newVnode.data.hook && newVnode.data.hook.remove | ||
if (hook) component.placeholder.data.hook.remove = hook | ||
} | ||
function destroy(vnode) { | ||
const comp = vnode.data.component; | ||
comp.vnode.elm.__comp__ = null; | ||
const comp = vnode.data.component | ||
comp.vnode.elm.__comp__ = null | ||
destroyVnode(comp.vnode); | ||
comp.destroyed = true; | ||
comp.lifecycle.destroyed(); | ||
destroyVnode(comp.vnode) | ||
comp.destroyed = true | ||
for (let i = 0; i < comp.subscriptions.length; i++) { | ||
comp.subscriptions[i]() | ||
} | ||
} | ||
@@ -143,19 +175,19 @@ | ||
function destroyVnode(vnode) { | ||
const data = vnode.data; | ||
const data = vnode.data | ||
if (!data) return; | ||
if (data.hook && data.hook.destroy) data.hook.destroy(vnode); | ||
if (!data) return | ||
if (data.hook && data.hook.destroy) data.hook.destroy(vnode) | ||
// Can't invoke modules' destroy hook as they're hidden in snabbdom's closure | ||
if (vnode.children) vnode.children.forEach(destroyVnode); | ||
if (data.vnode) destroyVnode(data.vnode); | ||
if (vnode.children) vnode.children.forEach(destroyVnode) | ||
if (data.vnode) destroyVnode(data.vnode) | ||
} | ||
function getDepth(elm) { | ||
let depth = 0; | ||
let parent = elm.parentElement; | ||
let depth = 0 | ||
let parent = elm.parentElement | ||
while (parent) { | ||
depth++; | ||
parent = parent.parentElement; | ||
depth++ | ||
parent = parent.parentElement | ||
} | ||
return depth; | ||
return depth | ||
} |
import snabbdom from 'snabbdom'; | ||
import h from 'snabbdom/h'; | ||
import snabbdom from 'snabbdom' | ||
import h from 'snabbdom/h' | ||
import Render, { renderApp } from './render'; | ||
import Component from './component'; | ||
import Message from './message'; | ||
import { GlobalStream } from './globalStream'; | ||
import Events, { snabbdomModule } from './events'; | ||
import log from './log'; | ||
import Render, { renderApp } from './render' | ||
import Component from './component' | ||
import Message from './message' | ||
import { snabbdomModule } from './events' | ||
import log from './log' | ||
function startApp({ app, elm, snabbdomModules }) { | ||
const modules = snabbdomModules.concat(snabbdomModule); | ||
patch = Render.patch = snabbdom.init(modules); | ||
renderApp(app, elm); | ||
const modules = snabbdomModules.concat(snabbdomModule) | ||
patch = Render.patch = snabbdom.init(modules) | ||
renderApp(app, elm) | ||
} | ||
let patch; | ||
let patch | ||
@@ -24,6 +23,4 @@ export { | ||
Message, | ||
Events, | ||
startApp, | ||
log, | ||
GlobalStream, | ||
patch, | ||
@@ -34,2 +31,2 @@ | ||
h | ||
}; | ||
} |
@@ -1,30 +0,30 @@ | ||
import most from 'most'; | ||
import { Set } from './util'; | ||
import { _sendToNode } from './messages'; | ||
import { Set } from './util' | ||
import { _sendToElement } from './messages' | ||
/* snabbdom module extension used to register Messages as event listeners */ | ||
function updateEventListeners(oldVnode, vnode) { | ||
const oldEvents = oldVnode.data.events; | ||
const events = vnode.data.events; | ||
const oldEvents = oldVnode.data.events | ||
const events = vnode.data.events | ||
if (!events) return; | ||
if (!events) return | ||
for (name in events) { | ||
const current = events[name]; | ||
const old = oldEvents && oldEvents[name]; | ||
const current = events[name] | ||
const old = oldEvents && oldEvents[name] | ||
if (old !== current) | ||
vnode.elm[name.toLowerCase()] = function(evt) { | ||
const [msg, arg] = Array.isArray(current) ? current : [current, evt]; | ||
_sendToNode(evt.target, msg(arg)); | ||
}; | ||
const [msg, arg] = Array.isArray(current) ? current : [current, evt] | ||
_sendToElement(evt.target, msg(arg)) | ||
} | ||
} | ||
if (!oldEvents) return; | ||
if (!oldEvents) return | ||
for (name in oldEvents) { | ||
if (events[name] == null) | ||
vnode.elm[name.toLowerCase()] = null; | ||
vnode.elm[name.toLowerCase()] = null | ||
} | ||
@@ -37,70 +37,1 @@ } | ||
} | ||
/* Listens to a DOM Event using delegation */ | ||
export default { | ||
listenAt: function(el, sel, name) { | ||
return most.create(add => { | ||
const listener = evt => { | ||
if (targetMatches(evt.target, sel, el)) add(evt); | ||
} | ||
const useCapture = name in nonBubblingEvents; | ||
el.addEventListener(name, listener, useCapture); | ||
return function unsub() { | ||
el.removeEventListener(name, listener, useCapture); | ||
}; | ||
}) | ||
} | ||
}; | ||
const proto = Element.prototype; | ||
const nativeMatches = proto.matches | ||
|| proto.matchesSelector | ||
|| proto.webkitMatchesSelector | ||
|| proto.mozMatchesSelector | ||
|| proto.msMatchesSelector | ||
|| proto.oMatchesSelector; | ||
function matches(el, selector) { | ||
return nativeMatches.call(el, selector); | ||
} | ||
const nonBubblingEvents = Set( | ||
'load', | ||
'unload', | ||
'focus', | ||
'blur', | ||
'mouseenter', | ||
'mouseleave', | ||
'submit', | ||
'change', | ||
'reset', | ||
'timeupdate', | ||
'playing', | ||
'waiting', | ||
'seeking', | ||
'seeked', | ||
'ended', | ||
'loadedmetadata', | ||
'loadeddata', | ||
'canplay', | ||
'canplaythrough', | ||
'durationchange', | ||
'play', | ||
'pause', | ||
'ratechange', | ||
'volumechange', | ||
'suspend', | ||
'emptied', | ||
'stalled', | ||
); | ||
function targetMatches(target, selector, root) { | ||
for (let el = target; el && el !== root; el = el.parentElement) { | ||
if (matches(el, selector)) return true; | ||
} | ||
return false; | ||
} |
@@ -5,3 +5,7 @@ | ||
render: false, | ||
stream: false | ||
message: false | ||
} | ||
export function shouldLog(log, key) { | ||
return (log === true || log === key) | ||
} |
let messageId = 1; | ||
let messageId = 1 | ||
@@ -7,18 +7,18 @@ /* User-defined component message factory */ | ||
const _id = messageId++; | ||
const _id = messageId++ | ||
function message(payload) { | ||
return { _id, _name: name, payload }; | ||
return { _id, _name: name, payload } | ||
} | ||
message._id = _id; | ||
message._name = name; | ||
message._isMessage = true; | ||
message._id = _id | ||
message._name = name | ||
message._isMessage = true | ||
message.with = payload => [message, payload]; | ||
message.with = payload => [message, payload] | ||
// Allows Actions to be used as Object keys with the correct behavior | ||
message.toString = () => _id; | ||
// Allows Messages to be used as Object keys with the correct behavior | ||
message.toString = () => _id | ||
return message; | ||
return message | ||
} |
@@ -1,95 +0,82 @@ | ||
import most from 'most'; | ||
import log from './log'; | ||
import Observable from './observable' | ||
export default function Messages(componentDestruction) { | ||
this.componentDestruction = componentDestruction; | ||
}; | ||
export default function Messages() {} | ||
Messages.prototype.listen = function(messageType) { | ||
this.subs = this.subs || []; | ||
this.subs = this.subs || [] | ||
return most.create(add => { | ||
return Observable.create(add => { | ||
const sub = { | ||
messageType, | ||
streamAdd: add | ||
}; | ||
observableAdd: add | ||
} | ||
this.subs.push(sub); | ||
this.subs.push(sub) | ||
return () => { | ||
this.subs.splice(this.subs.indexOf(sub), 1); | ||
this.subs.splice(this.subs.indexOf(sub), 1) | ||
} | ||
}) | ||
.until(this.componentDestruction); | ||
}; | ||
}).named(messageType._name) | ||
} | ||
Messages.prototype.listenAt = function(nodeSelector, messageType) { | ||
return most.create(add => { | ||
const el = document.querySelector(nodeSelector); | ||
if (!el) return; | ||
return Observable.create(add => { | ||
const el = document.querySelector(nodeSelector) | ||
if (!el) return | ||
el.__subs__ = el.__subs__ || []; | ||
const subs = el.__subs__; | ||
el.__subs__ = el.__subs__ || [] | ||
const subs = el.__subs__ | ||
const sub = { | ||
messageType, | ||
streamAdd: add | ||
observableAdd: add | ||
} | ||
subs.push(sub); | ||
subs.push(sub) | ||
return () => { | ||
subs.splice(subs.indexOf(sub), 1); | ||
if (subs.length === 0) el.__subs__ = undefined; | ||
subs.splice(subs.indexOf(sub), 1) | ||
if (subs.length === 0) el.__subs__ = undefined | ||
} | ||
}) | ||
.until(this.componentDestruction); | ||
}; | ||
}).named(messageType._name) | ||
} | ||
Messages.prototype.send = function(msg) { | ||
if (!this.el) | ||
throw new Error('Messages.send cannot be called synchronously in connect()'); | ||
this._receive(msg) | ||
} | ||
_sendToNode(this.el, msg); | ||
}; | ||
Messages.prototype.sendToParent = function(msg) { | ||
if (!this.el) throw new Error('Messages.send cannot be called synchronously in connect()') | ||
_sendToElement(this.el.parentElement, msg) | ||
} | ||
Messages.prototype._activate = function(el) { | ||
this.el = el; | ||
}; | ||
this.el = el | ||
} | ||
Messages.prototype._receive = function(msg) { | ||
const subs = this.subs; | ||
if (!subs) return; | ||
const subs = this.subs | ||
if (!subs) return | ||
for (let i = 0; i < subs.length; i++) { | ||
const sub = subs[i]; | ||
const sub = subs[i] | ||
if (sub.messageType._id === msg._id) | ||
sub.streamAdd(msg.payload); | ||
sub.observableAdd(msg.payload) | ||
} | ||
}; | ||
} | ||
export function _sendToNode(node, msg) { | ||
let parentEl = node.parentElement; | ||
while (parentEl) { | ||
export function _sendToElement(el, msg) { | ||
while (el) { | ||
// Classic component's listen | ||
if (parentEl.__comp__) { | ||
if (log) console.log('%c' + msg._name, 'color: #FAACF3', | ||
'sent locally to', parentEl , 'with payload ', msg.payload); | ||
return parentEl.__comp__.messages._receive(msg); | ||
} | ||
if (el.__comp__) | ||
return el.__comp__.messages._receive(msg) | ||
// listenAt | ||
else if (parentEl.__subs__) | ||
return parentEl.__subs__ | ||
else if (el.__subs__) | ||
return el.__subs__ | ||
.filter(sub => sub.messageType._id === msg._id) | ||
.forEach(sub => { | ||
if (log) console.log('%c' + msg._name, 'color: #FAACF3', | ||
'sent locally to', parentEl , 'with payload ', msg.payload); | ||
sub.streamAdd(msg.payload); | ||
}); | ||
.forEach(sub => sub.observableAdd(msg.payload)) | ||
parentEl = parentEl.parentElement; | ||
el = el.parentElement | ||
} | ||
}; | ||
} |
import h from 'snabbdom/h'; | ||
import Vnode from 'snabbdom/vnode'; | ||
import log from './log'; | ||
import h from 'snabbdom/h' | ||
import Vnode from 'snabbdom/vnode' | ||
import log, { shouldLog } from './log' | ||
let componentsToRender = []; | ||
let newComponents = []; | ||
let rendering = false; | ||
let nextRender; | ||
let componentsToRender = [] | ||
let newComponents = [] | ||
let rendering = false | ||
let nextRender | ||
const Render = { patch: undefined }; | ||
export default Render; | ||
const Render = { patch: undefined } | ||
export default Render | ||
export function renderApp(app, appElm) { | ||
logBeginRender(); | ||
logBeginRender() | ||
const el = document.createElement('div'); | ||
const emptyVnode = Vnode('div', { key: '_init' }, [], undefined, el); | ||
const appNode = Render.patch(emptyVnode, app); | ||
const el = document.createElement('div') | ||
const emptyVnode = Vnode('div', { key: '_init' }, [], undefined, el) | ||
const appNode = Render.patch(emptyVnode, app) | ||
newComponents.forEach(c => c.lifecycle.inserted(c)); | ||
newComponents.forEach(c => c.lifecycle.inserted(c)) | ||
appElm.appendChild(appNode.elm); | ||
appElm.appendChild(appNode.elm) | ||
logEndRender(); | ||
logEndRender() | ||
} | ||
@@ -31,4 +31,4 @@ | ||
if (rendering) { | ||
console.warn('A component tried to re-render while a rendering was already ongoing', component.elm); | ||
return; | ||
console.warn('A component tried to re-render while a rendering was already ongoing', component.elm) | ||
return | ||
} | ||
@@ -39,71 +39,72 @@ | ||
// Avoids doing more work than necessary when re-activating it. | ||
if (componentsToRender.indexOf(component) !== -1) return; | ||
if (componentsToRender.indexOf(component) !== -1) return | ||
componentsToRender.push(component); | ||
componentsToRender.push(component) | ||
if (!nextRender) | ||
nextRender = requestAnimationFrame(renderNow); | ||
}; | ||
nextRender = requestAnimationFrame(renderNow) | ||
} | ||
export function renderComponentSync(component) { | ||
renderComponent(component, true); | ||
}; | ||
renderComponent(component, true) | ||
} | ||
function renderComponent(component, checkRenderQueue) { | ||
const { props, state, elm, render, vnode, destroyed } = component; | ||
const { props, state, elm, render, vnode, destroyed } = component | ||
// Bail if the component is already destroyed. | ||
// This can happen if the parent renders first and decide a child component should be removed. | ||
if (destroyed) return; | ||
if (destroyed) return | ||
// The component is going to be rendered later as part of the normal | ||
// render process, do not force-render it now. | ||
if (checkRenderQueue && componentsToRender.indexOf(component) !== -1) return; | ||
if (checkRenderQueue && componentsToRender.indexOf(component) !== -1) return | ||
const isNew = vnode === undefined; | ||
const { patch } = Render; | ||
const isNew = vnode === undefined | ||
const { patch } = Render | ||
let beforeRender; | ||
let beforeRender | ||
if (log.render) beforeRender = performance.now(); | ||
const newVnode = render(props, state); | ||
if (log.render) beforeRender = performance.now() | ||
const newVnode = render(props, state) | ||
patch(vnode || Vnode('div', { key: '_init' }, [], undefined, elm), newVnode); | ||
patch(vnode || Vnode('div', { key: '_init' }, [], undefined, elm), newVnode) | ||
if (log.render) { | ||
const renderTime = Math.round((performance.now() - beforeRender) * 100) / 100; | ||
console.log(`Render component %c${component.key}`, 'font-weight: bold', renderTime + ' ms', props, state); | ||
if (shouldLog(log.render, component.key)) { | ||
const renderTime = Math.round((performance.now() - beforeRender) * 100) / 100 | ||
console.log(`Render component %c${component.key}`, | ||
'font-weight: bold', renderTime + ' ms', '| props: ', props, '| state: ',state) | ||
} | ||
component.lifecycle.rendered(component, newVnode); | ||
if (isNew) newComponents.push(component); | ||
component.lifecycle.rendered(component, newVnode) | ||
if (isNew) newComponents.push(component) | ||
} | ||
function renderNow() { | ||
rendering = true; | ||
rendering = true | ||
nextRender = undefined; | ||
newComponents = []; | ||
nextRender = undefined | ||
newComponents = [] | ||
logBeginRender(); | ||
logBeginRender() | ||
// Render components in a top-down fashion. | ||
// This ensures the rendering order is predictive and props & states are consistent. | ||
componentsToRender.sort((compA, compB) => compA.depth - compB.depth); | ||
componentsToRender.forEach(c => renderComponent(c, false)); | ||
componentsToRender.sort((compA, compB) => compA.depth - compB.depth) | ||
componentsToRender.forEach(c => renderComponent(c, false)) | ||
rendering = false; | ||
componentsToRender = []; | ||
rendering = false | ||
componentsToRender = [] | ||
newComponents.forEach(c => c.lifecycle.inserted(c)); | ||
newComponents.forEach(c => c.lifecycle.inserted(c)) | ||
logEndRender(); | ||
logEndRender() | ||
} | ||
function logBeginRender() { | ||
if (log.render) console.log('%cRender - begin', 'color: orange'); | ||
if (log.render) console.log('%cRender - begin', 'color: orange') | ||
} | ||
function logEndRender() { | ||
if (log.render) console.log('%cRender - end\n\n', 'color: orange'); | ||
if (log.render) console.log('%cRender - end\n\n\n', 'color: orange') | ||
} |
export function Set() { | ||
let set = {}; | ||
let set = {} | ||
for (let i = 0; i < arguments.length; i++) { | ||
set[arguments[i]] = 1; | ||
set[arguments[i]] = 1 | ||
} | ||
return set; | ||
}; | ||
return set | ||
} | ||
/* Efficient shallow comparison of two objects */ | ||
export function shallowEqual(objA, objB) { | ||
if (objA === objB) return true | ||
const keysA = Object.keys(objA) | ||
const keysB = Object.keys(objB) | ||
// Test for A's keys different from B's. | ||
for (var i = 0; i < keysA.length; i++) { | ||
if (objA[keysA[i]] !== objB[keysA[i]]) return false | ||
} | ||
// Test for B's keys different from A's. | ||
// Handles the case where B has a property that A doesn't. | ||
for (var i = 0; i < keysB.length; i++) { | ||
if (objA[keysB[i]] !== objB[keysB[i]]) return false | ||
} | ||
return true | ||
} |
@@ -10,7 +10,17 @@ { | ||
"filesGlob": [ | ||
"typings/**/*.d.ts" | ||
"src/**/*.d.ts" | ||
], | ||
"files": [ | ||
"typings/dompteuse.d.ts" | ||
"src/dompteuse.d.ts", | ||
"src/observable/debounce/index.d.ts", | ||
"src/observable/delay/index.d.ts", | ||
"src/observable/flatMapLatest/index.d.ts", | ||
"src/observable/fromPromise/index.d.ts", | ||
"src/observable/index.d.ts", | ||
"src/observable/map/index.d.ts", | ||
"src/observable/merge/index.d.ts", | ||
"src/observable/partition/index.d.ts", | ||
"src/observable/pure/index.d.ts", | ||
"src/store/index.d.ts" | ||
] | ||
} |
Sorry, the diff of this file is too big to display
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
98
78.18%541
53.26%238234
-82.69%3477
-76.02%2
Infinity%