@perbyhring/fhtml
v0.1.6
Published
My personal template literal-based reactive ui-framework for small web apps
Downloads
33
Readme
f=>html
This is my personal reactive ui-library for small web-apps. I use it to create interactive graphics and stuff like that.
It's pretty small. 4.7kb gzipped.
Documentation / Disclaimer
This library is not properly documented. And probably won't be. Ever. You should consider this library nothing more than a little experiment from a random person on the internet.
I mean, I use it. But I also made it. So I also know when not to use it. You don't, so perhaps you should use something else.
With that out of the way, here's how you use it XD
Import
import html from '@perbyhring/fhtml'
I like to import the libraray as html
because then I can use the lit-html syntax-highlighter in vscode to make my templates look <🐱>purrrrty</🐱>
.
The basic idea
The basic idea of f=>html
is that everything inserted as a function in the template-literal can be re-rendererd.
Basic example:
let greeting = 'Hello'
let name = 'World'
const app = html`
<div>
${greeting} ${f => name}!
</div>
`
.mount(placeholderElement)
greeting = 'Have a greeeeat day'
name = 'Buddy McGee!'
app.render()
Result:
Hello Buddy McGee!
😲 -The fuuu!!? Why didn't Buddy McGee recieve our message to Have a greeeeat day
? We did everything right. ...right? ... right!!?
The answer is: -YES!
Kidding, the answer is no
. Definitely no
.
Because, this is what happened when we called app.render()
:
greeting
was not updated in the DOM, because it's value was inserted directly (${greeting}
).name
was updated, because it was inserted as the returned value of a function (${f => name}
).
Data-model
If you don't want to call app.render()
every time you change the data, you can use the provided data-model.
Every component can contain three different types of data:
- State
- Data
- Props
See the examples below for how they are used.
Examples
Attributes
Prefix dynamic attributes with a :
.
If you don't do this you sometimes might get errors in your console. Which is not cool, according to the cool patrol.
Svg-elements sometimes contains attributes which are case-sensitive. These must be written like this:
baseFrequency
:base^frequency
xChannelSelector
:x^channel^selector
You have to do it this way because I tell you to. Also because this library does not use a virtual-dom implementation. Therefore the browser automatically translates all uppercase-attributes to lowercase. I could write more letters to explain this problem but it's pretty boring, so I won't.
Attribute example:
html`
<div class="app" :title=${f => f.data.message()}>
${f => f.data.message()}
</div>
`
.data({
message: 'Hello fhtml!'
})
.mount(document.querySelector('#app'))
Properties
Properties are prefixed with a .
.
A big heads-up here is that textContent
could also be written as textcontent
. The library will try to find the correct property automatically. If you for some reason have a dom-node which also contains a lowercase textcontent
-property, the results will be unpredictable. Which can be nice on valentines day or similar events where you're hungry for excitement, but not so nice in a ui-library. So just remember that.
Property example:
html`
<div class="app" .textContent=${f => f.data.message()}></div>
`
.data({
message: 'Hello fhtml!'
})
.mount(placeholder_element)
Directives
If you don't trust the library to make the right assumtion about which property to set, you could also set it through a directive.
Here's a simple little directive:
html`
<div
class="app" ${f =>
f.node.textContent = f.data.message()
}
></div>
`
.data({
message: 'Hello fhtml!'
})
.mount(placeholder_element)
Directives can also be used to run more complex and/or fun manipulations of a dom-node. For instance painting something to a canvas.
Canvas directive example:
html`
<canvas ${f => {
f.isStatic
const ctx = f.node.getContext('2d')
ctx.fillStyle = 'yellow'
ctx.beginPath()
ctx.rect(0,0,100,100)
ctx.fill()
}}></canvas>
`
By calling the special f.isStatic
-property you tell f=>html
to never re-run this function.
Events
Events are declared in a similar way to Vue's shorthand event-syntax, with the @
-prefix.
Event example:
const add = n1 => n2 => n1 + n2
const addOne = add(1)
html`
<div
class="app"
@click=${(f,event) =>
f.state.clicks(addOne)
}>
${f => f.data.message() }
${f => f.state.clicks().toString() }
</div>
`
.state({
clicks: 0
})
.data({
message: 'Number of clicks:'
})
.mount(placeholder_node)
Notice how you can make use of the functional pattern of the data model to create reusable manipulator-functions (f.state.clicks(addOne)
). Holy smokes, what a terrible sentence I just wrote. Sorry.
Also notice that the number of clicks are converted to a string with f.state.clicks().toString()
. This is because if f.state.clicks()
returned a value of 0
, it would be a falsey-value, which is an instruction for f=>html
to not render it.
Which leads us to the next chapter:
Conditionals
Conditionals can be written in a similar way to how I think it's done in React.
Conditional example:
html`
<div class="app">
${f => f.state.showMessage() && f.data.message()}
</div>
`
.state({
showMessage: true
})
.data({
message: 'Can you see me?'
})
.mount(document.querySelector('#app'))
Lifecycle events
There are four lifecycle-events you can use:
- beforemount
- mounted
- beforeunmount
- unmounted
These are written just like normal events, on the dom-nodes themselves.
Example:
const app = html`
<div
@beforemount=${f => console.log(f.node, 'before mount!)}
@mounted=${f => console.log(f.node, 'mounted!')}
@beforeunmount=${f => console.log(f.node, 'before unmount!)}
@unmounted=${f => console.log(f.node, 'unmounted!')}
></div>
`
.mount(placeholder_element)
setTimeout(app.unmount, 2000)
Input / forms
Inputs and other form-elements are bound in a similar way to other ui-libraries.
Input example:
html`
<div class="app">
${f => f.data.message()}
<input
type="text"
.value=${f => f.data.message()}
@input=${f => f.data.message(f.node.value)}
>
</div>
`
.data({
message: 'Hello fhtml!'
})
.mount(placeholder_element)
Classnames
Classnames can be written as a string or an object.
If you have a lot of conditional logic, objects are probably the way to go.
If you write your classnames as objects the property-name is the name of the class, and the property-value is a true
- or false
-value, which tells f=>html
whether to add the classname.
Classnames example:
html`
<div
:class=${f => ({
app: true,
'app--active': f.state.active()
})}
>
</div>
`
.state({
active: true
})
.mount(placeholder_element)
Styles
Styles can also be written as strings or objects.
The css transform
-property can contain nested values for each type of transform.
Example:
html`
<div
:style=${f => ({
width: '100px',
height: '100px',
backgroundColor: 'purple',
borderRadius: '10px'
})}
@mousemove=${(f,e) => {
f.state.x(e.x)
f.state.y(e.y)
}}
>
<div
:style=${f => ({
position:absolute,
top: 0,
left: 0,
width: '10px',
height: '10px',
background: 'orange',
transform: {
translateX: `${f.state.x()}px`,
translateY: `${f.state.y()}px`,
rotateX: `${f.state.x()}deg`,
rotateY: `${f.state.y()}deg`
}
})}
></div>
</div>
`
.state({
x: 0,
y: 0
})
.mount(placeholder_element)
Loopty-loop
This might look strange, but when you want to loop through your data, you need to write it like f.map.data.messages(component)
, instead of the normal javascript way of going array.map(function)
.
Example:
const message = () => html`
<li>${f => f.props.text()}</li>
`
html`
<div class="app">
<ul>
${f => f.map.data.messages(message)}
</ul>
</div>
`
.data({
messages: [
{ text: 'Hello fhtml!'},
{ text: 'How are you?'},
{ text: 'Fine, thanks!' }
]
})
.mount(document.querySelector('#app'))
OH SHIT, did I just create a component-ish looking thingy-ling!?
State vs. data vs. prop
State: State is like global data which is accessible from all nested child-components. You should only declare this on your outermost component.
Data: is only accessible from the component it is declared on.
Prop: Props are passed from a parent-component's state/data/prop, to a child-component.
Example of all three types of data:
const addOne = n => ++n
const message = () => html`
<li
@click=${f => {
f.state.clicks(addOne)
f.data.clicks(addOne)
}}
>
${f => f.props.text()} has been clicked
${f => f.data.clicks()} times.
</li>
`
.data({
clicks: 0
})
const app = html`
<div class="app">
<ul>
${f => f.map.data.messages(message)}
</ul>
All clicks: ${f => f.state.clicks().toString()}
</div>
`
.state({
clicks: 0
})
.data({
messages: [
{ text: 'Hello fhtml!'},
{ text: 'How are you?'},
{ text: 'Fine, thanks!' }
]
})
.mount(document.querySelector('#app'))
If you want to access or change the data outside of the application you do it like this:
const app = html`
<div>
${f => f.state.counter()}
</div>
`
.state({
counter: 1
})
setTimeout(() => {
// write
app.$state('counter', n => ++n)
console.log(
// read
app.$state('counter')
)
}, 1000)
To access data from outside the application you need to use a dot notation string. So for instance if you want to access a nested attribute it would look like this:
const firstPersonAge = app.$data('people.0.age')
You can subscribe to changes in the data from outside of the application like this:
app.$data().subscribe('people.0.age'. (newVal, oldVal) => {
console.log(`First person age updated from ${oldVal} to ${newVal}`)
})
You can also subscribe to changes in the data inside like this:
html`
<div class="app">
${f => f.map.data.people(() => html`
<div class="person">
Name:${f => f.props.name()}
<br>
Age: ${f => f.props.age()}
</div>
`)}
</div>
`
.data({
people: [
{
name: 'Geir',
age: 53
}
]
})
.watch({
data: {
'people.0.age'(newVal, oldVal) {
console.log(`First person age updated from ${oldVal} to ${newVal}`)
}
}
})
If you want to push something to an array you should do it like this:
// Add person from inside the application
const addPerson = f => {
f.data.people(people => [
...people,
{...f.state.newPerson()}
])
}
// Add person from outside of application
app.$data('people', people => [
...people,
{...newPerson}
])
If you did this...
f.data.people(people =>
people.push(f.state.newPerson())
)
... or this ...
f.data.people().push(f.state.newPerson())
...f=>html
wouldn't understand that the people-array had been updated. It's a pretty tedious excercise trying to explain exactly why you have to do it this way, so I won't attempt that right now :/
Examples of real stuff
I'll add some examples of real things I've built with this library soon. I think that'll maybe give a better overview of what types of projects it's appropriate for, and how the projects should be structured.