Skip to content
Batiste Bieler edited this page Aug 22, 2023 · 44 revisions

What are Blop 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:

  1. The 1st parameter is an object which properties are the attributes of the pseudo HTML element.
  2. The 2nd parameter is an Array of the children of this element (empty if no children exist).
  3. The 3rd parameter is a Component instance. This is the Blop internal representation of the instance of your component.

Component Class

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.

Available Component Class methods

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
}

Component State

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.

Component Context

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.

Component Lifecycle

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>
}

Reacting to attribute change

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>
}