create-modulo
Version:
Starter projects for Modulo.html - Ready for all uses - Markdown-SSG / SSR / API-backed SPA
458 lines (368 loc) • 12.7 kB
HTML
<script src=../static/Modulo.html></script><script type=md>---
title: State - Component Parts
---
# State
The _State_ is for component instances to store changing data. This could
include anything from text a user entered into a form, data received from an
API, or data that represents UI flow changes, such as the visibility of a
modal.
By default (that is, with no `-store` attribute), state data is unique to every
component instance, and components can never directly access sibling or parent
data. It is possible to indirectly reference it, however, by passing state data
from a "parent" component to a "child" components within the parent by passing
it via a _Props_ attribute. In this case, the data should be considered
read-only to the child component, like any other _Props_ data.
## Example
See below for a quick example, showing off an example of each of the 6 types of data:
```
<State
color="red" (String)
count:=1 (Number)
loading:=false (Boolean)
company:=null (Null)
items:='[ "abc", "def" ]' (Array)
obj:='{ "a": "b", "c": "d" }' (Object)
></State>
```
# Definition
State is traditionally included in Component definitions below the _Template_
tag, but above the _Script_ tag. This makes sense because functions in the
_Script_ tag typically manipulate state in order to render new HTML in the
_Template_, making _Script_ a sort of mutable bridge between _Script_ and
_Template_. State is defined in a similar way to Props: Only defined with
properties, but no contents.
See below for an example of defining a _State_ Component Part:
## Example 1
Two State variables specified, of type String and Number:
```
<State
name="Luiz"
favenum:=13
></State>
```
## Example 2
Building up complicated JSON data with "." syntax:
```
<State
user:={}
user.name="gigasquid"
user.uid:=1313
user.address:={}
user.address.billable:=null
user.address.ready:=true
></State>
```
Note that all "state variables" _must_ have an initial value. It's okay to make
the initial value be `null` (as in the "billable" example above), or other some
placeholder that will later be replaced. Undefined state variables are treated
as errors.
# Stores
If you want to share data between components globally, such that any component
can modify the data causing a re-render to all linked components, such as user
log-in information or global state data, then you can use the powerful `-store`
attribute:
```
<State
-store="userinfo"
username="pombo"
tags:='["admin", "printing"]'
></State>
```
With this, any state with the given store _throughout your application_ will
share state and subscriptions to updates.
#### Limiting a store with -only
Sometimes, you'll only want to subscribe to certain attributes parts of a store:
```
<State
-store="userinfo"
-only:='["username"]'
username="pombo"
></State>
```
# Component Part properties
The actual data in a _State_ Component Part is stored on it's "data" property. This
property is a regular JavaScript Object, and thus can store any JavaScript data
type. As an example, in a _Script_ Component Part, you can directly reference this
property with the code `cparts.state.data`.
When writing the _State_ Component Part definition, you must declare or pre-define each
"state variable" or property of the "data" Object that you want to use. It is
not permitted to create new state variables later on. In other words, if you
only define `cparts.state.data` as having `.count` and `.title` as "state
variables" (aka properties of the "data" Object), then an attempt like
`cparts.state.data.newstuff = 10;` may result in an error. If you are dealing
with a situation where you have an unknown quantity of data, such as from an
API, the correct approach is to store it all within a nested Object _inside_
the state data Object, e.g. such as an `data.apiResults` Object or Array.
Unlike top-level "state variables", it's okay to add properties, modify, or
build upon nested Objects.
While it's allowed to assign any arbitrary reference as a _State_ variable,
including complex, unserializable types such as function callbacks, it's highly
recommended to try to keep it to primitive and serializable types as much as
possible (e.g. String, Number, Boolean, Array, Object). The reason being that
there may be future features or third-party add-ons for _State_ which will only
support primitive types (as an example, that would be required to save state to
localStorage). If you want to store functions, consider using a
`prepareCallback` to generate the functions within a Script context, and only
store the data needed to "generate" the function in the state (as opposed to
storing a direct reference to the function itself).
### renderObj
State contributes it's current data values to the renderObj. Examples:
* State initialized like: `<State name="Luiz">` will be accessible on the
renderObj like `renderObj.state.name`, and in the Script or Template Component Parts
like `state.name`.
* State initialized like: `<State stuff:='["a", "b"]'>` will be accessible on
the renderObj like `renderObj.state.info` (with individual items accessed
with code that ends with "`.stuff[0]`"), and in the Script or Template Component Parts
like `state.info`.
### Directives
State provides a single directive:
* `state.bind` \- Two-way binding with State data, with the key determined by
the `name=` property of whatever it is attached to. You can attach a
`state.bind` directive to any HTML `<input>`, and the _State_ Component Part's
two-way binding will cause the input value to be updated if the state
variable ever changes, and if a user edits the input triggering a `"keyup"`
or `"change"` event, the state variable will be updated (along with,
typically, a re-render of the component).
# Syntax Examples
Examine below for how two different syntaxes can be used to construct data:
Either the JSON style all in one go, or the somewhat more verbose (but perhapse
easier to maintain) dataProp style:
```modulo
edit:demo=modulo
<Template>
{% if state_a|json is state_b|json %}
<strong style="color: green">MATCH</strong>
<pre>{{ state_a|json:2 }}</pre>
{% else %}
<strong style="color: red">NOT MATCH</strong>
<pre>{{ state_a|json }}</pre>
<pre style="color: red">{{ state_b|json }}</pre>
{% endif %}
</Template>
<State
-name="state_a"
count:=42
stuff:=null
articles:=[]
articles.0:={}
articles.1:={}
articles.2:={}
articles.0.headline="Modulo released!"
articles.1.headline="Can JS be fun again?"
articles.2.headline="MTL considered harmful"
articles.0.tease="The most exciting news of the century."
articles.2.tease="Why constructing JS is risky business."
></State>
<State
-name="state_b"
count:=42
stuff:=null
articles:='[
{"headline": "Modulo released!",
"tease": "The most exciting news of the century."},
{"headline": "Can JS be fun again?"},
{"headline": "MTL considered harmful",
"tease": "Why constructing JS is risky business."}
]'
></State>
```
# Binding Examples
* *Useful resource:* Read this for a full list of input types. With the
exception of some of the ones listed below, they will all be "String" in
terms of the State Component Part. [MDN input Element
documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)
### Example #1: Binding different input types
```modulo
edit:demo=modulo
<Template>
<h3>Customize</h3>
<!-- text (default) -->
<input state.bind name="subject" />
<!-- textarea -->
<textarea state.bind name="body"></textarea>
<!-- checkbox -->
<label>Underlined: <input state.bind name="underlined"
type="checkbox" min="50" max="500" step="10" /></label>
<!-- select -->
<label>Font
<select state.bind name="font">
<option value="serif">Serif</option>
<option value="sans-serif">Sans-Serif</option>
<option value="monospace">Monospace</option>
</select>
</label>
<!-- button element -->
<!-- (Note that the event has to be specified) -->
<label>RESET
<button state.bind.click name="subject" value="XXXXXXX">
SUBJ (XXXXXXX)
</button>
<button state.bind.click name="body" value="XXXXXXX">
BODY (XXXXXXX)
</button>
</label>
<!-- number -->
<label>Padding: <input state.bind name="paddingSize"
type="number" min="1" max="10" step="1" /></label>
<!-- range -->
<label>Width: <input state.bind name="contentWidth"
type="range" min="50" max="500" step="10" /></label>
<!-- color -->
<label>BG: <input state.bind name="accentColor"
type="color" /></label>
<h5 style="
{% if state.underlined %}
text-decoration: underline;
{% endif %}
font-family: {{ state.font }};
background: {{ state.accentColor }};
padding: {{ state.padding-size }}px;
width: {{ state.content-width }}px;
top: 0;
right: 0;
position: fixed;
">{{ state.subject }} - {{ state.body }}</h5>
</Template>
<State
subject="Testing message..."
body="Welcome to my blog"
underlined:=false
font="monospace"
padding-size:=5
content-width:=70
accent-color="#ffeeee"
></State>
<Style>
label {
display: block;
border: 1px solid black;
padding: 5px;
}
</Style>
```
### Example #2: Combining with filters
```modulo
edit:demo=modulo
<Template>
<div>
<label>Username:
<input state.bind name="username" /></label>
<label>Color ("green" or "blue"):
<input state.bind name="color" /></label>
<label>Opacity: <input state.bind
name="opacity"
type="number" min="0" max="10" step="1" /></label>
<h5 style="
opacity: {{ state.opacity|multiply:'0.1' }};
color: {{ state.color|allow:'green,blue'|default:'red' }};
">
{{ state.username|lower }}
</h5>
</div>
</Template>
<State
opacity:=5
color="blue"
username="Testing_Username"
></State>
```
### Example #3: Specifying events
```modulo
edit:demo=modulo
<Template>
<p>Default (.change):</p>
<input state.bind
name="c" type="range"
min="1" max="10" step="1" />
<p>Smooth (.input):</p>
<input state.bind.input
name="c" type="range"
min="1" max="10" step="1" />
<tt style="{% if state.c gt 7 %}color: green{% endif %}">
{{ state.c }}
</tt>
</Template>
<State
c:=5
></State>
<Style>
:host {
display: grid;
grid-template-columns: 90px 90px;
}
</Style>
```
# Store Examples
### Example #1: Store for simple state sharing
```modulo
edit: demo=modulo_component name="SharedState" usage="USAGE:"
<Template>
<input state.bind name="a" />
<input state.bind name="b" />
<input state.bind name="c" type="range"
min="1" max="10" step="1" />
<tt style="{% if state.c gt 7 %}color: green{% endif %}">
{{ state.c }}
</tt>
</Template>
<State
-store="my_global_info"
a="A b c"
b="do re me"
c:=5
></State>
<Style>
:host {
display: grid;
grid-template-columns: 60px 60px 60px 20px;
}
</Style>
USAGE:
<x-SharedState></x-SharedState>
<hr />
<x-SharedState></x-SharedState>
<hr />
<x-SharedState></x-SharedState>
```
### Example #2: Multiple States and bound buttons
Here we have an incomplete "chat" component with two State Component Parts. This one
shares state between instances of it. Note that "msg" is not shared (neither is
_Props_), but "messages" is shared.
```modulo
edit: demo=modulo_component name="StoreChat" usage="USAGE:"
<Props
username
></Props>
<Template>
{% for m in chat.messages %}
<em>{{ m.name }}</em><strong>{{ m.text }}</strong>
{% endfor %}
<input state.bind name="msg" />
<button
on.click=chat.messages.push
chat.bind.click="messages"
payload:='{
"text": "{{ state.msg|escapejs }}",
"name": "{{ props.username|escapejs }}"
}'
>SEND</button>
</Template>
<State
msg=""
></State>
<State
-name="chat"
-store="chat"
messages:=[]
></State>
<Style>
:host {
display: grid;
grid-template-columns: 100px 100px;
}
</Style>
USAGE:
<x-StoreChat username="ALICE"></x-StoreChat>
<hr />
<x-StoreChat username="BOB"></x-StoreChat>
```