From de96a33ea5e51379126f10e4eac7b48b56088c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sta=C5=9B=20Ma=C5=82olepszy?= Date: Thu, 21 Sep 2017 00:45:58 +0200 Subject: [PATCH 1/3] Pass an optional selector to connect --- CHANGELOG.md | 8 +++++ index.js | 4 +-- test/connect_test.js | 74 ++++++++++++++++++++++++++++++++++++-------- 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd48ae8..2f5f22f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased + - `connect` now takes a selector as an optional second argument. + + You can now pass an optional selector function to `connect`. It will be + passed the `state` and should return whatever the connected component + expects as the first argument. + + connect(FooComponent, state => state.foo); + - Filter out booleans in the html helper. Previously only `null` and `undefined` were filtered out. Now both `true` diff --git a/index.js b/index.js index 021a63a..0f7f9e4 100644 --- a/index.js +++ b/index.js @@ -44,9 +44,9 @@ export function createStore(reducer) { roots.set(root, component); render(); }, - connect(component) { + connect(component, selector = state => state) { // Return a decorated component function. - return (...args) => component(state, ...args); + return (...args) => component(selector(state), ...args); }, dispatch(action, ...args) { state = reducer(state, action, args); diff --git a/test/connect_test.js b/test/connect_test.js index 275f32e..8963fd1 100644 --- a/test/connect_test.js +++ b/test/connect_test.js @@ -3,36 +3,38 @@ require = require("@std/esm")(module, {esm: "js"}); const assert = require("assert"); const { createStore } = require("../index") -function counter(state = 0, action) { - switch (action) { - case "INCREMENT": - return state + 1; - default: - return state; - } -} - suite("connect", function() { let store; let root; + function counter(state = 0, action) { + switch (action) { + case "INCREMENT": + return state + 1; + default: + return state; + } + } + setup(function() { store = createStore(counter); root = document.createElement("div"); }); - test("passes the current state as the first argument", function() { + test("pass the current state as the first argument", function() { const { attach, connect, dispatch } = store; const TestApp = spyFunction(); const ConnectedTestApp = connect(TestApp); + attach(ConnectedTestApp, root); - dispatch("INCREMENT"); + assert.deepEqual(TestApp.args(), [0]); + dispatch("INCREMENT"); assert.deepEqual(TestApp.args(), [1]); }); - test("passes other args after the state", function() { + test("pass other args after the state", function() { const { attach, connect, dispatch } = store; const TestComponent = spyFunction(); @@ -43,8 +45,54 @@ suite("connect", function() { } attach(TestApp, root); - dispatch("INCREMENT"); + assert.deepEqual(TestComponent.args(), [0, "Foo"]); + dispatch("INCREMENT"); assert.deepEqual(TestComponent.args(), [1, "Foo"]); }); }); + +suite("connect with a selector", function() { + let store; + let root; + + function counter(state = {current: 0}, action) { + switch (action) { + case "INCREMENT": + return {current: state.current + 1}; + default: + return state; + } + } + + setup(function() { + store = createStore(counter); + root = document.createElement("div"); + }); + + test("use the identity selector by default", function() { + const { attach, connect, dispatch } = store; + + const TestApp = spyFunction(); + const ConnectedTestApp = connect(TestApp); + + attach(ConnectedTestApp, root); + assert.deepEqual(TestApp.args(), [{current: 0}]); + + dispatch("INCREMENT"); + assert.deepEqual(TestApp.args(), [{current: 1}]); + }); + + test("apply the selector to the state", function() { + const { attach, connect, dispatch } = store; + + const TestApp = spyFunction(); + const ConnectedTestApp = connect(TestApp, state => state.current); + + attach(ConnectedTestApp, root); + assert.deepEqual(TestApp.args(), [0]); + + dispatch("INCREMENT"); + assert.deepEqual(TestApp.args(), [1]); + }); +}); From c7b2d603063fa87797981e2152a566c4fdf85a81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sta=C5=9B=20Ma=C5=82olepszy?= Date: Thu, 21 Sep 2017 00:56:57 +0200 Subject: [PATCH 2/3] Merge props of connected components --- CHANGELOG.md | 13 +++++++++++-- example01/ActiveList.js | 5 ++--- example01/ActiveTask.js | 4 ++-- example01/ArchivedList.js | 5 ++--- example01/ArchivedTask.js | 4 ++-- example03/App.js | 3 +-- example03/TextInput.js | 2 +- index.js | 3 ++- 8 files changed, 23 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f5f22f..03c4fc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,20 @@ ## Unreleased + - Encourage passing the `props` object as the first argument to components. + + Components are just functions so this isn't mandatory and you can still + define arguments as you see fit. The pattern of passing `props` makes + composition easier, encourages code which is more readable and decouples + the implementation of the component from the action of connecting it to the + store. From now on, connecting will only work for components with zero + arity or which take an object as the first argument. + - `connect` now takes a selector as an optional second argument. You can now pass an optional selector function to `connect`. It will be - passed the `state` and should return whatever the connected component - expects as the first argument. + passed the `state` and should return an object which will be merged with + the component's `props` using `Object.assign({}, props, substate)`. connect(FooComponent, state => state.foo); diff --git a/example01/ActiveList.js b/example01/ActiveList.js index e4cd2cb..420cd6e 100644 --- a/example01/ActiveList.js +++ b/example01/ActiveList.js @@ -3,12 +3,11 @@ import { connect } from "./store"; import ActiveTask from "./ActiveTask"; import TaskInput from "./TaskInput"; -function ActiveList(state) { - const { tasks } = state; +function ActiveList({tasks}) { return html`

My Active Tasks