├── .github └── workflows │ └── codeql.yml ├── .gitignore ├── README.md ├── adapters ├── factory.js ├── factory_test.js ├── helpers.js ├── helpers_test.js ├── objects.js ├── objects_test.js ├── stream.js └── stream_test.js ├── examples ├── README.md ├── active-counter │ ├── README.md │ ├── index.html │ ├── index.js │ ├── package.json │ └── yarn.lock ├── basic │ ├── keypress │ │ ├── README.md │ │ ├── index.html │ │ ├── index.js │ │ ├── package.json │ │ ├── un-mount.js │ │ └── yarn.lock │ ├── submit-with-reset │ │ ├── README.md │ │ ├── index.html │ │ ├── index.js │ │ ├── package.json │ │ ├── un-mount.js │ │ └── yarn.lock │ ├── submit │ │ ├── README.md │ │ ├── index.html │ │ ├── index.js │ │ ├── package.json │ │ ├── un-mount.js │ │ └── yarn.lock │ ├── todos-with-delete │ │ ├── README.md │ │ ├── app.css │ │ ├── index.html │ │ ├── index.js │ │ ├── package.json │ │ ├── un-mount.js │ │ └── yarn.lock │ └── todos │ │ ├── README.md │ │ ├── index.html │ │ ├── index.js │ │ ├── package.json │ │ ├── un-mount.js │ │ └── yarn.lock ├── interframeworkability │ ├── react-pure │ │ ├── LICENSE │ │ ├── README.md │ │ ├── devServer.js │ │ ├── index.html │ │ ├── package.json │ │ ├── source │ │ │ ├── App.js │ │ │ ├── App.jsx │ │ │ ├── components │ │ │ │ ├── hello │ │ │ │ │ ├── index.js │ │ │ │ │ └── index.jsx │ │ │ │ └── title │ │ │ │ │ └── index.js │ │ │ ├── debug.js │ │ │ ├── index.js │ │ │ ├── store │ │ │ │ └── reducers │ │ │ │ │ └── hello │ │ │ │ │ └── index.js │ │ │ ├── test-fixtures │ │ │ │ └── components │ │ │ │ │ └── hello │ │ │ │ │ └── create-actions.js │ │ │ └── test │ │ │ │ ├── App.js │ │ │ │ ├── components │ │ │ │ ├── hello │ │ │ │ │ └── index.js │ │ │ │ └── title │ │ │ │ │ └── index.js │ │ │ │ ├── index.js │ │ │ │ └── store │ │ │ │ └── reducers │ │ │ │ └── hello │ │ │ │ └── index.js │ │ ├── webpack.config.dev.js │ │ ├── webpack.config.js │ │ └── webpack.config.prod.js │ ├── react-redux-counter │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── pnpm-lock.yaml │ │ ├── public │ │ │ └── index.html │ │ ├── src │ │ │ ├── components │ │ │ │ ├── Counter.js │ │ │ │ └── Counter.spec.js │ │ │ ├── index.js │ │ │ └── reducers │ │ │ │ ├── index.js │ │ │ │ └── index.spec.js │ │ └── un-mount.js │ └── react-redux-todomvc │ │ ├── README.md │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── public │ │ └── index.html │ │ └── src │ │ ├── actions │ │ ├── index.js │ │ └── index.spec.js │ │ ├── components │ │ ├── Footer.js │ │ ├── Footer.spec.js │ │ ├── Header.js │ │ ├── Header.spec.js │ │ ├── MainSection.js │ │ ├── MainSection.spec.js │ │ ├── TodoItem.js │ │ ├── TodoItem.spec.js │ │ ├── TodoTextInput.js │ │ └── TodoTextInput.spec.js │ │ ├── constants │ │ ├── ActionTypes.js │ │ └── TodoFilters.js │ │ ├── containers │ │ └── App.js │ │ ├── index.js │ │ └── reducers │ │ ├── index.js │ │ ├── todos.js │ │ └── todos.spec.js └── todo-mvc │ ├── .snyk │ ├── app.css │ ├── base.css │ ├── index.html │ ├── index.js │ ├── package.json │ └── un-mount.js ├── index.html ├── index.js ├── karma.conf-old.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── renovate.json └── test_of_test.js /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "34 13 * * 3" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v3 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v3 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build* 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | __ __ _____ 3 | / / / / / ___ \ ( Logo inspiration from MostJS 4 | / / / / / / / / https://github.com/cujojs/most ) 5 | / /__/ / / / / / 6 | \_____/ /_/ /_/ 7 | ``` 8 | 9 | # un.js 10 | 11 | 12 | 13 | Unframework for Universal Uncomponents 14 | 15 | > **We do not think in terms of reusable components.** 16 | >Instead, we focus on reusable *functions*. 17 | >It is a functional language after all! 18 | >-- [Scaling The Elm Architecture](https://guide.elm-lang.org/reuse/) 19 | 20 | ### Contributions and Feedback are highly welcome! 21 | 22 | ## Quick start 23 | 24 | Install [the `un.js` package](https://www.npmjs.com/package/un.js) with 25 | 26 | ```sh 27 | $ yarn add un.js 28 | ``` 29 | or with 30 | ```sh 31 | npm install un.js -S 32 | ``` 33 | or with [`pnpm`](https://github.com/pnpm/pnpm) 34 | ```sh 35 | pnpm install un.js -S 36 | ``` 37 | and try the [active counter example](https://github.com/dmitriz/un/tree/master/examples/active-counter) or read the introduction below. 38 | 39 | ## Philosophy and Universality 40 | 41 | - Write your business logic as pure functions with no external dependencies 42 | - No side-effects, testable by your favorite test runners 43 | - No external imports, no packages, no libraries 44 | - No extension of proprietary component classes (Backbone, React, ...) 45 | - No lock-ins, your uncomponents should be usable as plugins with or without any framework 46 | - Reuse your code and share with others accross frameworks with no boundaries 47 | 48 | 49 | ## Why unframework? 50 | 51 | - Frameworks try to provide and cater for everything, `un` tries the opposite - give you the maximal possible freedom. 52 | 53 | - Frameworks try to tell you exactly what to do, `un` tries the opposite - staying out of your way. 54 | 55 | - Frameworks make your code coupled with proprietary syntax, `un` lets you write your code with plain JavaScript functions, undistinguishable from any other functions. There is not a single trace of `un` in any of your functions. 56 | 57 | - Frameworks often like you to inherit from their proprietary classes, `un` tries to help you embrace the pure functional style and minimise use of classes and `this`. However, this is merely suggestive. Giving you maximum freedom and staying out of your way is a higher priority for `un`. 58 | 59 | 60 | ## What is provided? 61 | 62 | Currently a single tiny factory function called `createMount`. [See here the complete code.](https://github.com/dmitriz/un/blob/master/index.js) Its role is similar to `React.render`, in which you would typically see it in only few places in your app. 63 | 64 | Here is a usage example. Instead of learning new API, new framework or long set of new methods, your simply import your favorite familiar libraries that you are already using anyway: 65 | 66 | 67 | ```js 68 | const mount = createMount({ 69 | 70 | // your favorite stream factory 71 | // mithril/stream, TODO: flyd, most, xstream 72 | createStream: require("mithril/stream"), 73 | 74 | // your favorite element creator 75 | // mitrhil, TODO: (React|Preact|Inferno).createElement, snabbdom/h, hyperscript 76 | createElement: require('mithril'), 77 | 78 | // your favorite create tags helpers (optional) 79 | createTags: require('hyperscript-helpers'), 80 | 81 | // mithril.render, TODO: (React|Preact|Inferno).render, snabbdom-patch, replaceWith 82 | createRender: element => vnode => require('mithril').render(element, vnode) 83 | }) 84 | ``` 85 | 86 | So instead of having external dependencies in *every file*, 87 | `un` simply lets you provide those libraries **once** and return the `mount` function, the only function from `un` that you need. The role of the `mount` is similar (and inspired by) [`Mithril` `m.mount`](https://mithril.js.org/mount.html) or `React.render` with auto-redrawing facility. Our key vision is, attaching a live component to an element should be as simple as calling a function and `mount` does exactly that: 88 | 89 | 90 | ```js 91 | // mount our live uncomponent and get back its writeable stream of actions 92 | const actions = mount({ element, reducer, view, initState}) 93 | ``` 94 | 95 | So we call `mount` with 4 basic properties: 96 | 97 | - `element`: HTML element, where we attaching our uncomponent, similar to `React.render` the element's content will be overwritten 98 | 99 | - `reducer`: Redux style reducer from our model logic 100 | 101 | - `view`: Plain pure function taking `dispatcher` and `state` and returning new `state`, the state can be global, narrowed down, or completely local to the uncomponent, to cater for the [fractal architecture](https://staltz.com/unidirectional-user-interface-architectures.html). The view function dispatches actions just like in Redux and returns a virtual or real DOM element, depending on the library used in configuring the `mount`. But to be completely pure with no external dependency, the `view` must include the element creator factory as one of its parameters: 102 | 103 | ```js 104 | // all parameters are explicit, no dependencies, no magic 105 | // can be tested as pure dumb function in any environment 106 | const view = h => (state, dispatch) => 107 | h('div', `Hello World, your ${state} is wonderful!`) 108 | ``` 109 | 110 | where `h` stands for our favorite element creator passed to `createMount`. We find the [`hyperscript`](https://github.com/hyperhype/hyperscript) API supported by many libraries (e.g. Mithril, Snabbdom, or [`react-hyperscript`](https://github.com/mlmorg/react-hyperscript)) most convenient, but using JSX should also be possible as it is equivalent to the `React.createElement` calls. 111 | 112 | Or use the `createTags` helpers (like [`hyperscript-helpers`](https://github.com/ohanhi/hyperscript-helpers)) that you can conveniently destructure inside the view: 113 | 114 | ```js 115 | // again, no dependencies, only function parameters, 116 | // all inputs are instantly visible, no need to jump elsewhere 117 | const view = ({ div }) => (state, dispatch) => 118 | div(`Hello World, your ${state} is wonderful!`) 119 | ``` 120 | 121 | The other two parameters of the `view` are `dispatch` and `state` that match the types (or more precisely, interfaces) of the `action` and the `state` parameters of the `reducer`. (Note how the `view` signature matches the one of the `reducer`. Further, it also matches the [native JS `Array.prototype.reduce`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) as well as [the general `reduce` method](http://ramdajs.com/0.18.0/docs/#reduce) signatures, the latter provided by the [Foldable Typeclass](https://github.com/fantasyland/fantasy-land#foldable).) 122 | The state, updated by the reducer, will be passed directly to the view (from the same `mount` call). And every action value used to call the `dispatch` function, will be passed directly as action to the `reducer`. For example, calling `dispatch('foo')` in the event handler inside the `view` will result in `foo` being passed as `action` to the `reducer`. 123 | 124 | This style of writing was inspired by https://github.com/ericelliott/react-pure-component-starter and https://medium.com/javascript-scene/baby-s-first-reaction-2103348eccdd 125 | 126 | In `React` the role of the `view` would be played by the component `render` method, but we already have another static method `React.render`, so we prefer to call it the `view` as in Mithril. 127 | 128 | 129 | - `initState`: The state to initialise our uncomponent. 130 | 131 | 132 | ## Uncomponents 133 | 134 | Why "uncomponent"? Because there isn't really much of a "component", the `reducer` and the `view` are just two plain functions and the initial state is a plain value. 135 | 136 | ### So what is called "uncomponent"? 137 | 138 | - Native JavaScript functions. [Or native generators](https://github.com/funkia/turbine/#understanding-generator-functions). Or native object holding a few functions. 139 | - No proprietary syntax. Every part of the framework, library, package is hidden away from the user. In the configuration. 140 | - Pluggable into any framework. Or into no framework. This is what configuration is for. 141 | 142 | 143 | 144 | ## Streams 145 | 146 | Streams are in the core of `un`. [The introduction to Reactive Programming you've been missing](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754) by Andre Staltz is a great introduction to streams. [`flyd`](https://github.com/paldepind/flyd) is a great minimal but powerful stream library to use, including great examples to see the streams in action. The [Mithril stream library](https://mithril.js.org/stream.html) is even smaller but suffices to let `un` do its job. Note that some libraries, such as [`most`](https://github.com/cujojs/most) distinguish between "pending" and "active" streams, but to make things as simple as possible, all streams in `un` are always active, readable and writeable. 147 | 148 | Despite of their initial complexity, streams model very well the asynchronous processes such as user acton flow, and consequently help to greatly simplify the architecture. The state values are stored directly inside the stream, so no stores such as in Redux are needed. 149 | 150 | Instead of letting the framework do some "magic" behind the scene, when updating the DOM, with `un`, your view listens to its state stream. Whenever the state changes, its new value is passed to any subscriber, or which the view is one. The view function is pure with no side-effects, so all it does is pass the new updated element to the rendering library you provided to 151 | to `createMount`. It is then the library's responsibility to create the side-effect updating the DOM. 152 | 153 | Note that you can absolutely ignore the stream part and write your code without seeing a single stream. Like in Redux, every action passed to dispatcher will go through the cycle. However, using streams can give you additional control. The `mount` method returns the action stream that is both readable and writeable. That means, you can attach other subscribers to your actions, or you can actively pipe new values into it, causing the same or additional actions passed to the reducer and subsequently updating the DOM. 154 | 155 | 156 | A basic example below is demonstrating how the action stream can be externally driven in addition to user actions. 157 | 158 | 159 | ## Full reactive control of your uncomponents 160 | 161 | The `un` mount function, created as described above, 162 | returns for every uncomponent, the object 163 | ```js 164 | const { 165 | states: streamOfStates, 166 | actions: streamOfActions 167 | } = mount(...) 168 | ``` 169 | holding the state and action streams 170 | (we like to refer to streams by plurals to emphasize their collection nature). 171 | 172 | That means, you can conveniently add any complex behavior (such as loading external data) 173 | to your uncomponent by piping the external actions into its action stream, 174 | or you can attach an external subscriber to the state stream, 175 | to be updated on any state changes in a reactive fashion. 176 | 177 | [The active-counter example](https://github.com/dmitriz/un/tree/master/examples/active-counter) demonstrates this feature, see below. 178 | 179 | 180 | ### `un` reactive vision 181 | 182 | Right now the streams provided by `un` conform to the [Mithril Stream API](https://mithril.js.org/stream.html) 183 | 184 | In order to make using `un` as universal and painless as possible, 185 | and accessible to broader audience, we would like to facilitate plugging other stream libraries. 186 | So you can use your favorite stream api to control your uncomponents. 187 | 188 | ### Help and contributions are welcome! 189 | 190 | 191 | 192 | ## [The active-counter example](https://github.com/dmitriz/un/tree/master/examples/active-counter) 193 | 194 | 195 | ### Pure reducer function 196 | 197 | Our both state and action values are just numbers 198 | and the reducer simply adds the action value to the state: 199 | 200 | ```js 201 | const reducer = (state, action) => 202 | state + action 203 | ``` 204 | 205 | By making the state local and avoiding giving specific names to the actions, 206 | we can make the reducer function more of a general purpose and reusable. 207 | Just like a function in [`Ramda`](http://ramdajs.com/) or similar library. 208 | 209 | 210 | ### Pure view function 211 | 212 | Our view function example here demonstrates how a function helper inside 213 | can reuse all the arguments of the outside function: 214 | 215 | ```js 216 | const view = ({ button }) => (state, dispatch) => { 217 | 218 | // reusing the button function 219 | const change = amount => 220 | button( 221 | {onclick: () => dispatch(amount)}, 222 | (amount > 0) 223 | ? `+${amount}` 224 | : `-${-amount}` 225 | ) 226 | 227 | return [ 228 | `Increasing by 5 every second: `, 229 | change(10), 230 | ` ${state} `, 231 | change(-10) 232 | ] 233 | } 234 | ``` 235 | 236 | Here we attach to the `onclick` listener (following `Mithril`s API flavour) 237 | the anonymous function passing the `amount` to the `dispatch`. 238 | As mentioned above, that value is passed as action to the reducer. 239 | 240 | Behind the scene, every time the user clicks that button, 241 | the `amount` value is written into the `action` stream. 242 | That is essentially what the `dispatch` function does. 243 | 244 | 245 | ### Use with HTTP responses, Promises and other external actions 246 | 247 | In addition to user's actions, the counter is being updated 248 | from the application via this code: 249 | 250 | ```js 251 | const delayedConstant = (val, delay) => stream => { 252 | setInterval(() => stream(val), delay) 253 | return stream 254 | } 255 | delayedConstant(5, 1000)(actions) 256 | ``` 257 | 258 | The `actions` stream here is exported from the `un` `mount` function 259 | and gives access to the external drivers. 260 | The simple periodic values here are for demonstration purposes. 261 | They can be replaced by any HTTP request or any value returned by a JS Promise, 262 | or can be even subscribed to another stream, such as event stream from any event: 263 | 264 | ```js 265 | // the response value from the promise will appear in the actions stream 266 | // once the promise is resolved or the error object if the promise is rejected 267 | actions(fetch(someUrl)) 268 | 269 | // every value from the `externalStream` 270 | // will be passed down the actions stream in real time 271 | externalStream.map(actions) 272 | ``` 273 | 274 | The functionality here is based on the 275 | [getter-setter syntax of the `flyd` stream library](https://github.com/paldepind/flyd#creating-streams) and [the way promises are treated](https://github.com/paldepind/flyd#using-promises-for-asynchronous-operations). 276 | However, any another stream library with stream updates functionality 277 | (e.g. [`mostjs-subject`](https://github.com/mostjs-community/subject)) 278 | can be used instead. 279 | 280 | Once subscribed as above, all values will be automatically 281 | passed to the reducer as action values. 282 | That way all business logic needed can be put 283 | as pure function updating the state inside the reducer. 284 | 285 | The rest is full automatic: reducer runs on every new action, 286 | the state is updated and passed to the view 287 | that will be sent to the renderer to update the dom. 288 | 289 | 290 | ### The mount function 291 | 292 | ```js 293 | // the only method you ever use from 'un' 294 | const createMount = require('un.js') 295 | 296 | // or React.createElement 297 | 298 | const mount = createMount({ 299 | 300 | // your favorite stream factory 301 | // TODO: flyd, most, xstream 302 | createStream: require("mithril/stream"), 303 | 304 | // your favorite element creator 305 | // TODO: (React|Preact|Inferno).createElement, snabbdom/h, hyperscript 306 | createElement: require('mithril'), 307 | 308 | // your favorite create tags helpers 309 | createTags: require('hyperscript-helpers'), 310 | 311 | // TODO: (React|Preact|Inferno).render, snabbdom-patch, replaceWith 312 | createRender: element => vnode => require('mithril').render(element, vnode) 313 | }) 314 | 315 | // create dom element 316 | const e = document.createElement('div') 317 | document.body.appendChild(e) 318 | 319 | // mount our live uncomponent and get back its writeable stream of actions 320 | const actions = mount({ e, reducer, view, initState: 0 }) 321 | ``` 322 | 323 | 324 | ## More Examples 325 | 326 | The Basic Examples are intentionally made very simple and focused. 327 | 328 | [The submit example](https://github.com/dmitriz/un/tree/master/examples/basic/submit) demonstrates how to attach an simple update action to the `onsubmit` event of the `
`. 329 | 330 | [The submit-with-reset example](https://github.com/dmitriz/un/tree/master/examples/basic/submit-with-reset) in addition resets the input field after submission by providing additional state variable to control it. 331 | 332 | [The keypress example](https://github.com/dmitriz/un/tree/master/examples/basic/keypress) demonstrates how to pass all keys pressed to the dispatcher, and then via the reducer, back to the state. 333 | 334 | [The todos example](https://github.com/dmitriz/un/tree/master/examples/todos) demonstrates a basic interactive todos collector. 335 | 336 | [The todos-with-delete example](https://github.com/dmitriz/un/tree/master/examples/todos-with-delete) adds the delete feature to the previous example. It deviates slightly from the traditional todos examples, in which the deletion is done by name, i.e. all todos with the same name are deleted at the same time. 337 | 338 | 339 | ### More advanced examples are in active development, help and contributions are welcome! 340 | 341 | 342 | ## Inspirations (incomplete and in random order) 343 | 344 | Professor Frisby's Mostly Adequate Guide to Functional Programming 345 | by the same author. 346 | 347 | [Professor Frisby Introduces Composable Functional JavaScript](https://egghead.io/lessons/javascript-refactoring-imperative-code-to-a-single-composed-expression-using-box) 348 | 349 | [The introduction to Reactive Programming you've been missing](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754) by Andre Staltz, as well as [many other of his always enlightening posts](https://staltz.com/blog.html) as well as his [CycleJS project](https://cycle.js.org/) 350 | 351 | [@sindresorhus on reusable modules](https://github.com/sindresorhus/ama/issues/10#issuecomment-117766328) 352 | 353 | The [Ramda](http://ramdajs.com/) and its sister [Fantasy Land](https://github.com/fantasyland/fantasy-land) and [Sanctuary](https://github.com/sanctuary-js/sanctuary) projects 354 | 355 | The [Mithril framework](https://mithril.js.org/index.html) 356 | 357 | The [simple but powerful `flyd` stream library](https://github.com/paldepind/flyd) 358 | 359 | The new [Functional Reactive `Turbine` framework](https://github.com/funkia/turbine) and the oder [Functional Frontend Architecture](https://github.com/paldepind/functional-frontend-architecture) 360 | by @paldepind 361 | 362 | The [Snabbdom virtual dom library](https://github.com/snabbdom/snabbdom) and the [broader Hyperscript Ecosystem](https://github.com/hyperhype/awesome-hyperscript) 363 | 364 | The [Redux](http://redux.js.org/) and [Elm](https://guide.elm-lang.org/architecture/) projects 365 | 366 | [TodoMVC example built with `most.js`](https://github.com/briancavalier/most-todomvc) 367 | 368 | [Meiosis](https://github.com/foxdonut/meiosis) 369 | 370 | [redux-react-local](https://github.com/threepointone/redux-react-local) 371 | 372 | ...To be extended... 373 | 374 | -------------------------------------------------------------------------------- /adapters/factory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Transforms a _plain_ object creating Factory F to the new Factory T(F) 3 | * creating the same plain objects with the additional specified prototype set on each object 4 | * (can be used to provide additional instance methods on the object created) 5 | * 6 | * Warning. If the objects were created by F with prototypes, they will be overwritten by T(F). 7 | * This is by design, to avoid performance overheads overusing prototype chains. 8 | * In particular, this transform may not be suitable for Factories using constructors 9 | * and relying on their prototypal chains. 10 | */ 11 | const setInstanceProto = protoObj => Factory => 12 | (...args) => { 13 | const factoryObj = Factory(...args) 14 | let newObj = Object.create(protoObj) 15 | Object.assign(newObj, factoryObj) 16 | return newObj 17 | } 18 | 19 | module.exports = { setInstanceProto } 20 | -------------------------------------------------------------------------------- /adapters/factory_test.js: -------------------------------------------------------------------------------- 1 | const { setInstanceProto } = require('./factory.js') 2 | 3 | const Factory = (val, key) => ({ [key]: val }) 4 | const SuperFactory = setInstanceProto( 5 | { newKey: 'Yes' } 6 | )(Factory) 7 | 8 | const Factories = [ 9 | 10 | // producing plain objects 11 | (val, key) => ({ [key]: val }), 12 | 13 | // producing objects with prototype 14 | (val, key) => Object.create({ [key]: val }) 15 | ] 16 | 17 | const protoObj = { newKey: 'Yes' } 18 | const SuperFactories = Factories.map(setInstanceProto(protoObj)) 19 | 20 | 21 | describe('original factory', ()=>{ 22 | it('should create object with specified key and value', ()=>{ 23 | const obj = Factories[0](55, 'a') 24 | expect(obj.a).toBe(55) 25 | const obj1 = Factories[1](55, 'a') 26 | expect(obj1.a).toBe(55) 27 | }) 28 | }) 29 | 30 | describe('factory with added proto', ()=>{ 31 | const obj = SuperFactories[0](55, 'a') 32 | // const obj1 = SuperFactories[1](55, 'a') 33 | 34 | it('should preserve object own props', ()=>{ 35 | expect(obj.a).toBe(55) 36 | }) 37 | it('should provide props from the new prototype', ()=>{ 38 | expect(obj.newKey).toBe('Yes') 39 | }) 40 | }) -------------------------------------------------------------------------------- /adapters/helpers.js: -------------------------------------------------------------------------------- 1 | // aliases for different factory api 2 | 3 | // replace Factory.method() with Factory.of() 4 | const methodToFactory = methodName => Factory => { 5 | 6 | // prototypally inherit from Factory 7 | // const Transformed = Object.create(Factory) 8 | 9 | const Transformed = Factory[methodName] 10 | // Transformed.prototype = Factory 11 | Object.setPrototypeOf(Transformed, Factory) 12 | 13 | return Transformed 14 | } 15 | 16 | 17 | /** 18 | * Set proto object for all instances created by the Factory 19 | * (can be used to provide additional instance methods) 20 | */ 21 | const setInstanceProto = protoObj => Factory => 22 | (...agrs) => { 23 | let obj = Factory(...args) 24 | Object.setPrototypeOf(obj, protoObj) 25 | return obj 26 | } 27 | 28 | 29 | module.exports = { methodToFactory } 30 | -------------------------------------------------------------------------------- /adapters/helpers_test.js: -------------------------------------------------------------------------------- 1 | const { methodToFactory } = require('./helpers') 2 | 3 | // simple test to make sure karma is working 4 | describe('Addition', function () { 5 | const add = (x,y) => x+y 6 | it('should add numbers', function () { 7 | expect(add(2, 4)).toBe(6) 8 | expect(add(2, 4)).not.toBe(2) 9 | }) 10 | }) 11 | 12 | const Factory = { 13 | // simply return the array of its args 14 | method: (...args) => [...args] 15 | } 16 | const InheritedFactory = Object.create(Factory) 17 | 18 | // const TransformedFactory = methodToFactory('method')(Factory) 19 | // const TransformedInheritedFactory = methodToFactory('method')(InheritedFactory) 20 | 21 | const testFactory = Factory => { 22 | // const TransformedFactory = alias('from', 'to')(Factory) 23 | const TransformedFactory = methodToFactory('method')(Factory) 24 | 25 | xdescribe(`Factory transformer`, () => { 26 | 27 | it('should get called with all arguments', () => { 28 | expect(Factory.method()).toEqual([]) 29 | expect(Factory.method(1,2,'a3')).toEqual([1,2,'a3']) 30 | }) 31 | it('should transform factory into function executing method provided', () => { 32 | expect(TransformedFactory(1,2,'a3')).toEqual([1,2,'a3']) 33 | }) 34 | it('should keep all methods', () => { 35 | expect(TransformedFactory.method()).toEqual([]) 36 | expect(TransformedFactory.method(1,2,'a3')).toEqual([1,2,'a3']) 37 | }) 38 | it('should not create own properties', () => { 39 | expect(Object.keys(Factory)).toEqual([]) 40 | }) 41 | }) 42 | return; 43 | } 44 | 45 | ;[Factory, InheritedFactory].forEach(testFactory) 46 | 47 | 48 | -------------------------------------------------------------------------------- /adapters/objects.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Set prototype object for all instances created by the Factory 3 | * (can be used to provide additional instance methods) 4 | */ 5 | const addProto = protoObj => obj => { 6 | 7 | // get prototype object 8 | const oldProto = Object.getPrototypeOf(obj) 9 | 10 | // create new object with that prototype 11 | const newProto = Object.create(oldProto) 12 | 13 | return newProto 14 | } -------------------------------------------------------------------------------- /adapters/objects_test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmitriz/un/250e7633178f59fbf7f40ea9d5d06ab813662cc0/adapters/objects_test.js -------------------------------------------------------------------------------- /adapters/stream.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A stream is a sequence of values emitted at discrete time moments. 3 | */ 4 | 5 | /** 6 | * Create new stream 7 | 8 | const stream = createStream.of(value) 9 | 10 | * @param {*} value - (Optional) the initial value of the stream 11 | * @return {stream} the stream 12 | 13 | * @aliased: 14 | * 15 | * createStream["fantasy-land/of"]() 16 | * 17 | * mithril/stream: 18 | * createStream() 19 | * 20 | * flyd: 21 | * createStream.stream() 22 | */ 23 | 24 | 25 | 26 | /** 27 | * Creates a dependent stream whose value is set to the result of the callback function 28 | 29 | const stream = stream.map(callback) 30 | 31 | * @aliased: 32 | 33 | createStream["fantasy-land/map"](cb) 34 | stream.map(cb) 35 | */ 36 | 37 | /** 38 | * Creates a new stream with the results of calling the reducer function 39 | * on every value in the stream with an accumulator and the incoming value. 40 | * 41 | 42 | const childStream = parentStream.scan(reducer, initVal) 43 | 44 | * @param {Function} reducer: (acc, feed) -> acc 45 | * @param {*} initVal - the initial value of the accumulator 46 | * @param {stream} parentStream - the stream source 47 | * @return {stream} the new stream childStream 48 | * 49 | * @example 50 | * var numbers = flyd.stream(); 51 | * var sum = flyd.scan(function(sum, n) { return sum+n; }, 0, numbers); 52 | * numbers(2)(3)(5); 53 | * sum(); // 10 54 | */ 55 | 56 | -------------------------------------------------------------------------------- /adapters/stream_test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmitriz/un/250e7633178f59fbf7f40ea9d5d06ab813662cc0/adapters/stream_test.js -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Runing Examples 2 | 3 | ## Enter the example directory 4 | 5 | ```sh 6 | cd ${pathToExample}${example-name} 7 | ``` 8 | 9 | ## Run in browser with no installations 10 | 11 | ```sh 12 | $ yarn open 13 | ``` 14 | or 15 | ```sh 16 | $ npm run open 17 | ``` 18 | 19 | ## Auto-rebuild and reload on every file save 20 | 21 | Our favorite prototyping tool is [`budo`](https://github.com/mattdesl/budo), 22 | do its ease of use and features including: 23 | 24 | - works out of the box, no configuration needed 25 | - no `.html` file is required, budo will create one automatically 26 | - fast incremental building, suspending the response until the new source is ready 27 | - watches HTML and CSS files for changes; CSS is injected without reloading the page 28 | - can be used with any `browserify` options 29 | - additional rich API://github.com/mattdesl/budo/blob/master/docs/command-line-usage.md 30 | 31 | 32 | ### Install Budo 33 | 34 | ```sh 35 | $ yarn global add budo 36 | ``` 37 | or 38 | ```sh 39 | $ npm install budo -g 40 | ``` 41 | 42 | ### Run with budo 43 | 44 | This will start budo server and open browser: 45 | 46 | ```sh 47 | $ yarn 48 | $ yarn start 49 | ``` 50 | or 51 | ```sh 52 | $ npm install 53 | $ npm start 54 | ``` 55 | 56 | Now change and save the source files 57 | and see instant updates in your browser. 58 | -------------------------------------------------------------------------------- /examples/active-counter/README.md: -------------------------------------------------------------------------------- 1 | ## Runing the Example 2 | 3 | See https://github.com/dmitriz/un/tree/master/examples 4 | -------------------------------------------------------------------------------- /examples/active-counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/active-counter/index.js: -------------------------------------------------------------------------------- 1 | // uncomponent - no imports - no external dependencies! 2 | 3 | const reducer = (state, action) => 4 | state + action 5 | 6 | // pure with no dependencies 7 | const view = ({ button }) => (state, dispatch) => { 8 | 9 | const change = amount => 10 | button( 11 | {onclick: () => dispatch(amount)}, 12 | (amount > 0) 13 | ? `+${amount}` 14 | : `-${-amount}` 15 | ) 16 | 17 | return [ 18 | `Increasing by 5 every second: `, 19 | change(10), 20 | ` ${state} `, 21 | change(-10) 22 | ] 23 | } 24 | 25 | 26 | // the only basic method you need to import from 'un.js' 27 | // const createMount = require('../../') 28 | const createMount = require('un.js') 29 | 30 | // configure the 'mount' method, the only method we need 31 | const mount = createMount({ 32 | 33 | // your favorite stream factory 34 | // TODO: flyd, most, xstream 35 | // createStream: require("flyd"), 36 | createStream: require("mithril/stream"), 37 | 38 | // your favorite element creator 39 | // TODO: (React|Preact|Inferno).createElement, snabbdom/h, hyperscript 40 | createElement: require('mithril'), 41 | 42 | // your favorite create tags helpers 43 | createTags: require('hyperscript-helpers'), 44 | 45 | // TODO: (React|Preact|Inferno).render, snabbdom-patch, replaceWith 46 | createRender: element => vnode => require('mithril').render(element, vnode) 47 | }) 48 | 49 | // create dom element 50 | const el = document.createElement('div') 51 | document.body.appendChild(el) 52 | 53 | // mount our live uncomponent and get back its writeable stream of actions 54 | const { actions } = mount({ el, reducer, view, initState: 0 }) 55 | 56 | 57 | 58 | // --- Some more action - generate periodic actions 59 | 60 | const delayedConstant = (val, delay) => stream => { 61 | setInterval(() => stream(val), delay) 62 | return stream 63 | } 64 | delayedConstant(5, 1000)(actions) 65 | 66 | 67 | -------------------------------------------------------------------------------- /examples/active-counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "private": true, 4 | "description": "active-counter", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/dmitriz/un" 8 | }, 9 | "scripts": { 10 | "start": "npm run dev", 11 | "open": "open index.html", 12 | "build": "browserify index.js -o build.js", 13 | "dev": "budo index.js:build.js --live --open" 14 | }, 15 | "keywords": [], 16 | "author": "Dmitri Zaitsev (https://github.com/dmitriz)", 17 | "license": "MIT", 18 | "dependencies": { 19 | "flyd": "^0.2.8", 20 | "hyperscript-helpers": "^3.0.3", 21 | "mithril": "^2.0.2", 22 | "un.js": "^0.5.9" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/active-counter/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | flyd@^0.2.8: 6 | version "0.2.8" 7 | resolved "https://registry.yarnpkg.com/flyd/-/flyd-0.2.8.tgz#26b606a051431ec275dd7ca2cb5fb61756fcd7f1" 8 | integrity sha512-PVr++d+cOkf//kVIm66aTgmmrHNAhqNH9TIZQzunrVL9GaW7pMFTuLcaGkTV9z4bkvVWPB1VGuxjGFFfQUZNwA== 9 | dependencies: 10 | ramda "^0.25.0" 11 | 12 | hyperscript-helpers@^3.0.3: 13 | version "3.0.3" 14 | resolved "https://registry.yarnpkg.com/hyperscript-helpers/-/hyperscript-helpers-3.0.3.tgz#583ac45214c9ea810991feb97bdb37fc3d12a1e8" 15 | 16 | mithril@^2.0.2: 17 | version "2.0.4" 18 | resolved "https://registry.yarnpkg.com/mithril/-/mithril-2.0.4.tgz#d125969d992b924c7185d24ff92d997e5c00116b" 19 | integrity sha512-mgw+DMZlhMS4PpprF6dl7ZoeZq5GGcAuWnrg5e12MvaGauc4jzWsDZtVGRCktsiQczOEUr2K5teKbE5k44RlOg== 20 | 21 | ramda@^0.25.0: 22 | version "0.25.0" 23 | resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.25.0.tgz#8fdf68231cffa90bc2f9460390a0cb74a29b29a9" 24 | integrity sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ== 25 | 26 | un.js@^0.5.9: 27 | version "0.5.9" 28 | resolved "https://registry.yarnpkg.com/un.js/-/un.js-0.5.9.tgz#892076199843867e015760cf1491897d91e12fcf" 29 | integrity sha512-uS13MGvJlleNV/rBkyEH6igfvXQZH8PG6HPeVD6n9kKMFSHTpezqXaMyfDPc8aalshye6XDTsDwM/AY9ZinLYQ== 30 | -------------------------------------------------------------------------------- /examples/basic/keypress/README.md: -------------------------------------------------------------------------------- 1 | ## Runing the Example 2 | 3 | See https://github.com/dmitriz/un/tree/master/examples 4 | -------------------------------------------------------------------------------- /examples/basic/keypress/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/basic/keypress/index.js: -------------------------------------------------------------------------------- 1 | // uncomponent - the pair of functions, 2 | // no imports - no external dependencies! 3 | 4 | // compute new state 5 | const reducer = ({ submitted } = {}, value) => ({ 6 | 7 | // add new value to submitted array 8 | submitted: submitted.concat([value]), 9 | changed: true 10 | }) 11 | 12 | const style = {width: '100%'} 13 | 14 | // pure with no dependencies 15 | const view = ({ form, input, p, span }) => ({ submitted, changed }, dispatch) => [ 16 | `Press some key: `, 17 | form({ 18 | onkeypress: e => { 19 | // prevents page reload 20 | e.preventDefault() 21 | dispatch(e.code) 22 | } 23 | }, [ 24 | input({ 25 | name: 'in', 26 | style, 27 | class: 'new-todo', 28 | placeholder: 'What is your Key today?', 29 | autofocus: true, 30 | }) 31 | ]), 32 | changed 33 | ? `Thank you, your pressed keys are:` 34 | : `You have not yet pressed a key...`, 35 | p({style: { 36 | color: changed ? 'blue' : 'gray' 37 | }}, submitted.map(key => span(` ${key} `))) 38 | ] 39 | 40 | 41 | // the only method we need 42 | const mount = require('./un-mount') 43 | 44 | // create dom element 45 | const el = document.createElement('div') 46 | document.body.appendChild(el) 47 | 48 | // mount our live uncomponent 49 | mount({ 50 | el, 51 | reducer, 52 | view, 53 | initState: { 54 | submitted: [], 55 | changed: false 56 | } 57 | }) 58 | -------------------------------------------------------------------------------- /examples/basic/keypress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "main": "index.js", 4 | "description": "controls", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/dmitriz" 8 | }, 9 | "scripts": { 10 | "start": "npm run dev", 11 | "open": "open index.html", 12 | "build": "browserify index.js -o build.js", 13 | "dev": "budo index.js:build.js --live --open" 14 | }, 15 | "keywords": [], 16 | "author": "Dmitri Zaitsev (https://github.com/dmitriz)", 17 | "license": "MIT", 18 | "dependencies": { 19 | "hyperscript-helpers": "^3.0.3", 20 | "mithril": "^2.2.3", 21 | "un.js": "^0.5.9" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/basic/keypress/un-mount.js: -------------------------------------------------------------------------------- 1 | // the only basic method you need to import 2 | const createMount = require('un.js') 3 | 4 | const mount = createMount({ 5 | 6 | // your favorite stream factory 7 | // TODO: flyd, most, xstream 8 | createStream: require("mithril/stream"), 9 | 10 | // your favorite element creator 11 | // TODO: (React|Preact|Inferno).createElement, snabbdom/h, hyperscript 12 | createElement: require('mithril'), 13 | 14 | // your favorite create tags helpers 15 | createTags: require('hyperscript-helpers'), 16 | 17 | // TODO: (React|Preact|Inferno).render, snabbdom-patch, replaceWith 18 | createRender: element => vnode => require('mithril').render(element, vnode) 19 | }) 20 | 21 | // export configured 'mount' with our library choices 22 | module.exports = mount 23 | -------------------------------------------------------------------------------- /examples/basic/keypress/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | hyperscript-helpers@^3.0.3: 6 | version "3.0.3" 7 | resolved "https://registry.yarnpkg.com/hyperscript-helpers/-/hyperscript-helpers-3.0.3.tgz#583ac45214c9ea810991feb97bdb37fc3d12a1e8" 8 | 9 | mithril@^2.2.3: 10 | version "2.2.3" 11 | resolved "https://registry.yarnpkg.com/mithril/-/mithril-2.2.3.tgz#0e173f6c820b226f1cffc1a4a2f6e2b7144a43b1" 12 | integrity sha512-to8SZZ0rEw3eYbp61z++zT53kKLgrcOYUekb4u0FGIg7GK+222wXD4DvwFPTkK/MyFEGryv1eIGKmCNRoEAuCA== 13 | 14 | un.js@^0.5.9: 15 | version "0.5.9" 16 | resolved "https://registry.yarnpkg.com/un.js/-/un.js-0.5.9.tgz#892076199843867e015760cf1491897d91e12fcf" 17 | integrity sha512-uS13MGvJlleNV/rBkyEH6igfvXQZH8PG6HPeVD6n9kKMFSHTpezqXaMyfDPc8aalshye6XDTsDwM/AY9ZinLYQ== 18 | -------------------------------------------------------------------------------- /examples/basic/submit-with-reset/README.md: -------------------------------------------------------------------------------- 1 | ## Runing the Example 2 | 3 | See https://github.com/dmitriz/un/tree/master/examples 4 | -------------------------------------------------------------------------------- /examples/basic/submit-with-reset/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/basic/submit-with-reset/index.js: -------------------------------------------------------------------------------- 1 | // uncomponent - the pair of functions, 2 | // no imports - no external dependencies! 3 | 4 | // old state is not used, only action value 5 | const reducer = (state = {}, value) => ({ 6 | 7 | // reset input value here 8 | inputVal: ``, 9 | submitted: value, 10 | changed: true 11 | }) 12 | 13 | const style = {width: '100%'} 14 | 15 | // pure with no dependencies 16 | const view = ({ form, input, p }) => 17 | ({ inputVal, submitted, changed }, dispatch) => [ 18 | `Welcome to my Submit: `, 19 | `Type and hit ENTER: `, 20 | form({ 21 | onsubmit: e => { 22 | // prevents page reload 23 | e.preventDefault() 24 | dispatch(e.target.in.value) 25 | } 26 | }, [ 27 | input({ 28 | name: 'in', 29 | style, 30 | class: 'new-todo', 31 | placeholder: 'What is your string today?', 32 | autofocus: true, 33 | 34 | // inputs value is controlled by function argument 35 | value: inputVal 36 | }) 37 | ]), 38 | changed 39 | ? `Thank you, here is your submission: ` 40 | : `You have submitted: `, 41 | p({style: { 42 | color: changed ? 'blue' : 'gray' 43 | }}, submitted) 44 | ] 45 | 46 | 47 | // the only method we need 48 | const mount = require('./un-mount') 49 | 50 | // create dom element 51 | const el = document.createElement('div') 52 | document.body.appendChild(el) 53 | 54 | // mount our live uncomponent 55 | mount({ 56 | el, 57 | reducer, 58 | view, 59 | initState: { 60 | inputVal: ``, 61 | submitted: `Nothing as of yet`, 62 | changed: false 63 | } 64 | }) 65 | -------------------------------------------------------------------------------- /examples/basic/submit-with-reset/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "main": "index.js", 4 | "description": "controls", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/dmitriz" 8 | }, 9 | "scripts": { 10 | "start": "npm run dev", 11 | "open": "open index.html", 12 | "build": "browserify index.js -o build.js", 13 | "dev": "budo index.js:build.js --live --open" 14 | }, 15 | "keywords": [], 16 | "author": "Dmitri Zaitsev (https://github.com/dmitriz)", 17 | "license": "MIT", 18 | "dependencies": { 19 | "hyperscript-helpers": "^3.0.3", 20 | "mithril": "^2.2.2", 21 | "un.js": "^0.5.9" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/basic/submit-with-reset/un-mount.js: -------------------------------------------------------------------------------- 1 | // the only basic method you need to import 2 | const createMount = require('un.js') 3 | 4 | const mount = createMount({ 5 | 6 | // your favorite stream factory 7 | // TODO: flyd, most, xstream 8 | createStream: require("mithril/stream"), 9 | 10 | // your favorite element creator 11 | // TODO: (React|Preact|Inferno).createElement, snabbdom/h, hyperscript 12 | createElement: require('mithril'), 13 | 14 | // your favorite create tags helpers 15 | createTags: require('hyperscript-helpers'), 16 | 17 | // TODO: (React|Preact|Inferno).render, snabbdom-patch, replaceWith 18 | createRender: element => vnode => require('mithril').render(element, vnode) 19 | }) 20 | 21 | // export configured 'mount' with our library choices 22 | module.exports = mount 23 | -------------------------------------------------------------------------------- /examples/basic/submit-with-reset/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | balanced-match@^1.0.0: 6 | version "1.0.2" 7 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 8 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 9 | 10 | brace-expansion@^1.1.7: 11 | version "1.1.11" 12 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 13 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 14 | dependencies: 15 | balanced-match "^1.0.0" 16 | concat-map "0.0.1" 17 | 18 | concat-map@0.0.1: 19 | version "0.0.1" 20 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 21 | integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== 22 | 23 | fs.realpath@^1.0.0: 24 | version "1.0.0" 25 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 26 | integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== 27 | 28 | glob@^7.1.3: 29 | version "7.2.3" 30 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" 31 | integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== 32 | dependencies: 33 | fs.realpath "^1.0.0" 34 | inflight "^1.0.4" 35 | inherits "2" 36 | minimatch "^3.1.1" 37 | once "^1.3.0" 38 | path-is-absolute "^1.0.0" 39 | 40 | hyperscript-helpers@^3.0.3: 41 | version "3.0.3" 42 | resolved "https://registry.yarnpkg.com/hyperscript-helpers/-/hyperscript-helpers-3.0.3.tgz#583ac45214c9ea810991feb97bdb37fc3d12a1e8" 43 | 44 | inflight@^1.0.4: 45 | version "1.0.6" 46 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 47 | integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== 48 | dependencies: 49 | once "^1.3.0" 50 | wrappy "1" 51 | 52 | inherits@2: 53 | version "2.0.4" 54 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 55 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 56 | 57 | minimatch@^3.1.1: 58 | version "3.1.2" 59 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" 60 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 61 | dependencies: 62 | brace-expansion "^1.1.7" 63 | 64 | mithril@^2.2.2: 65 | version "2.2.2" 66 | resolved "https://registry.yarnpkg.com/mithril/-/mithril-2.2.2.tgz#5a0743bd04726cc1efa6e91477f995d80fc6c539" 67 | integrity sha512-YRm6eLv2UUaWaWHdH8L+desW9+DN7+oM34CxJv6tT2e1lNVue8bxQlknQeDRn9aKlO8sIujm2wqUHwM+Hb1wGQ== 68 | dependencies: 69 | ospec "4.0.0" 70 | 71 | once@^1.3.0: 72 | version "1.4.0" 73 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 74 | integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== 75 | dependencies: 76 | wrappy "1" 77 | 78 | ospec@4.0.0: 79 | version "4.0.0" 80 | resolved "https://registry.yarnpkg.com/ospec/-/ospec-4.0.0.tgz#7aaf8cb6d352a3f22e879460eebbc5f12df99779" 81 | integrity sha512-MpDtkpscOxHYb4w71v7GB4LBsRuzxZnM+HdwjhzJQzu+5EJvA80yxTaKw+wp5Dmf5RV2/Bg3Uvz2vlI/PhW9Ow== 82 | dependencies: 83 | glob "^7.1.3" 84 | 85 | path-is-absolute@^1.0.0: 86 | version "1.0.1" 87 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 88 | integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== 89 | 90 | un.js@^0.5.9: 91 | version "0.5.9" 92 | resolved "https://registry.yarnpkg.com/un.js/-/un.js-0.5.9.tgz#892076199843867e015760cf1491897d91e12fcf" 93 | integrity sha512-uS13MGvJlleNV/rBkyEH6igfvXQZH8PG6HPeVD6n9kKMFSHTpezqXaMyfDPc8aalshye6XDTsDwM/AY9ZinLYQ== 94 | 95 | wrappy@1: 96 | version "1.0.2" 97 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 98 | integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== 99 | -------------------------------------------------------------------------------- /examples/basic/submit/README.md: -------------------------------------------------------------------------------- 1 | ## Runing the Example 2 | 3 | See https://github.com/dmitriz/un/tree/master/examples 4 | -------------------------------------------------------------------------------- /examples/basic/submit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/basic/submit/index.js: -------------------------------------------------------------------------------- 1 | // uncomponent - the pair of functions, 2 | // no imports - no external dependencies! 3 | 4 | // compute new state 5 | const reducer = (state = {}, value) => ({ 6 | submitted: value, 7 | changed: true 8 | }) 9 | 10 | const style = {width: '100%'} 11 | 12 | // pure with no dependencies 13 | const view = ({ form, input, p }) => 14 | ({ submitted, changed }, dispatch) => [ 15 | `Welcome to my Submit: `, 16 | `Type and hit ENTER: `, 17 | form({ 18 | onsubmit: e => { 19 | // prevents page reload 20 | e.preventDefault() 21 | dispatch(e.target.in.value) 22 | } 23 | }, [ 24 | input({ 25 | name: 'in', 26 | style, 27 | class: 'new-todo', 28 | placeholder: 'What is your string today?', 29 | autofocus: true, 30 | }) 31 | ]), 32 | changed 33 | ? `Thank you, here is your submission: ` 34 | : `You have submitted: `, 35 | p({style: { 36 | color: changed ? 'blue' : 'gray' 37 | }}, submitted) 38 | ] 39 | 40 | 41 | // the only method we need 42 | const mount = require('./un-mount') 43 | 44 | // create dom element 45 | const el = document.createElement('div') 46 | document.body.appendChild(el) 47 | 48 | // mount our live uncomponent 49 | mount({ 50 | el, 51 | reducer, 52 | view, 53 | initState: { 54 | submitted: `Nothing as of yet`, 55 | changed: false 56 | } 57 | }) 58 | -------------------------------------------------------------------------------- /examples/basic/submit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "main": "index.js", 4 | "description": "controls", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/dmitriz" 8 | }, 9 | "scripts": { 10 | "start": "npm run dev", 11 | "open": "open index.html", 12 | "build": "browserify index.js -o build.js", 13 | "dev": "budo index.js:build.js --live --open" 14 | }, 15 | "keywords": [], 16 | "author": "Dmitri Zaitsev (https://github.com/dmitriz)", 17 | "license": "MIT", 18 | "dependencies": { 19 | "hyperscript-helpers": "^3.0.3", 20 | "mithril": "^2.0.3", 21 | "un.js": "^0.5.9" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/basic/submit/un-mount.js: -------------------------------------------------------------------------------- 1 | // the only basic method you need to import 2 | const createMount = require('un.js') 3 | 4 | const mount = createMount({ 5 | 6 | // your favorite stream factory 7 | // TODO: flyd, most, xstream 8 | createStream: require("mithril/stream"), 9 | 10 | // your favorite element creator 11 | // TODO: (React|Preact|Inferno).createElement, snabbdom/h, hyperscript 12 | createElement: require('mithril'), 13 | 14 | // your favorite create tags helpers 15 | createTags: require('hyperscript-helpers'), 16 | 17 | // TODO: (React|Preact|Inferno).render, snabbdom-patch, replaceWith 18 | createRender: element => vnode => require('mithril').render(element, vnode) 19 | }) 20 | 21 | // export configured 'mount' with our library choices 22 | module.exports = mount 23 | -------------------------------------------------------------------------------- /examples/basic/submit/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | hyperscript-helpers@^3.0.3: 6 | version "3.0.3" 7 | resolved "https://registry.yarnpkg.com/hyperscript-helpers/-/hyperscript-helpers-3.0.3.tgz#583ac45214c9ea810991feb97bdb37fc3d12a1e8" 8 | 9 | mithril@^2.0.3: 10 | version "2.0.4" 11 | resolved "https://registry.yarnpkg.com/mithril/-/mithril-2.0.4.tgz#d125969d992b924c7185d24ff92d997e5c00116b" 12 | integrity sha512-mgw+DMZlhMS4PpprF6dl7ZoeZq5GGcAuWnrg5e12MvaGauc4jzWsDZtVGRCktsiQczOEUr2K5teKbE5k44RlOg== 13 | 14 | un.js@^0.5.9: 15 | version "0.5.9" 16 | resolved "https://registry.yarnpkg.com/un.js/-/un.js-0.5.9.tgz#892076199843867e015760cf1491897d91e12fcf" 17 | integrity sha512-uS13MGvJlleNV/rBkyEH6igfvXQZH8PG6HPeVD6n9kKMFSHTpezqXaMyfDPc8aalshye6XDTsDwM/AY9ZinLYQ== 18 | -------------------------------------------------------------------------------- /examples/basic/todos-with-delete/README.md: -------------------------------------------------------------------------------- 1 | ## Runing the Example 2 | 3 | See https://github.com/dmitriz/un/tree/master/examples 4 | -------------------------------------------------------------------------------- /examples/basic/todos-with-delete/app.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-font-smoothing: antialiased; 21 | font-smoothing: antialiased; 22 | } 23 | 24 | body { 25 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 26 | line-height: 1.4em; 27 | background: #f5f5f5; 28 | color: #4d4d4d; 29 | min-width: 230px; 30 | max-width: 550px; 31 | margin: 0 auto; 32 | -webkit-font-smoothing: antialiased; 33 | -moz-font-smoothing: antialiased; 34 | font-smoothing: antialiased; 35 | font-weight: 300; 36 | } 37 | 38 | button, 39 | input[type="checkbox"] { 40 | outline: none; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | } 46 | 47 | #todoapp { 48 | background: #fff; 49 | margin: 130px 0 40px 0; 50 | position: relative; 51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 52 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 53 | } 54 | 55 | #todoapp input::-webkit-input-placeholder { 56 | font-style: italic; 57 | font-weight: 300; 58 | color: #e6e6e6; 59 | } 60 | 61 | #todoapp input::-moz-placeholder { 62 | font-style: italic; 63 | font-weight: 300; 64 | color: #e6e6e6; 65 | } 66 | 67 | #todoapp input::input-placeholder { 68 | font-style: italic; 69 | font-weight: 300; 70 | color: #e6e6e6; 71 | } 72 | 73 | #todoapp h1 { 74 | position: absolute; 75 | top: -155px; 76 | width: 100%; 77 | font-size: 100px; 78 | font-weight: 100; 79 | text-align: center; 80 | color: rgba(175, 47, 47, 0.15); 81 | -webkit-text-rendering: optimizeLegibility; 82 | -moz-text-rendering: optimizeLegibility; 83 | text-rendering: optimizeLegibility; 84 | } 85 | 86 | #new-todo, 87 | .edit { 88 | position: relative; 89 | margin: 0; 90 | width: 100%; 91 | font-size: 24px; 92 | font-family: inherit; 93 | font-weight: inherit; 94 | line-height: 1.4em; 95 | border: 0; 96 | outline: none; 97 | color: inherit; 98 | padding: 6px; 99 | border: 1px solid #999; 100 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 101 | box-sizing: border-box; 102 | -webkit-font-smoothing: antialiased; 103 | -moz-font-smoothing: antialiased; 104 | font-smoothing: antialiased; 105 | } 106 | 107 | #new-todo { 108 | padding: 16px 16px 16px 60px; 109 | border: none; 110 | background: rgba(0, 0, 0, 0.003); 111 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 112 | } 113 | 114 | #main { 115 | position: relative; 116 | z-index: 2; 117 | border-top: 1px solid #e6e6e6; 118 | } 119 | 120 | label[for='toggle-all'] { 121 | display: none; 122 | } 123 | 124 | #toggle-all { 125 | position: absolute; 126 | top: -55px; 127 | left: -12px; 128 | width: 60px; 129 | height: 34px; 130 | text-align: center; 131 | border: none; /* Mobile Safari */ 132 | } 133 | 134 | #toggle-all:before { 135 | content: '❯'; 136 | font-size: 22px; 137 | color: #e6e6e6; 138 | padding: 10px 27px 10px 27px; 139 | } 140 | 141 | #toggle-all:checked:before { 142 | color: #737373; 143 | } 144 | 145 | #todo-list { 146 | margin: 0; 147 | padding: 0; 148 | list-style: none; 149 | } 150 | 151 | #todo-list li { 152 | position: relative; 153 | font-size: 24px; 154 | border-bottom: 1px solid #ededed; 155 | } 156 | 157 | #todo-list li:last-child { 158 | border-bottom: none; 159 | } 160 | 161 | #todo-list li.editing { 162 | border-bottom: none; 163 | padding: 0; 164 | } 165 | 166 | #todo-list li.editing .edit { 167 | display: block; 168 | width: 506px; 169 | padding: 13px 17px 12px 17px; 170 | margin: 0 0 0 43px; 171 | } 172 | 173 | #todo-list li.editing .view { 174 | display: none; 175 | } 176 | 177 | #todo-list li .toggle { 178 | text-align: center; 179 | width: 40px; 180 | /* auto, since non-WebKit browsers doesn't support input styling */ 181 | height: auto; 182 | position: absolute; 183 | top: 0; 184 | bottom: 0; 185 | margin: auto 0; 186 | border: none; /* Mobile Safari */ 187 | -webkit-appearance: none; 188 | appearance: none; 189 | } 190 | 191 | #todo-list li .toggle:after { 192 | content: url('data:image/svg+xml;utf8,'); 193 | } 194 | 195 | #todo-list li .toggle:checked:after { 196 | content: url('data:image/svg+xml;utf8,'); 197 | } 198 | 199 | #todo-list li label { 200 | white-space: pre; 201 | word-break: break-word; 202 | padding: 15px 60px 15px 15px; 203 | margin-left: 45px; 204 | display: block; 205 | line-height: 1.2; 206 | transition: color 0.4s; 207 | } 208 | 209 | #todo-list li.completed label { 210 | color: #d9d9d9; 211 | text-decoration: line-through; 212 | } 213 | 214 | #todo-list li .destroy { 215 | display: none; 216 | position: absolute; 217 | top: 0; 218 | right: 10px; 219 | bottom: 0; 220 | width: 40px; 221 | height: 40px; 222 | margin: auto 0; 223 | font-size: 30px; 224 | color: #cc9a9a; 225 | margin-bottom: 11px; 226 | transition: color 0.2s ease-out; 227 | } 228 | 229 | #todo-list li .destroy:hover { 230 | color: #af5b5e; 231 | } 232 | 233 | #todo-list li .destroy:after { 234 | content: '×'; 235 | } 236 | 237 | #todo-list li:hover .destroy { 238 | display: block; 239 | } 240 | 241 | #todo-list li .edit { 242 | display: none; 243 | } 244 | 245 | #todo-list li.editing:last-child { 246 | margin-bottom: -1px; 247 | } 248 | 249 | #footer { 250 | color: #777; 251 | padding: 10px 15px; 252 | height: 20px; 253 | text-align: center; 254 | border-top: 1px solid #e6e6e6; 255 | } 256 | 257 | #footer:before { 258 | content: ''; 259 | position: absolute; 260 | right: 0; 261 | bottom: 0; 262 | left: 0; 263 | height: 50px; 264 | overflow: hidden; 265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 266 | 0 8px 0 -3px #f6f6f6, 267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 268 | 0 16px 0 -6px #f6f6f6, 269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 270 | } 271 | 272 | #todo-count { 273 | float: left; 274 | text-align: left; 275 | } 276 | 277 | #todo-count strong { 278 | font-weight: 300; 279 | } 280 | 281 | #filters { 282 | margin: 0; 283 | padding: 0; 284 | list-style: none; 285 | position: absolute; 286 | right: 0; 287 | left: 0; 288 | } 289 | 290 | #filters li { 291 | display: inline; 292 | } 293 | 294 | #filters li a { 295 | color: inherit; 296 | margin: 3px; 297 | padding: 3px 7px; 298 | text-decoration: none; 299 | border: 1px solid transparent; 300 | border-radius: 3px; 301 | } 302 | 303 | #filters li a.selected, 304 | #filters li a:hover { 305 | border-color: rgba(175, 47, 47, 0.1); 306 | } 307 | 308 | #filters li a.selected { 309 | border-color: rgba(175, 47, 47, 0.2); 310 | } 311 | 312 | #clear-completed, 313 | html #clear-completed:active { 314 | float: right; 315 | position: relative; 316 | line-height: 20px; 317 | text-decoration: none; 318 | cursor: pointer; 319 | position: relative; 320 | } 321 | 322 | #clear-completed:hover { 323 | text-decoration: underline; 324 | } 325 | 326 | #info { 327 | margin: 65px auto 0; 328 | color: #bfbfbf; 329 | font-size: 10px; 330 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 331 | text-align: center; 332 | } 333 | 334 | #info p { 335 | line-height: 1; 336 | } 337 | 338 | #info a { 339 | color: inherit; 340 | text-decoration: none; 341 | font-weight: 400; 342 | } 343 | 344 | #info a:hover { 345 | text-decoration: underline; 346 | } 347 | 348 | /* 349 | Hack to remove background from Mobile Safari. 350 | Can't use it globally since it destroys checkboxes in Firefox 351 | */ 352 | @media screen and (-webkit-min-device-pixel-ratio:0) { 353 | #toggle-all, 354 | #todo-list li .toggle { 355 | background: none; 356 | } 357 | 358 | #todo-list li .toggle { 359 | height: 40px; 360 | } 361 | 362 | #toggle-all { 363 | -webkit-transform: rotate(90deg); 364 | transform: rotate(90deg); 365 | -webkit-appearance: none; 366 | appearance: none; 367 | } 368 | } 369 | 370 | @media (max-width: 430px) { 371 | #footer { 372 | height: 50px; 373 | } 374 | 375 | #filters { 376 | bottom: 10px; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /examples/basic/todos-with-delete/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/basic/todos-with-delete/index.js: -------------------------------------------------------------------------------- 1 | // uncomponent - the pair of functions, 2 | // no imports - no external dependencies! 3 | 4 | // compute new state 5 | const reducer = ({ todos } = {}, { type, todo } = {}) => ({ 6 | 'ADD': { 7 | newTitle: ``, 8 | todos: todos.concat([todo]), 9 | }, 10 | 'DELETE': { 11 | // delete by name! 12 | todos: todos.filter(t => t !== todo) 13 | }, 14 | }[type]) 15 | 16 | 17 | 18 | // pure with no dependencies 19 | const view = ({ div, form, input, label, ul, li, h1, button }) => 20 | ({ newTitle, todos }, dispatch) => { 21 | 22 | const NewTodoInput = newTitle => 23 | form({ 24 | onsubmit: e => { 25 | e.preventDefault() 26 | const todo = e.target.in.value.trim() 27 | todo && dispatch({ type: 'ADD', todo }) 28 | } 29 | }, [ 30 | input('#new-todo', { 31 | name: 'in', 32 | placeholder: 'What needs to be done?', 33 | autofocus: true, 34 | value: newTitle, 35 | }) 36 | ]) 37 | 38 | const Todo = todo => 39 | li([ 40 | label(todo), 41 | button(".destroy", { 42 | onclick: () => dispatch({ type: 'DELETE', todo }) 43 | }), 44 | ]) 45 | 46 | const Main = todos => 47 | ul('#todo-list', todos.map(Todo)) 48 | 49 | return [ 50 | h1("my todos"), 51 | NewTodoInput(newTitle), 52 | Main(todos), 53 | ] 54 | } 55 | 56 | 57 | // the only method we need 58 | const mount = require('./un-mount') 59 | 60 | // create dom element 61 | const el = document.createElement('div') 62 | document.body.appendChild(el) 63 | 64 | // mount our live uncomponent 65 | mount({ 66 | el, 67 | reducer, 68 | view, 69 | initState: { 70 | newTitle: ``, 71 | todos: [], 72 | } 73 | }) 74 | -------------------------------------------------------------------------------- /examples/basic/todos-with-delete/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "main": "index.js", 4 | "description": "controls", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/dmitriz" 8 | }, 9 | "scripts": { 10 | "start": "npm run dev", 11 | "open": "open index.html", 12 | "build": "browserify index.js -o build.js", 13 | "dev": "budo index.js:build.js --live --open" 14 | }, 15 | "keywords": [], 16 | "author": "Dmitri Zaitsev (https://github.com/dmitriz)", 17 | "license": "MIT", 18 | "dependencies": { 19 | "hyperscript-helpers": "^3.0.3", 20 | "mithril": "^2.0.3", 21 | "un.js": "^0.5.9" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/basic/todos-with-delete/un-mount.js: -------------------------------------------------------------------------------- 1 | // the only basic method you need to import 2 | const createMount = require('un.js') 3 | 4 | const mount = createMount({ 5 | 6 | // your favorite stream factory 7 | // TODO: flyd, most, xstream 8 | createStream: require("mithril/stream"), 9 | 10 | // your favorite element creator 11 | // TODO: (React|Preact|Inferno).createElement, snabbdom/h, hyperscript 12 | createElement: require('mithril'), 13 | 14 | // your favorite create tags helpers 15 | createTags: require('hyperscript-helpers'), 16 | 17 | // TODO: (React|Preact|Inferno).render, snabbdom-patch, replaceWith 18 | createRender: element => vnode => require('mithril').render(element, vnode) 19 | }) 20 | 21 | // export configured 'mount' with our library choices 22 | module.exports = mount 23 | -------------------------------------------------------------------------------- /examples/basic/todos-with-delete/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | hyperscript-helpers@^3.0.3: 6 | version "3.0.3" 7 | resolved "https://registry.yarnpkg.com/hyperscript-helpers/-/hyperscript-helpers-3.0.3.tgz#583ac45214c9ea810991feb97bdb37fc3d12a1e8" 8 | 9 | mithril@^2.0.3: 10 | version "2.0.4" 11 | resolved "https://registry.yarnpkg.com/mithril/-/mithril-2.0.4.tgz#d125969d992b924c7185d24ff92d997e5c00116b" 12 | integrity sha512-mgw+DMZlhMS4PpprF6dl7ZoeZq5GGcAuWnrg5e12MvaGauc4jzWsDZtVGRCktsiQczOEUr2K5teKbE5k44RlOg== 13 | 14 | un.js@^0.5.9: 15 | version "0.5.9" 16 | resolved "https://registry.yarnpkg.com/un.js/-/un.js-0.5.9.tgz#892076199843867e015760cf1491897d91e12fcf" 17 | integrity sha512-uS13MGvJlleNV/rBkyEH6igfvXQZH8PG6HPeVD6n9kKMFSHTpezqXaMyfDPc8aalshye6XDTsDwM/AY9ZinLYQ== 18 | -------------------------------------------------------------------------------- /examples/basic/todos/README.md: -------------------------------------------------------------------------------- 1 | ## Runing the Example 2 | 3 | See https://github.com/dmitriz/un/tree/master/examples 4 | -------------------------------------------------------------------------------- /examples/basic/todos/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/basic/todos/index.js: -------------------------------------------------------------------------------- 1 | // uncomponent - the pair of functions, 2 | // no imports - no external dependencies! 3 | 4 | // compute new state 5 | const reducer = ({ submitted } = {}, title) => ({ 6 | title: ``, 7 | 8 | // add new value to submitted array 9 | submitted: submitted.concat([title]), 10 | changed: true 11 | }) 12 | 13 | const style = {width: '100%'} 14 | 15 | // pure with no dependencies 16 | const view = ({ form, input, ul, li }) => 17 | ({ title, submitted, changed }, dispatch) => [ 18 | `Enter your Todo: `, 19 | form({ 20 | onsubmit: e => { 21 | // prevents page reload 22 | e.preventDefault() 23 | dispatch(e.target.in.value) 24 | } 25 | }, [ 26 | input({ 27 | name: 'in', 28 | style, 29 | class: 'new-todo', 30 | placeholder: 'Your next Todo...', 31 | autofocus: true, 32 | value: title 33 | }) 34 | ]), 35 | changed 36 | ? `Thank you, here are your Todos:` 37 | : `You have no Todos`, 38 | ul({style: { 39 | color: changed ? 'blue' : 'gray' 40 | }}, submitted.map(key => li(` ${key} `))) 41 | ] 42 | 43 | 44 | // the only method we need 45 | const mount = require('./un-mount') 46 | 47 | // create dom element 48 | const el = document.createElement('div') 49 | document.body.appendChild(el) 50 | 51 | // mount our live uncomponent 52 | mount({ 53 | el, 54 | reducer, 55 | view, 56 | initState: { 57 | submitted: [], 58 | changed: false 59 | } 60 | }) 61 | -------------------------------------------------------------------------------- /examples/basic/todos/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "main": "index.js", 4 | "description": "controls", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/dmitriz" 8 | }, 9 | "scripts": { 10 | "start": "npm run dev", 11 | "open": "open index.html", 12 | "build": "browserify index.js -o build.js", 13 | "dev": "budo index.js:build.js --live --open" 14 | }, 15 | "keywords": [], 16 | "author": "Dmitri Zaitsev (https://github.com/dmitriz)", 17 | "license": "MIT", 18 | "dependencies": { 19 | "hyperscript-helpers": "^3.0.3", 20 | "mithril": "^2.0.3", 21 | "un.js": "^0.5.9" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/basic/todos/un-mount.js: -------------------------------------------------------------------------------- 1 | // the only basic method you need to import 2 | const createMount = require('un.js') 3 | 4 | const mount = createMount({ 5 | 6 | // your favorite stream factory 7 | // TODO: flyd, most, xstream 8 | createStream: require("mithril/stream"), 9 | 10 | // your favorite element creator 11 | // TODO: (React|Preact|Inferno).createElement, snabbdom/h, hyperscript 12 | createElement: require('mithril'), 13 | 14 | // your favorite create tags helpers 15 | createTags: require('hyperscript-helpers'), 16 | 17 | // TODO: (React|Preact|Inferno).render, snabbdom-patch, replaceWith 18 | createRender: element => vnode => require('mithril').render(element, vnode) 19 | }) 20 | 21 | // export configured 'mount' with our library choices 22 | module.exports = mount 23 | -------------------------------------------------------------------------------- /examples/basic/todos/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | hyperscript-helpers@^3.0.3: 6 | version "3.0.3" 7 | resolved "https://registry.yarnpkg.com/hyperscript-helpers/-/hyperscript-helpers-3.0.3.tgz#583ac45214c9ea810991feb97bdb37fc3d12a1e8" 8 | 9 | mithril@^2.0.3: 10 | version "2.0.4" 11 | resolved "https://registry.yarnpkg.com/mithril/-/mithril-2.0.4.tgz#d125969d992b924c7185d24ff92d997e5c00116b" 12 | integrity sha512-mgw+DMZlhMS4PpprF6dl7ZoeZq5GGcAuWnrg5e12MvaGauc4jzWsDZtVGRCktsiQczOEUr2K5teKbE5k44RlOg== 13 | 14 | un.js@^0.5.9: 15 | version "0.5.9" 16 | resolved "https://registry.yarnpkg.com/un.js/-/un.js-0.5.9.tgz#892076199843867e015760cf1491897d91e12fcf" 17 | integrity sha512-uS13MGvJlleNV/rBkyEH6igfvXQZH8PG6HPeVD6n9kKMFSHTpezqXaMyfDPc8aalshye6XDTsDwM/AY9ZinLYQ== 18 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Eric Elliott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/README.md: -------------------------------------------------------------------------------- 1 | # React Pure Component Starter 2 | [![Circle CI](https://circleci.com/gh/ericelliott/react-pure-component-starter.svg?style=svg)](https://circleci.com/gh/ericelliott/react-pure-component-starter) 3 | 4 | [React 0.14](https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html) introduced the ability to use pure functions as components. The react team calls them **functional components** in their announcement. The rest of the world calls them **pure components**. 5 | 6 | This repo demonstrates pure components. It's based on the [React Transform Boilerplate](https://github.com/gaearon/react-transform-boilerplate) and features: 7 | 8 | * Examples of pure components. 9 | * Pure component factories, so you can use a single React instance, even if you load React from CDN. 10 | * Unit test example with tape, demonstrating an easy way to test pure components. 11 | 12 | ## Getting Started 13 | 14 | **Clone the repo & install:** 15 | 16 | ```sh 17 | git clone git@github.com:ericelliott/react-pure-component-starter.git 18 | cd react-pure-component-starter 19 | npm install 20 | ``` 21 | 22 | **Start the dev server and follow instructions:** 23 | 24 | ```sh 25 | npm start 26 | ``` 27 | 28 | **In another terminal window, start the dev console for lint/test feedback when you save files:** 29 | 30 | ```sh 31 | npm run watch 32 | ``` 33 | 34 | 35 | ## Pure Component Factories 36 | 37 | Pure component factories let you inject your React instance into the component so that you can share a single React instance across your entire app -- even if you load React from CDN for client use (which may save lots of users time, because they'll already have it cached locally). 38 | 39 | I recommend that all your reusable components export factories and take a React instance as a dependency. It's really easy. A regular pure component looks like this: 40 | 41 | ```js 42 | export default (props) =>

{ props.title }

; 43 | ``` 44 | 45 | To add the factory wrapper for React injection, just insert another arrow function with a `React` parameter: 46 | 47 | ```js 48 | export default React => (props) =>

{ props.title }

; 49 | ``` 50 | 51 | If you're still confused, this desugars to this ordinary ES5: 52 | 53 | ```js 54 | "use strict"; 55 | 56 | module.exports = function (React) { 57 | return function (props) { 58 | return React.createElement( 59 | "h1", 60 | null, 61 | props.title 62 | ); 63 | }; 64 | }; 65 | ``` 66 | 67 | Yeah. Arrow functions rock. 68 | 69 | In case you blinked and missed it, the ES6 factory again: 70 | 71 | ```js 72 | export default React => (props) =>

{ props.title }

; 73 | ``` 74 | 75 | As you can see, React is a parameter, but it doesn't get explicitly mentioned anywhere in the rest of the line... and there are no other lines. So why do we need it? 76 | 77 | Remember that [**JSX is not real DOM markup**](https://medium.com/javascript-scene/jsx-looks-like-an-abomination-1c1ec351a918). There's a compile step that transforms the JSX code into this: 78 | 79 | ```js 80 | React.createElement( 81 | "h1", 82 | null, 83 | props.title 84 | ); 85 | ``` 86 | 87 | That compiled output uses React, and expects it to be available inside the component scope, so you need to pass `React` in, even though it's not obvious that it's being used. 88 | 89 | ## Unit Testing React Components 90 | 91 | I recommend Test Driven Development (TDD). Write your tests first. Learn how to write unit tests: Read [5 Questions Every Unit Test Must Answer](https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d). 92 | 93 | Unit testing React components is a lot easier than it sounds. Let's look at the imports for the `title` example in `test/components/title/index.js`: 94 | 95 | ```js 96 | import React from 'react'; 97 | import reactDom from 'react-dom/server'; 98 | import test from 'tape'; 99 | import dom from 'cheerio'; 100 | ``` 101 | 102 | The first line pulls in React, which you'll need to pass into the component factory. As we already mentioned, it's also required for the JSX to work. 103 | 104 | 105 | ### react-dom 106 | 107 | ```js 108 | import reactDom from 'react-dom/server' 109 | ```` 110 | 111 | React 0.14 split the DOM utilities out of the main React package. There are several reasons for this change. One of the more important reasons is that React is not always used to render HTML DOM. For instance, Netflix uses it to render to an in-house rendering library called [Gibbon](https://www.youtube.com/watch?v=eNC0mRYGWgc), and Facebook has another framework called [React Native](https://facebook.github.io/react-native/), which lets you build native user interfaces on mobile using JavaScript, sharing much of the same code and architecture with your web and server apps. 112 | 113 | So, react's DOM utilities now live in `react-dom`, which is split in two: 114 | 115 | * `react-dom/server` 116 | * `react-dom/client` 117 | 118 | ### Tape 119 | 120 | ```js 121 | import test from 'tape'; 122 | ``` 123 | 124 | Tape is a great testrunner because it keeps everything **very simple**. For details, read [Why I Use Tape Instead of Mocha, and So Should You](https://medium.com/javascript-scene/why-i-use-tape-instead-of-mocha-so-should-you-6aa105d8eaf4). 125 | 126 | ### Cheerio 127 | 128 | ```js 129 | import dom from 'cheerio'; 130 | ``` 131 | 132 | Cheerio is a jQuery-like API for querying and manipulating DOM nodes. If you know jQuery, you know Cheerio. 133 | 134 | I use it for testing React component outputs. Much better than the peculiarly named `.findRenderedDOMComponentWithClass()` and `.scryRenderedDOMComponentsWithClass()` (I swear, [I'm not making these up](https://facebook.github.io/react/docs/test-utils.html)). 135 | 136 | It does not need JSDom. It does not need Selenium web driver. It does not need a browser. Not even PhantomJS. Your DOM is just DOM. Save the browser wrangling for your critical path functional tests. Keep your component unit tests **simple**. 137 | 138 | > "Simplicity is a feature." ~ Jafar Husain (Netflix, TC39) 139 | 140 | Learn JavaScript with Eric Elliott 141 | ================================== 142 | eejs-screenshot 143 | 144 | [![Join the chat at https://gitter.im/learn-javascript-courses/javascript-questions](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/learn-javascript-courses/javascript-questions?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 145 | 146 | An online course series for application developers. Ready to jump in? [Learn more](https://ericelliottjs.com/). 147 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/devServer.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | 4 | var app = express(); 5 | 6 | app.get('*', function(req, res) { 7 | res.sendFile(path.join(__dirname, 'index.html')); 8 | }); 9 | 10 | app.listen(3000, 'localhost', function(err) { 11 | if (err) { 12 | console.log(err); 13 | return; 14 | } 15 | 16 | console.log('Listening at http://localhost:3000'); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Pure Component Starter 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-transform-boilerplate", 3 | "version": "2.2.0", 4 | "description": "A new Webpack boilerplate with hot reloading React components, and error handling on module and component level.", 5 | "scripts": { 6 | "clean": "rimraf build", 7 | "build:webpack": "webpack --config webpack.config.prod.js", 8 | "build": "npm run clean && npm run build:webpack", 9 | "start": "cross-env NODE_PATH='source' node devServer.js", 10 | "debug": "cross-env NODE_PATH='source' echo 'Nothing? `npm install -g iron-node`' && iron-node source/debug.js", 11 | "lint": "eslint source", 12 | "pretest": "npm run lint", 13 | "test": "cross-env NODE_PATH='source' babel-node source/test/index.js", 14 | "watch": "watch \"clear && npm run test -s\" source", 15 | "update": "updtr" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/gaearon/react-transform-boilerplate.git" 20 | }, 21 | "keywords": [ 22 | "react", 23 | "reactjs", 24 | "boilerplate", 25 | "webpack", 26 | "babel", 27 | "react-transform" 28 | ], 29 | "author": "Dan Abramov (http://github.com/gaearon)", 30 | "license": "CC0-1.0", 31 | "bugs": { 32 | "url": "https://github.com/gaearon/react-transform-boilerplate/issues" 33 | }, 34 | "homepage": "https://github.com/gaearon/react-transform-boilerplate", 35 | "devDependencies": { 36 | "cross-env": "5.2.1", 37 | "deep-freeze": "0.0.1", 38 | "rimraf": "3.0.1", 39 | "tape": "4.11.0", 40 | "updtr": "3.1.0", 41 | "watch": "1.0.2", 42 | "webpack": "4.36.0", 43 | "webpack-dev-middleware": "3.7.1" 44 | }, 45 | "dependencies": { 46 | "react": "^16.8.6", 47 | "redux": "^4.0.1", 48 | "react-dom": "^16.8.6", 49 | "express": "4.19.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/source/App.js: -------------------------------------------------------------------------------- 1 | import createTitle from 'components/title'; 2 | import createHello from 'components/hello'; 3 | import { createStore } from 'redux'; 4 | 5 | import hello from 'store/reducers/hello'; 6 | 7 | const store = createStore(hello); 8 | 9 | const setMode = (mode) => store.dispatch({ type: 'SET_MODE', mode }); 10 | 11 | store.subscribe(() => { 12 | console.log(store.getState()); 13 | }); 14 | 15 | export default React => ({ foo, ...props }) => { 16 | const Title = createTitle(React); 17 | const Hello = createHello(React); 18 | const helloProps = { 19 | ...props, 20 | actions: { 21 | setMode 22 | } 23 | }; 24 | 25 | return ( 26 |
27 | 28 | <Hello { ...helloProps } /> 29 | <p>Content goes here: { foo }</p> 30 | </div> 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/source/App.jsx: -------------------------------------------------------------------------------- 1 | import createTitle from 'components/title'; 2 | import createHello from 'components/hello'; 3 | import { createStore } from 'redux'; 4 | 5 | import hello from 'store/reducers/hello'; 6 | 7 | const store = createStore(hello); 8 | 9 | const setMode = (mode) => store.dispatch({ type: 'SET_MODE', mode }); 10 | 11 | store.subscribe(() => { 12 | console.log(store.getState()); 13 | }); 14 | 15 | export default React => ({ foo, ...props }) => { 16 | const Title = createTitle(React); 17 | const Hello = createHello(React); 18 | const helloProps = { 19 | ...props, 20 | actions: { 21 | setMode 22 | } 23 | }; 24 | 25 | return ( 26 | <div className="content"> 27 | <Title { ...props } /> 28 | <Hello { ...helloProps } /> 29 | <p>Content goes here: { foo }</p> 30 | </div> 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/source/components/hello/index.js: -------------------------------------------------------------------------------- 1 | export default React => { 2 | 3 | const { 4 | string, shape, func 5 | } = React.PropTypes; 6 | 7 | const hello = ({ helloClass, subject = 'World', actions: { setMode } }) => { 8 | return ( 9 | <p className={ helloClass } onClick={ () => setMode('edit') }>Hello, { subject }!</p> 10 | ); 11 | }; 12 | 13 | hello.propTypes = { 14 | helloClass: string.isRequired, 15 | subject: string, 16 | actions: shape({ 17 | setMode: func.isRequired 18 | }) 19 | }; 20 | 21 | return hello; 22 | }; 23 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/source/components/hello/index.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | 3 | const { 4 | string, shape, func 5 | } = React.PropTypes; 6 | 7 | const hello = ({ helloClass, subject = 'World', actions: { setMode } }) => { 8 | return ( 9 | <p className={ helloClass } onClick={ () => setMode('edit') }>Hello, { subject }!</p> 10 | ); 11 | }; 12 | 13 | hello.propTypes = { 14 | helloClass: string.isRequired, 15 | subject: string, 16 | actions: shape({ 17 | setMode: func.isRequired 18 | }) 19 | }; 20 | 21 | return hello; 22 | }; 23 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/source/components/title/index.js: -------------------------------------------------------------------------------- 1 | export default React => ({ titleClass, title }) => { 2 | return <h1 className={ titleClass }>{ title }</h1>; 3 | }; 4 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/source/debug.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register'); 2 | require('../devServer.js'); 3 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/source/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import createApp from './App'; 4 | 5 | const App = createApp(React); 6 | 7 | const props = { 8 | foo: 'yay! 🎸🎶', 9 | title: 'Pure Components Rock', 10 | helloClass: 'hello' 11 | }; 12 | 13 | render( 14 | <App { ...props }></App>, 15 | document.getElementById('root') 16 | ); 17 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/source/store/reducers/hello/index.js: -------------------------------------------------------------------------------- 1 | const assign = Object.assign; 2 | 3 | export default ( 4 | state = { mode: 'display', subject: 'World' }, { mode, subject, type } = {} 5 | ) => { 6 | 7 | switch (type) { 8 | case 'SET_MODE': 9 | return assign({}, state, { 10 | mode 11 | }); 12 | case 'SET_SUBJECT': 13 | return assign({}, state, { 14 | subject 15 | }); 16 | default: 17 | return state; 18 | } 19 | 20 | }; 21 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/source/test-fixtures/components/hello/create-actions.js: -------------------------------------------------------------------------------- 1 | const createActions = (actions) => { 2 | return Object.assign( 3 | {}, 4 | { 5 | setWord () {}, 6 | setMode () {} 7 | }, 8 | actions 9 | ); 10 | }; 11 | 12 | export default createActions; 13 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/source/test/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import reactDom from 'react-dom/server'; 3 | import test from 'tape'; 4 | import dom from 'cheerio'; 5 | 6 | import createApp from 'App.js'; 7 | import createActions from 'test-fixtures/components/hello/create-actions'; 8 | 9 | const render = reactDom.renderToStaticMarkup; 10 | const App = createApp(React); 11 | 12 | test('Hello', assert => { 13 | const msg = 'Should render all sections.'; 14 | 15 | const props = { 16 | foo: 'foo', 17 | helloClass: 'hello', 18 | titleClass: 'title', 19 | title: 'Yay!', 20 | actions: createActions() 21 | }; 22 | 23 | const el = <App{ ...props } />; 24 | const $ = dom.load(render(el)); 25 | 26 | const actual = { 27 | Hello: Boolean($(`.${ props.helloClass }`).html()), 28 | Title: Boolean($(`.${ props.titleClass }`).html()) 29 | }; 30 | 31 | const expected = { 32 | Hello: true, 33 | Title: true 34 | }; 35 | 36 | assert.deepEqual(actual, expected, msg); 37 | 38 | assert.end(); 39 | }); 40 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/source/test/components/hello/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import reactDom from 'react-dom/server'; 3 | import test from 'tape'; 4 | import dom from 'cheerio'; 5 | 6 | import hello from 'components/hello'; 7 | import createActions from 'test-fixtures/components/hello/create-actions'; 8 | 9 | const Hello = hello(React); 10 | const render = reactDom.renderToStaticMarkup; 11 | 12 | test('Hello', nest => { 13 | nest.test('...with no parameters', assert => { 14 | const msg = 'should render our hello greeting!'; 15 | 16 | const text = '<p>Hello, World!</p>'; 17 | const re = new RegExp(text, 'g'); 18 | 19 | const props = { 20 | actions: createActions() 21 | }; 22 | 23 | const el = <Hello { ...props } />; 24 | const $ = dom.load(render(el)); 25 | const output = $.html(); 26 | 27 | const actual = re.test(output); 28 | const expected = true; 29 | 30 | assert.equal(actual, expected, msg); 31 | 32 | assert.end(); 33 | }); 34 | 35 | nest.test('...with a subject', assert => { 36 | const msg = 'should render greeting with correct subject!'; 37 | 38 | const text = '<p>Hello, React!</p>'; 39 | const re = new RegExp(text, 'g'); 40 | 41 | const props = { 42 | subject: 'React', 43 | actions: createActions() 44 | }; 45 | 46 | const el = <Hello { ...props } />; 47 | const $ = dom.load(render(el)); 48 | const output = $.html(); 49 | 50 | const actual = re.test(output); 51 | const expected = true; 52 | 53 | assert.equal(actual, expected, msg); 54 | 55 | assert.end(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/source/test/components/title/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import reactDom from 'react-dom/server'; 3 | import test from 'tape'; 4 | import dom from 'cheerio'; 5 | 6 | import createTitle from 'components/title'; 7 | 8 | const Title = createTitle(React); 9 | const render = reactDom.renderToStaticMarkup; 10 | 11 | test('Title', assert => { 12 | const titleText = 'Hello!'; 13 | const props = { 14 | title: titleText, 15 | titleClass: 'title' 16 | }; 17 | const re = new RegExp(titleText, 'g'); 18 | 19 | const el = <Title { ...props } />; 20 | const $ = dom.load(render(el)); 21 | const output = $('.title').html(); 22 | const actual = re.test(output); 23 | const expected = true; 24 | 25 | assert.equal(actual, expected, 26 | 'should output the correct title text'); 27 | 28 | assert.end(); 29 | }); 30 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/source/test/index.js: -------------------------------------------------------------------------------- 1 | import './components/title'; 2 | import './components/hello'; 3 | import './App.js'; 4 | import './store/reducers/hello'; 5 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/source/test/store/reducers/hello/index.js: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import deepFreeze from 'deep-freeze'; 3 | 4 | import hello from 'store/reducers/hello'; 5 | 6 | test('SET_MODE', nest => { 7 | nest.test('...initial', assert => { 8 | const message = `should set { mode: 'display', subject: 'world' }`; 9 | 10 | const expected = { 11 | mode: 'display', 12 | subject: 'World' 13 | }; 14 | 15 | const actual = hello(); 16 | 17 | assert.deepEqual(actual, expected, message); 18 | assert.end(); 19 | }); 20 | 21 | 22 | nest.test(`...with { mode: 'edit' }`, assert => { 23 | const message = 'should set mode to edit mode'; 24 | 25 | const stateBefore = { 26 | mode: 'display', 27 | subject: 'World' 28 | }; 29 | const action = { 30 | type: 'SET_MODE', 31 | mode: 'edit' 32 | }; 33 | const expected = { 34 | mode: 'edit', 35 | subject: 'World' 36 | }; 37 | 38 | deepFreeze(stateBefore); 39 | deepFreeze(action); 40 | 41 | const actual = hello(stateBefore, action); 42 | 43 | assert.deepEqual(actual, expected, message); 44 | assert.end(); 45 | }); 46 | 47 | nest.test(`...with { subject: 'foo'}`, assert => { 48 | const message = 'should set subject to "foo"'; 49 | 50 | const stateBefore = { 51 | mode: 'display', 52 | subject: 'World' 53 | }; 54 | const action = { 55 | type: 'SET_SUBJECT', 56 | subject: 'foo' 57 | }; 58 | const expected = { 59 | mode: 'display', 60 | subject: 'foo' 61 | }; 62 | 63 | deepFreeze(stateBefore); 64 | deepFreeze(action); 65 | 66 | const actual = hello(stateBefore, action); 67 | 68 | assert.deepEqual(actual, expected, message); 69 | assert.end(); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | devtool: 'eval', 5 | resolve: { 6 | root: __dirname + '/source' 7 | }, 8 | entry: [ 9 | './source/index' 10 | ], 11 | output: { 12 | path: path.join(__dirname, 'build'), 13 | filename: 'index.js', 14 | publicPath: '/static/' 15 | }, 16 | module: { 17 | loaders: [{ 18 | test: /\.js$/, 19 | loader: 'babel-loader', 20 | include: path.join(__dirname, 'source'), 21 | query: { 22 | presets: ['es2015', 'stage-0', 'react'] 23 | } 24 | }] 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/webpack.config.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmitriz/un/250e7633178f59fbf7f40ea9d5d06ab813662cc0/examples/interframeworkability/react-pure/webpack.config.js -------------------------------------------------------------------------------- /examples/interframeworkability/react-pure/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | resolve: { 7 | root: __dirname + '/source' 8 | }, 9 | entry: [ 10 | './source/index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'build'), 14 | filename: 'index.js', 15 | publicPath: '/static/' 16 | }, 17 | plugins: [ 18 | new webpack.optimize.OccurenceOrderPlugin(), 19 | new webpack.DefinePlugin({ 20 | 'process.env': { 21 | 'NODE_ENV': JSON.stringify('production') 22 | } 23 | }), 24 | new webpack.optimize.UglifyJsPlugin({ 25 | compressor: { 26 | warnings: false 27 | } 28 | }) 29 | ], 30 | module: { 31 | loaders: [{ 32 | test: /\.js$/, 33 | loader: 'babel-loader', 34 | include: path.join(__dirname, 'source'), 35 | query: { 36 | presets: ['es2015', 'stage-0', 'react'] 37 | } 38 | }] 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-redux-counter/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-redux-counter/README.md: -------------------------------------------------------------------------------- 1 | # Redux Counter Example 2 | 3 | Source: https://github.com/reactjs/redux/tree/master/examples/counter 4 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-redux-counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "enzyme": "^3.9.0", 7 | "react-addons-test-utils": "^15.3.0", 8 | "react-scripts": "^3.0.1" 9 | }, 10 | "dependencies": { 11 | "prop-types": "^15.5.8", 12 | "react": "^16.8.6", 13 | "react-dom": "^16.8.6", 14 | "react-hyperscript": "^3.0.0", 15 | "react-hyperscript-helpers": "^2.0.0", 16 | "react-redux": "^7.2.0", 17 | "redux": "^4.0.1", 18 | "un.js": "^0.5.9" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "eject": "react-scripts eject", 24 | "test": "react-scripts test" 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-redux-counter/public/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8"> 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> 6 | <title>Redux Counter Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-redux-counter/src/components/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import hh from 'react-hyperscript-helpers' 5 | const { button } = hh 6 | 7 | // uncomponent: pure function, no dependencies, 8 | // testable in any environment 9 | const StyledButton = ({ button }) => (style, onClick, label) => 10 | button({ style, onClick }, label) 11 | 12 | const StyledButtonWithProps = ({style, onClick, label}) => 13 | button({ style, onClick }, label) 14 | 15 | 16 | class Counter extends Component { 17 | static propTypes = { 18 | value: PropTypes.number.isRequired, 19 | onIncrement: PropTypes.func.isRequired, 20 | onDecrement: PropTypes.func.isRequired 21 | } 22 | 23 | incrementIfOdd = () => { 24 | if (this.props.value % 2 !== 0) { 25 | this.props.onIncrement() 26 | } 27 | } 28 | 29 | incrementAsync = () => { 30 | setTimeout(this.props.onIncrement, 1000) 31 | } 32 | 33 | render() { 34 | 35 | // custom style 36 | const style = {color: 'blue'} 37 | 38 | const { value, onIncrement, onDecrement } = this.props 39 | return ( 40 |

41 | Clicked: {value} times 42 | {' '} 43 | 46 | {' '} 47 | 50 | {' '} 51 | 52 | { 53 | // place your pure function right here 54 | StyledButton(hh)(style, onDecrement, `pure unbutton for - `) 55 | } 56 | 57 | {' '} 58 | 59 | 64 | 65 | {' '} 66 | 69 | {' '} 70 | 73 |

74 | ) 75 | } 76 | } 77 | 78 | export default Counter 79 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-redux-counter/src/components/Counter.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import Counter from './Counter' 4 | 5 | function setup(value = 0) { 6 | const actions = { 7 | onIncrement: jest.fn(), 8 | onDecrement: jest.fn() 9 | } 10 | const component = shallow( 11 | 12 | ) 13 | 14 | return { 15 | component: component, 16 | actions: actions, 17 | buttons: component.find('button'), 18 | p: component.find('p') 19 | } 20 | } 21 | 22 | describe('Counter component', () => { 23 | it('should display count', () => { 24 | const { p } = setup() 25 | expect(p.text()).toMatch(/^Clicked: 0 times/) 26 | }) 27 | 28 | it('first button should call onIncrement', () => { 29 | const { buttons, actions } = setup() 30 | buttons.at(0).simulate('click') 31 | expect(actions.onIncrement).toBeCalled() 32 | }) 33 | 34 | it('second button should call onDecrement', () => { 35 | const { buttons, actions } = setup() 36 | buttons.at(1).simulate('click') 37 | expect(actions.onDecrement).toBeCalled() 38 | }) 39 | 40 | it('third button should not call onIncrement if the counter is even', () => { 41 | const { buttons, actions } = setup(42) 42 | buttons.at(2).simulate('click') 43 | expect(actions.onIncrement).not.toBeCalled() 44 | }) 45 | 46 | it('third button should call onIncrement if the counter is odd', () => { 47 | const { buttons, actions } = setup(43) 48 | buttons.at(2).simulate('click') 49 | expect(actions.onIncrement).toBeCalled() 50 | }) 51 | 52 | it('third button should call onIncrement if the counter is odd and negative', () => { 53 | const { buttons, actions } = setup(-43) 54 | buttons.at(2).simulate('click') 55 | expect(actions.onIncrement).toBeCalled() 56 | }) 57 | 58 | it('fourth button should call onIncrement in a second', (done) => { 59 | const { buttons, actions } = setup() 60 | buttons.at(3).simulate('click') 61 | setTimeout(() => { 62 | expect(actions.onIncrement).toBeCalled() 63 | done() 64 | }, 1000) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-redux-counter/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { createStore } from 'redux' 4 | import Counter from './components/Counter' 5 | import counter from './reducers' 6 | 7 | const store = createStore(counter) 8 | const rootEl = document.getElementById('root') 9 | 10 | const render = () => ReactDOM.render( 11 | store.dispatch({ type: 'INCREMENT' })} 14 | onDecrement={() => store.dispatch({ type: 'DECREMENT' })} 15 | />, 16 | rootEl 17 | ) 18 | 19 | render() 20 | store.subscribe(render) 21 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-redux-counter/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | export default (state = 0, action) => { 2 | switch (action.type) { 3 | case 'INCREMENT': 4 | return state + 1 5 | case 'DECREMENT': 6 | return state - 1 7 | default: 8 | return state 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-redux-counter/src/reducers/index.spec.js: -------------------------------------------------------------------------------- 1 | import counter from './index' 2 | 3 | describe('reducers', () => { 4 | describe('counter', () => { 5 | it('should provide the initial state', () => { 6 | expect(counter(undefined, {})).toBe(0) 7 | }) 8 | 9 | it('should handle INCREMENT action', () => { 10 | expect(counter(1, { type: 'INCREMENT' })).toBe(2) 11 | }) 12 | 13 | it('should handle DECREMENT action', () => { 14 | expect(counter(1, { type: 'DECREMENT' })).toBe(0) 15 | }) 16 | 17 | it('should ignore unknown actions', () => { 18 | expect(counter(1, { type: 'unknown' })).toBe(1) 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-redux-counter/un-mount.js: -------------------------------------------------------------------------------- 1 | // the only basic method you need to import 2 | const createMount = require('un.js') 3 | 4 | const mount = createMount({ 5 | 6 | // your favorite stream factory 7 | // TODO: flyd, most, xstream 8 | createStream: require("mithril/stream"), 9 | 10 | // your favorite create tags helpers 11 | createTags: require('react-hyperscript-helpers'), 12 | 13 | // TODO: (React|Preact|Inferno).render, snabbdom-patch, replaceWith 14 | createRender: require('react').Render 15 | }) 16 | 17 | // export configured 'mount' with our library choices 18 | module.exports = mount 19 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-redux-todomvc/README.md: -------------------------------------------------------------------------------- 1 | # Redux TodoMVC Example 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app), which provides a simple way to start React projects with no build configuration needed. 4 | 5 | Projects built with Create-React-App include support for ES6 syntax, as well as several unofficial / not-yet-final forms of Javascript syntax such as Class Properties and JSX. See the list of [language features and polyfills supported by Create-React-App](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#supported-language-features-and-polyfills) for more information. 6 | 7 | ## Available Scripts 8 | 9 | In the project directory, you can run: 10 | 11 | ### `npm start` 12 | 13 | Runs the app in the development mode.
14 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 15 | 16 | The page will reload if you make edits.
17 | You will also see any lint errors in the console. 18 | 19 | ### `npm run build` 20 | 21 | Builds the app for production to the `build` folder.
22 | It correctly bundles React in production mode and optimizes the build for the best performance. 23 | 24 | The build is minified and the filenames include the hashes.
25 | Your app is ready to be deployed! 26 | 27 | ### `npm run eject` 28 | 29 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 30 | 31 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 32 | 33 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 34 | 35 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 36 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-redux-todomvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "enzyme": "^3.9.0", 7 | "react-addons-test-utils": "^15.3.0", 8 | "react-scripts": "^5.0.1" 9 | }, 10 | "dependencies": { 11 | "classnames": "^2.3.3", 12 | "prop-types": "^15.8.1", 13 | "react": "^16.8.6", 14 | "react-dom": "^16.8.6", 15 | "react-hyperscript-helpers": "^2.0.0", 16 | "react-redux": "^7.2.9", 17 | "react-test-renderer": "^16.8.6", 18 | "redux": "^4.2.1", 19 | "todomvc-app-css": "^2.1.0" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "eject": "react-scripts eject", 25 | "test": "react-scripts test" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-redux-todomvc/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux TodoMVC Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-redux-todomvc/src/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes' 2 | 3 | export const addTodo = text => ({ type: types.ADD_TODO, text }) 4 | export const deleteTodo = id => ({ type: types.DELETE_TODO, id }) 5 | export const editTodo = (id, text) => ({ type: types.EDIT_TODO, id, text }) 6 | export const completeTodo = id => ({ type: types.COMPLETE_TODO, id }) 7 | export const completeAll = () => ({ type: types.COMPLETE_ALL }) 8 | export const clearCompleted = () => ({ type: types.CLEAR_COMPLETED }) 9 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-redux-todomvc/src/actions/index.spec.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes' 2 | import * as actions from './index' 3 | 4 | describe('todo actions', () => { 5 | it('addTodo should create ADD_TODO action', () => { 6 | expect(actions.addTodo('Use Redux')).toEqual({ 7 | type: types.ADD_TODO, 8 | text: 'Use Redux' 9 | }) 10 | }) 11 | 12 | it('deleteTodo should create DELETE_TODO action', () => { 13 | expect(actions.deleteTodo(1)).toEqual({ 14 | type: types.DELETE_TODO, 15 | id: 1 16 | }) 17 | }) 18 | 19 | it('editTodo should create EDIT_TODO action', () => { 20 | expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({ 21 | type: types.EDIT_TODO, 22 | id: 1, 23 | text: 'Use Redux everywhere' 24 | }) 25 | }) 26 | 27 | it('completeTodo should create COMPLETE_TODO action', () => { 28 | expect(actions.completeTodo(1)).toEqual({ 29 | type: types.COMPLETE_TODO, 30 | id: 1 31 | }) 32 | }) 33 | 34 | it('completeAll should create COMPLETE_ALL action', () => { 35 | expect(actions.completeAll()).toEqual({ 36 | type: types.COMPLETE_ALL 37 | }) 38 | }) 39 | 40 | it('clearCompleted should create CLEAR_COMPLETED action', () => { 41 | expect(actions.clearCompleted()).toEqual({ 42 | type: types.CLEAR_COMPLETED 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-redux-todomvc/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters' 5 | 6 | const FILTER_TITLES = { 7 | [SHOW_ALL]: 'All', 8 | [SHOW_ACTIVE]: 'Active', 9 | [SHOW_COMPLETED]: 'Completed' 10 | } 11 | 12 | export default class Footer extends Component { 13 | static propTypes = { 14 | completedCount: PropTypes.number.isRequired, 15 | activeCount: PropTypes.number.isRequired, 16 | filter: PropTypes.string.isRequired, 17 | onClearCompleted: PropTypes.func.isRequired, 18 | onShow: PropTypes.func.isRequired 19 | } 20 | 21 | renderTodoCount() { 22 | const { activeCount } = this.props 23 | const itemWord = activeCount === 1 ? 'item' : 'items' 24 | 25 | return ( 26 | 27 | {activeCount || 'No'} {itemWord} left 28 | 29 | ) 30 | } 31 | 32 | renderFilterLink(filter) { 33 | const title = FILTER_TITLES[filter] 34 | const { filter: selectedFilter, onShow } = this.props 35 | 36 | return ( 37 | onShow(filter)}> 40 | {title} 41 | 42 | ) 43 | } 44 | 45 | renderClearButton() { 46 | const { completedCount, onClearCompleted } = this.props 47 | if (completedCount > 0) { 48 | return ( 49 | 53 | ) 54 | } 55 | } 56 | 57 | render() { 58 | return ( 59 |
60 | {this.renderTodoCount()} 61 |
    62 | {[ SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED ].map(filter => 63 |
  • 64 | {this.renderFilterLink(filter)} 65 |
  • 66 | )} 67 |
68 | {this.renderClearButton()} 69 |
70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/interframeworkability/react-redux-todomvc/src/components/Footer.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRenderer } from 'react-test-renderer/shallow'; 3 | import Footer from './Footer' 4 | import { SHOW_ALL, SHOW_ACTIVE } from '../constants/TodoFilters' 5 | 6 | const setup = propOverrides => { 7 | const props = Object.assign({ 8 | completedCount: 0, 9 | activeCount: 0, 10 | filter: SHOW_ALL, 11 | onClearCompleted: jest.fn(), 12 | onShow: jest.fn() 13 | }, propOverrides) 14 | 15 | const renderer = createRenderer() 16 | renderer.render(