Skip to content

Commit efed401

Browse files
authored
Feature/persistent state and Feature/nested-components (#16)
* added a persistent storage add-on: - called "storage" - uses localStorage, works in browser only for now - tested and working with Counter and Todo apps so far - TODO: add NodeJS support, do more testing, and then code cleanups * fix/nested-components (#17) - fix in `html` - register the right event handlers with the right elems: Create a `html.funcs` as well as a `html.i`, and use those as references to the functions to be inserted. This fixes Component so that the new stuff in examples/recipes.js works OK. NOTE: `html` will detect a statful Component, and will only retrieve its view, **not** run its rull setState()/render() lifecycle. This has a number of implications: - better performance - parent component holds the only state - parent component inly triggers re-renders - nested stateful components have a undefined `.container` - therefore calling setState() of child components does nothing * updated examples * updated README
1 parent a589b60 commit efed401

20 files changed

+430
-24
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# local storage dir, used by examples
2+
scratch/
3+
14
# Logs
25
logs
36
*.log

README.md

Lines changed: 202 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ A "state" is a snapshot of your application data at a specific time.
3636
- `validator`: validate states against a schema (like a simple PropTypes)
3737
- `html`/`htmel`: simpler, more powerful Template Literals (like a simple JSX)
3838
- `emitter`: an event emitter, for sharing updates between components
39+
- `storage`: enables persistent states (between page refreshes, etc)
3940
- `tweenState`: animate from one state to the next
4041
- Supports **"middleware"** functions:
4142
- easily customise a components setState and re-render behaviour
@@ -47,7 +48,7 @@ A "state" is a snapshot of your application data at a specific time.
4748
- a log of all state history can be kept, for debugging (_optional_):
4849
- rewind or fast-forward to any point in the state history
4950
- save/load current or any previous state as "snapshots"
50-
- Simple, stateless "child" components
51+
- Nested components
5152
- ...and more
5253

5354

@@ -93,7 +94,6 @@ Todo.view = props => htmel
9394
Todo.render('.container')
9495
```
9596

96-
9797
### A *re-usable* HTML component:
9898

9999
Unlike the previous two examples, this one below is a function which generates re-usable components - a new component is created from the given definition (state, view, etc) each time it's called.
@@ -129,6 +129,48 @@ header1({ title: "Hello a 3rd time!" });
129129

130130
```
131131

132+
### Nested components
133+
134+
Child components should be regular functions that return part of the view of the parent component:
135+
136+
```js
137+
const Foo = new Component({ title: "Hey!", list: [ "one", "two" ] });
138+
139+
const Header = txt => `<h2>${txt}</h2>`
140+
const List = i => `<ul>${i.map(item => `<li>${i}</li>`).join('')}</ul>`
141+
142+
Foo.view = props =>
143+
`<div id="myapp">
144+
${Header(props.title)}
145+
${List(props.items)}
146+
</div>`
147+
```
148+
149+
But you can also nest proper (stateful) components inside other components, too:
150+
151+
```js
152+
// create 3 buttons from a re-usable component
153+
const btn1 = new Button({ txt: "1", fn: e => alert("btn1") });
154+
const btn2 = new Button({ txt: "2", fn: e => alert("btn2") });
155+
const btn3 = new Button({ txt: "3", fn: e => alert("btn3") });
156+
157+
// create the main (parent) component
158+
const Menu = new Component({ txt: 'Click the buttons!' });
159+
160+
// create a view with our buttons included:
161+
Menu.view = props => htmel`
162+
<div>
163+
<h2>${props.txt}</h2>
164+
${btn1}
165+
${btn2}
166+
${btn3}
167+
</div>
168+
`;
169+
170+
// add our main/parent component to page
171+
Menu.render('.container');
172+
```
173+
132174
See more short recipes in [examples/recipes.js](examples/recipes.js).
133175

134176
## Installation
@@ -541,6 +583,76 @@ foo.html.addEventListener("click", e => {
541583
});
542584
```
543585

586+
### Using the `storage` module
587+
588+
Use the storage module to make your components remember their state between page refreshes and sessions, using `localStorage`.
589+
590+
Note that `storage` can be polyfilled for NodeJS, so will work in Node too - by saving to JSON files.
591+
592+
In NodeJS, the state persists between script invocations, rather than page refreshes.
593+
594+
To use the `storage` add-on, include it in your project like so:
595+
596+
#### In browsers:
597+
598+
```html
599+
<script src="https://unpkg.com/@scottjarvis/component"></script>
600+
<script src="https://unpkg.com/@scottjarvis/component/dist/storage.min.js"></script>
601+
<script>
602+
Component.storage = storage
603+
604+
// use it here
605+
</script>
606+
```
607+
608+
#### In NodeJS:
609+
610+
```js
611+
var { Component, storage } = require('@scottjarvis/component');
612+
Component.storage = storage
613+
614+
// use it here
615+
616+
```
617+
618+
To enable persistent storage for a component, just define a **store name** (where to save your data) as `myComponent.store = "something"`.
619+
620+
**In a browser**, this is how you add persistent storage to our Counter app:
621+
622+
```js
623+
const Counter = new Component({ count: 1 });
624+
625+
const add = num => Counter({ count: Counter.state.count + num })
626+
627+
Counter.view = props => htmel`
628+
<div>
629+
<h1>Counter: ${props.count}</h1>
630+
<button onclick="${e => add(+1)}"> + </button>
631+
<button onclick="${e => add(-1)}"> - </button>
632+
</div>`;
633+
634+
// simply define a "store name" (where to save your data) before you render
635+
Counter.store = 'Counter';
636+
637+
// now we can render it into the page - this will load in the persistent state
638+
// from its store as the initial state for the component
639+
Counter.render('.container')
640+
```
641+
642+
**In NodeJS**, this is how you add persistent storage to our Counter app:
643+
644+
- install the NodeJS localStorage polyfill: `npm i node-localstorage -D`
645+
- run your scripts with the `-r node-localstorage/register` option to enable it
646+
- the "local storage" used will be a local folder/file, called `./scratch/<store name>`
647+
648+
Example command, running a component with a persistent state in NodeJS:
649+
650+
```
651+
node -r node-localstorage/register examples/usage-persistant-state.js
652+
```
653+
654+
See [examples/usage-persistant-state.js](examples/usage-persistant-state.js) for more info.
655+
544656
### Using the `tweenState` module
545657

546658
With `tweenState` it's super easy to do animations that use `requestAnimationFrame` and DOM diffing.
@@ -819,6 +931,94 @@ Adding linked data to your components is easy - just define it as part of your v
819931
- use the `props` passed in to define/update whatever you need
820932
- your JSON-LD will be updated along with your view, whenever your component re-renders
821933

934+
### Nested components
935+
936+
Components that are nested inside other components are called _child components_.
937+
938+
There are two kinds of child component - _stateless_ and _stateful_ - and while they behave the same in most ways, they have slightly difference syntax and features.
939+
940+
All child components have the following in common:
941+
- you include the child component in the "view" of the parent component
942+
- child components do not trigger a re-render of the page
943+
- to re-render a child component that has changed, you must update the parent component
944+
- nested components work with or without the `html`/`htmel` add-on(s)
945+
946+
**About "stateless" child components:**
947+
948+
Stateless components are just _regular functions_ that take `props` as input, and return a view - usually HTML as a string.
949+
950+
```js
951+
// a stateless child component is just a function that receives `props`, and returns a view
952+
const h2 = text => `<h2>${text}</h2>`;
953+
954+
// ...used inside the view of another component:
955+
Foo.view = props => `
956+
<div>
957+
${h2(props.txt)}
958+
<p> ... </p>
959+
</div>
960+
`;
961+
```
962+
963+
**About stateful child components**
964+
965+
Stateful components are _any components with a state_, usually created like so:
966+
967+
```js
968+
const Foo = new Component({ ...someData });
969+
```
970+
971+
NOTE: When nested inside another component, even stateful components _do not_ run `setState()` & `render()` - they simply return their view, just like stateless child components.
972+
973+
This has a number of implications:
974+
975+
- better performance (fewer page re-renders)
976+
- enforces similar behaviour to stateless child components
977+
- only parent components trigger page re-renders
978+
- nested components have an undefined `.container` property
979+
- therefore calling the `render()` method of a child component (usually) does nothing
980+
- calling `setState()` of a child component _will_ update its state and run its "middleware", but _doesn't_ re-render
981+
982+
```js
983+
// let's create a re-usable, stateful button component:
984+
// the `htmel` add-on is used as we're attaching Event Listeners to the buttons
985+
986+
function Button(state) {
987+
const Button = new Component({ ...state });
988+
Button.view = props => htmel`<button onclick="${props.fn}">${props.txt}</button>`;
989+
return Button;
990+
}
991+
992+
// create 3 buttons from our re-usable component
993+
const btn1 = new Button({ txt: "1", fn: e => console.log("btn1 'click' Event: ", e) });
994+
const btn2 = new Button({ txt: "2", fn: e => console.log("btn2 'click' Event: ", e) });
995+
const btn3 = new Button({ txt: "3", fn: e => console.log("btn3 'click' Event: ", e) });
996+
997+
// create the main (parent) component
998+
const Menu = new Component({ txt: 'Click the buttons!' });
999+
1000+
// create a view with our buttons included:
1001+
Menu.view = props => htmel`
1002+
<div>
1003+
<h2>${props.txt}</h2>
1004+
${btn1}
1005+
${btn2}
1006+
${btn3}
1007+
</div>
1008+
`;
1009+
1010+
// add our main/parent component to page
1011+
Menu.render('.container');
1012+
1013+
// we can update the state of the main component, it will re-render
1014+
// what is needed - each child component returns their view based on
1015+
// their own state
1016+
Menu.setState({ txt: "3 Buttons:"})
1017+
1018+
// to change the text of the buttons, you must update their state,
1019+
// then update the state of Menu, to trigger a re-render on the page...
1020+
```
1021+
8221022
### Server side rendering
8231023

8241024
If running a NodeJS server, you can render the components as HTML strings or JSON.

dist/component.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/component.min.js.gz

112 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)