From 9e1840d2abc4dfeb487fb540e80cc939ccb07a9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Cevey?= Date: Mon, 1 Jun 2015 23:41:00 +0300 Subject: [PATCH 1/6] Strip to demo base --- demo/app.js | 169 ++++++++---------------------------------------- demo/helpers.js | 33 ++++++++++ 2 files changed, 59 insertions(+), 143 deletions(-) create mode 100644 demo/helpers.js diff --git a/demo/app.js b/demo/app.js index 8b54924..004ecb5 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; @@ -37,71 +40,21 @@ 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]) - ); -} - - -// 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) - }); - return { - model: { - value$: inputted$.map(event => event.target.value).startWith('') - }, - tree$: $Obs.return(inputEl) - }; -} - - - -function checkboxEl(checked, changed$) { +function inputElement(value, changed$) { return h('input', { - type: 'checkbox', - checked: checked, - onchange: (event) => changed$.onNext(event) + type: 'text', + placeholder: 'Enter query...', + value: value, + oninput: (event) => changed$.onNext(event) }); } -function checkboxView() { +function inputView() { const changed$ = new Rx.Subject; function render$(model) { - return model.checked$.map(checked => checkboxEl(checked, changed$)); + return model.value$.map(val => inputElement(val, changed$)); } return { @@ -110,118 +63,47 @@ function checkboxView() { }; } -function checkboxIntents(view) { +function inputIntent(view) { return { - change$: view.events.changed$.map(event => event.target.checked) + update$: view.events.changed$.map(ev => ev.target.value) }; } -function checkboxModel(intents) { +function inputModel(intent) { return { - checked$: intents.change$.startWith(false) + value$: intent.update$.startWith('') }; } -function checkbox() { - const view = checkboxView(); - const model = checkboxModel(checkboxIntents(view)); + +function inputComponent() { + const view = inputView(); + const model = inputModel(inputIntent(view)); return { - model: { - value$: model.checked$ - }, + model, tree$: view.render$(model) }; } -function freeFilter() { - const choice = checkbox(); - const tree$ = choice.tree$.map(tree => h('label', [tree, 'free only'])); - - return { - model: choice.model, - tree$: tree$ - }; -} -function filtersView() { - const queryInput = input(); - const freeChoice = freeFilter(); +function filtersComponent() { + const query = inputComponent(); return { model: { - query$: queryInput.model.value$, - free$: freeChoice.model.value$ + query$: query.model.value$ }, - // tree$: $Obs.combineLatest( - // queryInput.tree$, - // freeChoice.tree$, - // (queryInputTree, freeChoiceTree) => { - // return h('form', [queryInputTree, freeChoiceTree]); - // }) - tree$: container$('form', [queryInput.tree$, freeChoice.tree$]) + tree$: query.tree$ }; } -function imageCell(src, loaded) { - const state = loaded ? 'loaded' : 'loading'; - return h('span', {className: `image image--${state}`}, [ - h('img', {src: 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; - - return preloadImage$(imageUrl). - map(() => imageCell(imageUrl, true)). - startWith(imageCell(imageUrl, false)); - }); - - return sequenceCombine$(imageEls$).map(imageEls => { - return h('div', imageEls); - }); - }); - - const tree$ = container$('div', [ - resultsHeader$, - imageList$ - ]); - - 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)); - - const results = resultsComponent({query$, results$}); + const filters = filtersComponent(); - const tree$ = container$('div', [filters.tree$, results.tree$]); + const tree$ = filters.tree$; return { tree$ @@ -238,6 +120,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/helpers.js b/demo/helpers.js new file mode 100644 index 0000000..2902113 --- /dev/null +++ b/demo/helpers.js @@ -0,0 +1,33 @@ +import h from 'virtual-dom/h'; +import Rx from 'rx'; + + +// Convenience +const $Obs = Rx.Observable; + +export function container$(tagName, children) { + return $Obs.combineLatest( + children, + (...views) => h(tagName, [...views]) + ); +} + +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); +} From 5559c0c689496e58f81997efbaae03a2f8e92bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Cevey?= Date: Tue, 2 Jun 2015 09:36:25 +0300 Subject: [PATCH 2/6] Export helpers --- demo/helpers.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/demo/helpers.js b/demo/helpers.js index 2902113..8b973eb 100644 --- a/demo/helpers.js +++ b/demo/helpers.js @@ -10,9 +10,9 @@ export function container$(tagName, children) { children, (...views) => h(tagName, [...views]) ); -} +}; -function sequenceCombine$(observables$) { +export function sequenceCombine$(observables$) { // Work around odd behaviour of combineLatest with empty Array // (never yields a value) if (observables$.length === 0) { @@ -20,9 +20,9 @@ function sequenceCombine$(observables$) { } else { return Rx.Observable.combineLatest(observables$, (...all) => all); } -} +}; -function preloadImage$(src) { +export function preloadImage$(src) { const loaded = new Promise((resolve, reject) => { const image = new Image(); image.addEventListener('load', resolve); @@ -30,4 +30,4 @@ function preloadImage$(src) { image.src = src; }); return Rx.Observable.fromPromise(loaded); -} +}; From 660e6495787448211ca4c2b12137a2e04ac701f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Cevey?= Date: Mon, 15 Jun 2015 16:52:56 +0100 Subject: [PATCH 3/6] Some more base code --- demo/app.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/demo/app.js b/demo/app.js index 004ecb5..805fa85 100644 --- a/demo/app.js +++ b/demo/app.js @@ -19,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`, @@ -99,11 +99,30 @@ function filtersComponent() { }; } +function imageCell(src) { + return h(`span.image`, [ + h('img', {src}) + ]); +} + +function resultListComponent() { + const heading$ = $Obs.return(h('h2', `Search results`)); + + const tree$ = heading$; + + return {tree$}; +} + function view() { const filters = filtersComponent(); - const tree$ = filters.tree$; + const resultList = resultListComponent(); + + const tree$ = container$('div', [ + filters.tree$, + resultList.tree$ + ]); return { tree$ From a38bf32a45267fb3751c9b0676f80c401642fcd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Cevey?= Date: Mon, 15 Jun 2015 17:16:17 +0100 Subject: [PATCH 4/6] More code --- demo/helpers.js | 6 ++---- demo/style.css | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/demo/helpers.js b/demo/helpers.js index 8b973eb..b7588b5 100644 --- a/demo/helpers.js +++ b/demo/helpers.js @@ -6,10 +6,8 @@ import Rx from 'rx'; const $Obs = Rx.Observable; export function container$(tagName, children) { - return $Obs.combineLatest( - children, - (...views) => h(tagName, [...views]) - ); + return sequenceCombine$(children). + map(views => h(tagName, [...views])); }; export function sequenceCombine$(observables$) { 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; } From 85175d451b35ecff1b473cd3423407ddd2aaeecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Cevey?= Date: Mon, 15 Jun 2015 17:21:15 +0100 Subject: [PATCH 5/6] Move code around --- demo/app.js | 55 +++++++++++++++++++---------------------------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/demo/app.js b/demo/app.js index 805fa85..429e6c3 100644 --- a/demo/app.js +++ b/demo/app.js @@ -40,49 +40,32 @@ function searchImages$({query, free}) { // import {searchImages} from './fake-api'; +import {inputComponent} from './components'; -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$} - }; -} +/* +export function inputComponent() { + const view = inputView(); + const model = inputModel(inputIntent(view)); -function inputIntent(view) { return { - update$: view.events.changed$.map(ev => ev.target.value) + model: { + value$: model.value$ + }, + tree$: view.render$(model) }; } +*/ -function inputModel(intent) { - return { - value$: intent.update$.startWith('') - }; -} +import {checkboxComponent} from './components'; -function inputComponent() { - const view = inputView(); - const model = inputModel(inputIntent(view)); +function freeFilter() { + const choice = checkboxComponent(); + const tree$ = choice.tree$.map(tree => h('label', [tree, 'free only'])); return { - model, - tree$: view.render$(model) + model: choice.model, + tree$: tree$ }; } @@ -90,12 +73,14 @@ function inputComponent() { function filtersComponent() { const query = inputComponent(); + const free = freeFilter(); return { model: { - query$: query.model.value$ + query$: query.model.value$, + free$: free.model.value$ }, - tree$: query.tree$ + tree$: container$('form', [query.tree$, free.tree$]) }; } From 057ce89c6cbe2bec3cfac009b216ea075acb827e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Cevey?= Date: Mon, 15 Jun 2015 17:47:29 +0100 Subject: [PATCH 6/6] Missing file, duh --- demo/components.js | 96 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 demo/components.js 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) + }; +}