diff --git a/demo/app.js b/demo/app.js index 8b54924..429e6c3 100644 --- a/demo/app.js +++ b/demo/app.js @@ -6,6 +6,9 @@ import Rx from 'rx'; import virtualize from 'vdom-virtualize'; +// Other helpers +import {container$, sequenceCombine$, preloadImage$} from './helpers'; + // Convenience const $Obs = Rx.Observable; @@ -16,7 +19,7 @@ import reqwest from 'reqwest'; import {mediaApiUri} from './api-config'; -function searchImages({query, free}) { +function searchImages$({query, free}) { const freeFilter = free ? {free: true} : {}; const req = reqwest({ url: `${mediaApiUri}/images`, @@ -37,106 +40,27 @@ function searchImages({query, free}) { // import {searchImages} from './fake-api'; -// Other helpers -function sequenceCombine$(observables$) { - // Work around odd behaviour of combineLatest with empty Array - // (never yields a value) - if (observables$.length === 0) { - return Rx.Observable.return([]); - } else { - return Rx.Observable.combineLatest(observables$, (...all) => all); - } -} - -function preloadImage$(src) { - const loaded = new Promise((resolve, reject) => { - const image = new Image(); - image.addEventListener('load', resolve); - image.addEventListener('error', reject); - image.src = src; - }); - return Rx.Observable.fromPromise(loaded); -} - - - - -function container$(tagName, children) { - return $Obs.combineLatest( - children, - (...views) => h(tagName, [...views]) - ); -} - +import {inputComponent} from './components'; -// TODO: split into MVI? -// TODO: close loop? (V = f(M)) -function input() { - const inputted$ = new Rx.Subject; - const inputEl = h('input', { - type: 'text', - placeholder: 'Search images...', - oninput: (event) => inputted$.onNext(event) - }); +/* +export function inputComponent() { + const view = inputView(); + const model = inputModel(inputIntent(view)); return { model: { - value$: inputted$.map(event => event.target.value).startWith('') - }, - tree$: $Obs.return(inputEl) - }; -} - - - -function checkboxEl(checked, changed$) { - return h('input', { - type: 'checkbox', - checked: checked, - onchange: (event) => changed$.onNext(event) - }); -} - -function checkboxView() { - const changed$ = new Rx.Subject; - - function render$(model) { - return model.checked$.map(checked => checkboxEl(checked, changed$)); - } - - return { - render$, - events: {changed$} - }; -} - -function checkboxIntents(view) { - return { - change$: view.events.changed$.map(event => event.target.checked) - }; -} - -function checkboxModel(intents) { - return { - checked$: intents.change$.startWith(false) - }; -} - -function checkbox() { - const view = checkboxView(); - const model = checkboxModel(checkboxIntents(view)); - - return { - model: { - value$: model.checked$ + value$: model.value$ }, tree$: view.render$(model) }; } +*/ + +import {checkboxComponent} from './components'; function freeFilter() { - const choice = checkbox(); + const choice = checkboxComponent(); const tree$ = choice.tree$.map(tree => h('label', [tree, 'free only'])); return { @@ -145,83 +69,45 @@ function freeFilter() { }; } -function filtersView() { - const queryInput = input(); - const freeChoice = freeFilter(); + + +function filtersComponent() { + const query = inputComponent(); + const free = freeFilter(); return { model: { - query$: queryInput.model.value$, - free$: freeChoice.model.value$ + query$: query.model.value$, + free$: free.model.value$ }, - // tree$: $Obs.combineLatest( - // queryInput.tree$, - // freeChoice.tree$, - // (queryInputTree, freeChoiceTree) => { - // return h('form', [queryInputTree, freeChoiceTree]); - // }) - tree$: container$('form', [queryInput.tree$, freeChoice.tree$]) + tree$: container$('form', [query.tree$, free.tree$]) }; } - -function imageCell(src, loaded) { - const state = loaded ? 'loaded' : 'loading'; - return h('span', {className: `image image--${state}`}, [ - h('img', {src: src}) +function imageCell(src) { + return h(`span.image`, [ + h('img', {src}) ]); } -function resultsComponent({results$, query$}) { - const resultsHeader$ = query$.map(query => { - return h('h2', `Search results for “${query}”`); - }); - - const imageList$ = results$.flatMapLatest(results => { - const imageEls$ = results.data.map(result => { - const imageUrl = result.data.thumbnail.secureUrl; +function resultListComponent() { + const heading$ = $Obs.return(h('h2', `Search results`)); - return preloadImage$(imageUrl). - map(() => imageCell(imageUrl, true)). - startWith(imageCell(imageUrl, false)); - }); + const tree$ = heading$; - return sequenceCombine$(imageEls$).map(imageEls => { - return h('div', imageEls); - }); - }); - - const tree$ = container$('div', [ - resultsHeader$, - imageList$ - ]); - - return { - tree$ - }; + return {tree$}; } -function view() { - const filters = filtersView(); - - const query$ = filters.model.query$.debounce(500); - const free$ = filters.model.free$; - - const pollPeriod = 10000; - - const searchQuery$ = Rx.Observable.combineLatest( - query$, - free$, - $Obs.timer(0, pollPeriod), // emits to refresh - (query, free) => ({query, free}) - ); - const results$ = searchQuery$. - flatMapLatest(searchQuery => searchImages(searchQuery)); +function view() { + const filters = filtersComponent(); - const results = resultsComponent({query$, results$}); + const resultList = resultListComponent(); - const tree$ = container$('div', [filters.tree$, results.tree$]); + const tree$ = container$('div', [ + filters.tree$, + resultList.tree$ + ]); return { tree$ @@ -238,6 +124,7 @@ const theView = view(); theView.tree$. startWith(initialDom). bufferWithCount(2, 1). + filter(pair => pair.length == 2). map(([last, current]) => diff(last, current)). reduce((out, patches) => patch(out, patches), out). subscribeOnError(err => console.error(err)); diff --git a/demo/components.js b/demo/components.js new file mode 100644 index 0000000..0d968ba --- /dev/null +++ b/demo/components.js @@ -0,0 +1,96 @@ +import h from 'virtual-dom/h'; + +import Rx from 'rx'; + + +function inputElement(value, changed$) { + return h('input', { + type: 'text', + placeholder: 'Enter query...', + value: value, + oninput: (event) => changed$.onNext(event) + }); +} + +function inputView() { + const changed$ = new Rx.Subject; + + function render$(model) { + return model.value$.map(val => inputElement(val, changed$)); + } + + return { + render$, + events: {changed$} + }; +} + +function inputIntent(view) { + return { + update$: view.events.changed$.map(ev => ev.target.value) + }; +} + +function inputModel(intent) { + return { + value$: intent.update$.startWith('') + }; +} + + +export function inputComponent() { + const view = inputView(); + const model = inputModel(inputIntent(view)); + + return { + model, + tree$: view.render$(model) + }; +} + + + +function checkboxEl(checked, changed$) { + return h('input', { + type: 'checkbox', + checked: checked, + onchange: (event) => changed$.onNext(event) + }); +} + +function checkboxView() { + const changed$ = new Rx.Subject; + + function render$(model) { + return model.checked$.map(checked => checkboxEl(checked, changed$)); + } + + return { + render$, + events: {changed$} + }; +} + +function checkboxIntents(view) { + return { + change$: view.events.changed$.map(event => event.target.checked) + }; +} + +function checkboxModel(intents) { + return { + checked$: intents.change$.startWith(false) + }; +} + +export function checkboxComponent() { + const view = checkboxView(); + const model = checkboxModel(checkboxIntents(view)); + + return { + model: { + value$: model.checked$ + }, + tree$: view.render$(model) + }; +} diff --git a/demo/helpers.js b/demo/helpers.js new file mode 100644 index 0000000..b7588b5 --- /dev/null +++ b/demo/helpers.js @@ -0,0 +1,31 @@ +import h from 'virtual-dom/h'; +import Rx from 'rx'; + + +// Convenience +const $Obs = Rx.Observable; + +export function container$(tagName, children) { + return sequenceCombine$(children). + map(views => h(tagName, [...views])); +}; + +export function sequenceCombine$(observables$) { + // Work around odd behaviour of combineLatest with empty Array + // (never yields a value) + if (observables$.length === 0) { + return Rx.Observable.return([]); + } else { + return Rx.Observable.combineLatest(observables$, (...all) => all); + } +}; + +export function preloadImage$(src) { + const loaded = new Promise((resolve, reject) => { + const image = new Image(); + image.addEventListener('load', resolve); + image.addEventListener('error', reject); + image.src = src; + }); + return Rx.Observable.fromPromise(loaded); +}; diff --git a/demo/style.css b/demo/style.css index 2e895c0..ef24582 100644 --- a/demo/style.css +++ b/demo/style.css @@ -44,13 +44,13 @@ img { } .image img { - transition: opacity 0.2s ease-in; + transition: opacity 0.3s ease-in; -webkit-transition: opacity 0.3s ease-in; opacity: 1; } .image--loading img { - transition: opacity 0.2s ease-in; + transition: opacity 0.3s ease-in; -webkit-transition: opacity 0.3s ease-in; opacity: 0; }