├── .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 `