Appearance
States
What is a State?
In Paintor, State is an object, whose values are bound to chosen parts of the web page. This means that when you update the values of the State, the corresponding parts of the page are being automatically updated.
To create a State, use the function state()
. It takes an Array or an Object and returns a Proxy of it.
Object <--> State
This makes both, the original Object and the State bound together. When the values of one change, the corresponding values of the other also change.
WARNING
state()
is useless when generating HTML code, because the code is a string and is not reactive.
html
<script type="module">
import { state } from 'paintor'
/* Create a State from an Object */
const myObject = { count: 0 }
const myState = state(myObject)
/* Increment myState.count on every second */
setInterval(() => {
myState.count += 1
console.log(`myObject.count: ${myObject.count} | myState.count: ${myState.count}`)
}, 1000)
</script>
html
<script type="module">
import { state } from 'paintor'
/* Create a State from an Object */
const myObject = { count: 0 }
const myState = state(myObject)
/* Increment myObject.count on every second */
setInterval(() => {
myObject.count += 1
console.log(`myObject.count: ${myObject.count} | myState.count: ${myState.count}`)
}, 1000)
</script>
If we run either of these for 3 seconds, the output will be:
bash
myObject.count: 1 | myState.count: 1
myObject.count: 2 | myState.count: 2
myObject.count: 3 | myState.count: 3
State <--> DOM
Now that we have a State, we can use it in a Template to bind the State's values with the DOM.
But first, in order to achieve reactivity, here are few IMPORTANT things to remember:
- Although the State and the original Object are bound together, in the Template you should work with the State. For example,
myState.value
will be reactive, butmyObject.value
will not. - Where you want to reactively get a value from the State, wrap it in a callback function. For example, use
() => myState.value
instead ofmyState.value
.
Change the DOM on State changes
Let's have two buttons, -
and +
, and a <span> element between them. Clicking on either button going to change myState.value
, which is bound with the text content of the <span> element.
js
import { component, state } from 'paintor'
component((x) => {
const myState = state({ value: 0 })
const increment = () => myState.value += 1
const decrement = () => myState.value -= 1
x.div(
x.button({ onClick: decrement }, '-'),
x.span(() => myState.value), // The callback here is needed for reactivity
x.button({ onClick: increment }, '+')
)
}).paint('#states-1')
css
#states-1 {
& span {
padding: 0 0.5rem;
}
}
html
<div id="states-1"></div>
Click on the -
and +
buttons below to try it out:
Change the State on DOM changes
Let's have myState.text
and an input field from which myState.text
can be changed.
js
import { component, state } from 'paintor'
component((x) => {
const myState = state({ text: '' })
const setText = (event) => {
myState.text = event.target.value
}
x.div(
x.input({
type: 'text',
placeholder: 'Type something here',
onKeyUp: setText
}),
x.span(() => myState.text), // The callback here is needed for reactivity
)
}).paint('#states-2')
html
<div id="states-2"></div>
Type something in the input field below. You should see the same text that you are typing on the right side of the input field, where the <span> is.
exampleScope
Local State
Each template can have its own local (internal) state:
js
import { component, state, template } from 'paintor'
const plusMinus = template((x) => {
const localState = state({ value: 0 })
x.div(
x.button({ onClick: () => localState.value -= 1 }, '-'),
x.span(() => localState.value),
x.button({ onClick: () => localState.value += 1 }, '+')
)
})
component((x) => {
x.div('Template 1', plusMinus)
x.div('Template 2', plusMinus)
}).paint('#states-scope-local')
css
#states-scope-local {
& span {
padding: 0 0.5rem;
}
}
html
<div id="states-scope-local"></div>
Global State
Or, the template can use states from upper scope. In this case, multiple templates can use the same state.
js
import { component, state, template } from 'paintor'
const globalState = state({ value: 0 })
const plusMinus = template((x) => {
x.div(
x.button({ onClick: () => globalState.value -= 1 }, '-'),
x.span(() => globalState.value),
x.button({ onClick: () => globalState.value += 1 }, '+')
)
})
component((x) => {
x.div('Template 1', plusMinus)
x.div('Template 2', plusMinus)
}).paint('#states-scope-global')
css
#states-scope-global {
& span {
padding: 0 0.5rem;
}
}
html
<div id="states-scope-global"></div>
One State in Many Templates
One State can serve multiple Templates at the same time.
js
import { component, state, template } from 'paintor'
const globalState = state({ tick: 0 })
setInterval(() => {
globalState.tick += 1
}, 1000)
const tick = () => globalState.tick
const button = template((x) => {
x.button({ textContent: tick })
})
const paragraph = template((x) => {
x.p(tick)
})
const textarea = template((x) => {
x.textarea({ value: tick })
})
component(
button,
paragraph,
textarea
).paint('#states-one-in-many')
html
<div id="states-one-in-many"></div>
Many States in One Template
js
import { component, state, template } from 'paintor'
const stateOne = state({ number: 0 })
const stateTwo = state({ number: 0 })
setInterval(() => {
stateOne.number += 1
stateTwo.number -= 1
}, 1000)
const myTemplate = template((x) => {
x.p(() => stateOne.number )
x.p(() => stateTwo.number )
})
component(myTemplate).paint('#states-many-in-one')
html
<div id="states-many-in-one"></div>
State from Array
js
import { component, state } from 'paintor'
const globalState = state([ 0, 0 ])
setInterval(() => {
globalState[0] += 1
globalState[1] -= 1
}, 2000)
component((x) => {
x.p(() => globalState[0] )
x.p(() => globalState[1] )
}).paint('#states-array')
html
<div id="states-array"></div>