-
Notifications
You must be signed in to change notification settings - Fork 2
Components
Blop Components can be a Function or a Class.
Input = (attributes, children, node) => {
<label>
= attributes.label
<input name=attributes.name value=attributes.value
type=attributes.type || "text" />
</label>
}
LoginForm = (attributes, children, node) => {
<form>
<Input label='Username' name='username' value='' />
<Input label='Password' name='password' type='password' value='' />
</form>
}
In this example the Input
function is recognised by Blop as a Component because its name is capitalised. Behind the scene Blop will call the Input
function passing 3 parameters:
- The 1st parameter is an object which properties are the attributes of the pseudo HTML element.
- The 2nd parameter is an Array of the children of this element (empty if no children exist).
- The 3rd parameter is a Component instance. This is the Blop internal representation of the instance of your component.
Behind the scene, Blop will create an instance of a Component class for every Component function call. You can directly extends this Component class provided from the Blop runtime and use that instead of a function.
The only method that you need to provide is the render
method.
import { Component } from 'blop'
class MouseTracker extends Component {
def render() {
// you have direct access to attributes and children using `this`
{ text } = this.attributes
<div>
<p>'hello 'text''</p>
<p>JSON.stringify(this.pos)</p>
</div>
}
def mouseMove(e) {
// you can store state on your component.
// Blop doesn't know this change will require a re-render so you will need to do it manually.
this.pos = { x: e.x, y: e.y }
// refresh will trigger the re-render of this component (and its children)
this.refresh()
}
def onMount() {
this.mouseMoveHandler = (e) => this.mouseMove(e)
document.addEventListener('mousemove', this.mouseMoveHandler)
}
def onUnmount() {
document.removeEventListener('mousemove', this.mouseMoveHandler)
}
}
You can then simply instantiate the class using the same syntax than with the function version of a component:
<MouseMouseTracker text="world" />
As you can see, there is also 2 special methods: onMount and onUnmount which are both called when the component is first created and destroyed. Those are the 2 principal lifecyle hooks of Blop.
import { Component } from 'blop'
class MyComponent extends Component {
constructor(componentFunction, attributes, children, name) {
super(...arguments)
// only if you wish to redefine the constructor
}
render(attributes, children, this) {
// has to be defined by the user
}
refresh() // schedule a re-render of this component, and its children
onMount() {
// empty function, meant to be redefined if needed
}
onUnmount() {
// empty function, meant to be redefined if needed
}
useState(name, initialValue) // state hook
useContext(name, initialValue) // context hook
onChange(attribute, callback) // call the callback when an attribute value has changed
}
If you need a component to retain a value you can store it globally by passing the data through an attribute.
If the state is not meant to be stored long term and you don't mind losing the state when the component is destroyed then you can use the setState
function hook:
Signature: node.useState(name: string, initial: any): { value: any, setState: Function }
Counter = (attributes, children, node) => {
{ value as counter, setState } = node.useState('counter', 0)
increase = () => setState(counter + 1)
decrease = () => setState(counter - 1)
<div>
<button on={ click: increase }>'increase'</button>
<button on={ click: decrease }>'decrease'</button>
<b style={ 'font-size': '20px' }>' 'counter' '</b>
</div>
}
Blop runtime will store this state internally on the node.state
attribute. Calling setState
trigger a partial re-render of the Component itself.
The useContext
function setup a way for a component to communicate values down to its own children.
Signature: node.useContext(name: string, initialValue: any): { value: any, setContext: Function }
ContextConsumer = (attributes, children, node) => {
{ value } = node.useContext('specialNumber')
<p>value</p>
}
ContextHolder = (attributes, children, node) => {
{ setContext } = node.useContext('specialNumber', Math.random())
changeValue = () => setContext(Math.random())
<div>
<ContextConsumer />
<button on={ click: changeValue }>'Change value of the context'</button>
</div>
}
useContext
allow you to pass values down the tree while still being segregated hierarchically.
Changing a context value in a parent will trigger a re-render in the listening children components.
Note: Children passed down to a component as a second parameter will not have access to its context. It is because those children are rendered/called first in the parent before being passed down.
A component has mount
and an unmount
method that allow you to register lifecycle callbacks.
Signature: node.mount(Function)
and node.unmount(Function)
Those callbacks are called when the component is created (mount) and when is destroyed (unmount).
def useWindowWidth(node) {
{ value as width, setState as setWidth } = node.useState('width', window.innerWidth)
handleResize = () => setWidth(window.innerWidth)
node.mount(() => {
console.log('mount useWindowWidth')
window.addEventListener('resize', handleResize)
})
node.unmount(() => {
console.log('unmount useWindowWidth')
window.removeEventListener('resize', handleResize)
})
return width
}
WidthReactive = (attributes, children, node) => {
width = useWindowWidth(node)
<p>width</p>
}
When an attribute is changed blop will re-render a component, but no lifecycle methods
will be called. If an attribute change should have a side effect like calling an
API, you can use the onChange
method. You will need to set it up a mount time like so:
class FetchOnURLChangeComponent extends Component {
def render() {
<div>
if this.list {
<ul>
for item in this.list {
<li>item.name</li>
}
</ul>
}
</div>
}
async def fetchData() {
response = await fetch(this.attributes.url)
this.list = (await response.json()).results
this.refresh()
}
def onMount() {
this.fetchData()
this.onChange('url', () => this.fetchData())
}
}
ChangeURLComponent = (attributes, children, node) => {
{ value, setState } = node.useState('counter', 0)
node.mount(() => {
setInterval(() => setState(value + 1))
})
<div>
<FetchOnURLChangeComponent url="http://api.example.com?counter="value""></FetchOnURLChangeComponent>
</div>
}