Use the render event to restore focus and caret position
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example03/README.md:
--------------------------------------------------------------------------------
1 | # innerself example 03
2 |
3 | An example illustrating how much hand-holding innerself needs to handle
4 | form-based elements. Because the assignment to innerHTML completely re-creates
5 | the entire DOM tree under the root, all local state of the descendant elements
6 | is lost upop re-render. Focus and selection need to be restored manually.
7 |
8 | To install the dependencies and serve the example locally run:
9 |
10 | $ npm install
11 | $ npm start
12 |
--------------------------------------------------------------------------------
/example01/ActiveList.js:
--------------------------------------------------------------------------------
1 | import html from "../index";
2 | import { connect } from "./store";
3 | import ActiveTask from "./ActiveTask";
4 | import TaskInput from "./TaskInput";
5 |
6 | function ActiveList(state) {
7 | const { tasks } = state;
8 | return html`
9 |
A tiny view + state management solution using innerHTML.
12 |
13 |
14 | [`innerHTML` is fast][quirksmode]. It's not fast enough if you're a Fortune 500 company
15 | or even if your app has more than just a handful of views. But it might be
16 | just fast enough for you if you care about code size.
17 |
18 | I wrote _innerself_ because I needed to make sense of the UI for a game I wrote
19 | for the [js13kGames][] jam. The whole game had to fit into 13KB. I needed
20 | something extremely small which would not make me lose sanity. _innerself_
21 | clocks in at under 50 lines of code. That's around 600 bytes minified, ~350
22 | gzipped.
23 |
24 | _innerself_ is inspired by React and Redux. It offers the following familiar
25 | concepts:
26 |
27 | - composable components,
28 | - a single store,
29 | - a `dispatch` function,
30 | - reducers,
31 | - and even an optional logging middleware for debugging!
32 |
33 | It does all of this by serializing your component tree to a string and
34 | assigning it to `innerHTML` of a root element. It even imitates Virtual DOM
35 | diffing by comparing last known output of components with the new one :)
36 | I know this sounds like I'm crazy but it actually works quite nice for small
37 | and simple UIs.
38 |
39 | If you don't care about size constraints, _innerself_ might not be for you.
40 | Real frameworks like React have much more to offer, don’t sacrifice safety,
41 | accessibility, nor performance, and you probably won’t notice their size
42 | footprint.
43 |
44 | _innerself_ was a fun weekend project for me. Let me know what you think!
45 |
46 | [Live demo]: https://stasm.github.io/innerself/example01/
47 | [quirksmode]: https://www.quirksmode.org/dom/innerhtml.html
48 | [js13kGames]: http://js13kgames.com/
49 |
50 |
51 | ## Caveats
52 |
53 | You need to know a few things before you jump right in. _innerself_ is
54 | a less-than-serious pet project and I don't recommend using it in production.
55 |
56 | It's a poor choice for form-heavy UIs. It tries to avoid unnecessary
57 | re-renders, but they still happen if the DOM needs even a tiniest update. Your
58 | form elements will keep losing focus because every re-render is essentially
59 | a new assignment to the root element's `innerHTML`.
60 |
61 | When dealing with user input in serious scenarios, any use of `innerHTML`
62 | requires sanitization. _innerself_ doesn't do anything to protect you or your
63 | users from XSS attacks. If you allow keyboard input or display data fetched
64 | from a database, please take special care to secure your app. The
65 | `innerself/sanitize` module provides a rudimentary sanitization function.
66 |
67 | Perhaps the best use-case for _innerself_ are simple mouse-only UIs with no
68 | keyboard input at all :)
69 |
70 |
71 | ## Showcase
72 |
73 | - [A moment lost in time.][moment-lost] - a first-person exploration puzzle
74 | game by [@michalbe][] and myself. I originally wrote _innerself_ for this.
75 | - [Innerself Hacker News Clone][innerself-hn] - a Hacker News single page app by [@bsouthga][] with
76 | _innerself_ as the only dependency. Also serves as an example of a [TypeScript][typescript] _innerself_ app.
77 | - [Reach/Steal Draft Tracker][reach-steal] - a fantasy football draft tracker by [@bcruddy][] that tests the rendering performance with 300+ table rows backed by an [expressjs][] server.
78 | - [TodoMVC][todomvc-innerself] - a [TodoMVC][todomvc] app based on _innerself_ by [@Cweili][@cweili].
79 |
80 |
81 | [moment-lost]: https://github.com/piesku/moment-lost
82 | [@michalbe]: https://github.com/michalbe
83 | [innerself-hn]: https://github.com/bsouthga/innerself-hn
84 | [@bsouthga]: https://github.com/bsouthga
85 | [typescript]: https://github.com/Microsoft/TypeScript
86 | [reach-steal]: https://github.com/bcruddy/reach-steal
87 | [@bcruddy]: https://github.com/bcruddy
88 | [expressjs]: https://github.com/expressjs/express
89 | [todomvc-innerself]: https://codepen.io/Cweili/pen/ZXOeQa
90 | [todomvc]: http://todomvc.com/
91 | [@cweili]: https://github.com/Cweili
92 |
93 |
94 | ## Install
95 |
96 | $ npm install innerself
97 |
98 | For a more structured approach [@bsouthga][] created [innerself-app][]. Use it
99 | to bootstrap new _innerself_ apps from a predefined template.
100 |
101 | [innerself-app]: https://github.com/bsouthga/innerself-app
102 |
103 |
104 | ## Usage
105 |
106 | _innerself_ expects you to build a serialized version of your DOM which will
107 | then be assigned to `innerHTML` of a root element. The `html` helper allows
108 | you to easily interpolate Arrays.
109 |
110 | ```javascript
111 | import html from "innerself";
112 | import ActiveTask from "./ActiveTask";
113 |
114 | export default function ActiveList(tasks) {
115 | return html`
116 |
My Active Tasks
117 |
118 | ${tasks.map(ActiveTask)}
119 |
120 | `;
121 | }
122 | ```
123 |
124 | The state of your app lives in a store, which you create by passing the reducer
125 | function to `createStore`:
126 |
127 | ```javascript
128 | const { attach, connect, dispatch } = createStore(reducer);
129 | window.dispatch = dispatch;
130 | export { attach, connect };
131 | ```
132 |
133 | You need to make `dispatch` available globally in one way or another. You can
134 | rename it, namespace it or put it on a DOM Element. The reason why it needs to
135 | be global is that the entire structure of your app must be serializable to
136 | string at all times. This includes event handlers, too.
137 |
138 | ```javascript
139 | import html from "innerself";
140 |
141 | export default function ActiveTask(text, index) {
142 | return html`
143 |
144 | ${text} ${index}
145 |
148 |
149 | `;
150 | }
151 | ```
152 |
153 | You can put any JavaScript into the `on` attributes. [The browser will
154 | wrap it in a function][mdn-event] which takes the `event` as the first argument
155 | (in most cases) and in which `this` refers to the DOM Element on which the
156 | event has been registered.
157 |
158 | [mdn-event]: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Event_handlers#Event_handler's_parameters_this_binding_and_the_return_value
159 |
160 | The `dispatch` function takes an action name and a variable number of
161 | arguments. They are passed to the reducer which should return a new version of
162 | the state.
163 |
164 | ```javascript
165 | const init = {
166 | tasks: [],
167 | archive: []
168 | };
169 |
170 | export default function reducer(state = init, action, args) {
171 | switch (action) {
172 | case "ADD_TASK": {
173 | const {tasks} = state;
174 | const [value] = args;
175 | return Object.assign({}, state, {
176 | tasks: [...tasks, value],
177 | });
178 | }
179 | case "COMPLETE_TASK": {
180 | const {tasks, archive} = state;
181 | const [index] = args;
182 | const task = tasks[index];
183 | return Object.assign({}, state, {
184 | tasks: [
185 | ...tasks.slice(0, index),
186 | ...tasks.slice(index + 1)
187 | ],
188 | archive: [...archive, task]
189 | });
190 | }
191 | default:
192 | return state;
193 | }
194 | }
195 | ```
196 |
197 | If you need side-effects, you have three choices:
198 |
199 | - Put them right in the `on` attributes.
200 | - Expose global action creators.
201 | - Put them in the reducer. (This is considered a bad practice in Redux
202 | because it makes the reducer unpredictable and harder to test.)
203 |
204 | The `dispatch` function will also re-render the entire top-level component if
205 | the state changes require it. In order to be able to do so, it needs to know
206 | where in the DOM to put the `innerHTML` the top-level component generated.
207 | This is what `attach` returned by `createStore` is for:
208 |
209 | ```javascript
210 | import { attach } from "./store";
211 | import App from "./App";
212 |
213 | attach(App, document.querySelector("#root"));
214 | ```
215 |
216 | `createStore` also returns a `connect` function. Use it to avoid passing data
217 | from top-level components down to its children where it makes sense. In the
218 | first snippet above, `ActiveList` receives a `tasks` argument which must be
219 | passed by the top-level component.
220 |
221 | Instead you can do this:
222 |
223 | ```javascript
224 | import html from "innerself";
225 | import { connect } from "./store";
226 | import ActiveTask from "./ActiveTask";
227 | import TaskInput from "./TaskInput";
228 |
229 | function ActiveList(state) {
230 | const { tasks } = state;
231 | return html`
232 |
My Active Tasks
233 |
234 | ${tasks.map(ActiveTask)}
235 |
236 | ${TaskInput()}
237 |
238 |
239 | `;
240 | }
241 |
242 | export default connect(ActiveList);
243 | ```
244 |
245 | You can then avoid passing the state explicitly in the top-level component:
246 |
247 | ```javascript
248 |
249 | import html from "innerself";
250 | import { connect } from "./store";
251 |
252 | import ActiveList from "./ActiveList";
253 | import ArchivedList from "./ArchivedList";
254 |
255 | export default function App(tasks) {
256 | return html`
257 | ${ActiveList()}
258 | ${ArchivedList()}
259 | `;
260 | }
261 | ```
262 |
263 | Connected components always receive the current state as their first argument,
264 | and then any other arguments passed explicitly by the parent.
265 |
266 |
267 | ## Logging Middleware
268 |
269 | _innerself_ comes with an optional helper middleware which prints state
270 | changes to the console. To use it, simply decorate your reducer with the
271 | default export of the `innerself/logger` module:
272 |
273 | ```javascript
274 | import { createStore } from "innerself";
275 | import withLogger from "innerself/logger";
276 | import reducer from "./reducer"
277 |
278 | const { attach, connect, dispatch } =
279 | createStore(withLogger(reducer));
280 | ```
281 |
282 |
283 | ## Crazy, huh?
284 |
285 | I know, I know. But it works! Check out the examples:
286 |
287 | - [example01][] - an obligatory Todo App.
288 | - [example02][] by @flynnham.
289 | - [example03][] illustrates limitations of _innerself_ when dealing with text
290 | inputs and how to work around them.
291 |
292 | [example01]: https://stasm.github.io/innerself/example01/
293 | [example02]: https://stasm.github.io/innerself/example02/
294 | [example03]: https://stasm.github.io/innerself/example03/
295 |
296 |
297 | ## How It Works
298 |
299 | The update cycle starts with the `dispatch` function which passes the action to
300 | the reducer and updates the state.
301 |
302 | When the state changes, the store [compares the entire string output][diff] of
303 | top-level components (the ones attached to a root element in the DOM) with the
304 | output they produced last. This means that most of the time, even a slightest
305 | change in output will re-render the entire root.
306 |
307 | It's possible to dispatch actions which change the state and don't trigger
308 | re-renders. For instance in `example01` the text input dispatches
309 | `CHANGE_INPUT` actions on `keyup` events. The current value of the input is
310 | then saved in the store. Crucially, this value is not used by the `TaskInput`
311 | component to populate the input element. The whole thing relies on the fact
312 | that the native HTML input element stores its own state when the user is typing
313 | into it.
314 |
315 | This limitation was fine for my use-case but it's worth pointing out that it
316 | badly hurts accessibility. Any change to the state which causes a re-render
317 | will make the currently focused element lose focus.
318 |
319 | React is of course much smarter: the Virtual DOM is a lightweight
320 | representation of the render tree and updates to components produce an actual
321 | diff. React maps the items in the Virtual DOM to the elements in the real DOM
322 | and is able to only update what has really changed, regardless of its position
323 | in the tree.
324 |
325 | Here's an interesting piece of trivia that I learned about while working on
326 | this project. React only re-renders components when their local state changes,
327 | as signaled by `this.setState()`. The fact that it also looks like components
328 | re-render when their props change derives from that as well. Something needs to
329 | pass those props in, after all, and this something is the parent component
330 | which first needs to decide to re-render itself.
331 |
332 | When you think about how you can `connect` components with _react-redux_ to
333 | avoid passing state to them from parents it becomes clear why behind the scenes
334 | it calls [`this.setState(dummyState)`][dummy] (which is an empty object) to
335 | trigger a re-render of the connected component :) It does this only when the
336 | sub-state as described by the selector (`mapStateToProps`) changes, which is
337 | easy to compute (and fast) if the reducers use immutability right. In the best
338 | case scenario it only needs to compare the identity of the sub-state to know
339 | that it's changed.
340 |
341 | [diff]: https://github.com/stasm/innerself/blob/7aa2e6857fd05cc7047dcd3bbdda6d3820b76f42/index.js#L20-L27
342 | [dummy]: https://github.com/reactjs/react-redux/blob/fd81f1812c2420aa72805b61f1d06754cb5bfb43/src/components/connectAdvanced.js#L218
343 |
--------------------------------------------------------------------------------