UNPKG

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
<!DOCTYPE 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> ```