mmm​.s‑ol.nu

fun stuff with code and wires
an error occured while rendering this view:

mmm.component

mmm.component is a small and DOM-centric framework for reactive web interfaces.

Built for reactive UI, mmm.component is meant to run on the client using fengari.io. However, like mmm.dom, the API is supported both on the client as well as on the server (were most reactive features have been omitted) so that static pre-rendered content can still be generated from the same code that powers the reactive interface.

Examples

Feel free to read the documentation below, or take a look at the following examples using the mmmfs inspect mode:

You will also find that most interactive UI on this page, including the navigation and browsing system, have been built using mmm.component as well. The source code is available here.

Guide

Begin by requiring mmm.component. The module returns a table containing the following:

  • ReactiveVar: class/constructor for reactive state variables.
  • ReactiveElement: class/constructor for reactive DOM elements. Rarely used directly.
  • elements: 'magic table' containing constructors for ReactiveElements by tag name.
  • tohtml: helper to convert from ReactiveElements to mmm/dom (DOM nodes / HTML strings)
  • text: helper to convert Lua strings to DOM Text Nodes.
  • get_or_create: helper for rehydratable views.

Reactive Variables

mmm.component is centered around the concept of Reactive Variables (ReactiveVars). A ReactiveVar is a container for a piece of application state that other pieces of code can subscribe to. These attached callbacks are invoked whenever the value changes.

You can instantiate a ReactiveVar via the constructor at any time. The constructor takes the initial variable as an argument, but if you omit it nil will work fine as well. After instantiation, :get() and :set(val) will give access to the value:

import ReactiveVar from require 'mmm.component'

test = ReactiveVar 3
print test\get! -- prints '3'
test\set 4
print test\get! -- prints '4'
local ReactiveVar = require 'mmm.component'.ReactiveVar

local test = ReactiveVar(3)
print(test:get()) -- prints '3'
test:set(4)
print(test:get()) -- prints '4'

The value can also be changed using :transform(fn), which is simply a shorthand for var:set(fn(var:get())):

import ReactiveVar from require 'mmm.component'

add_one = (n) -> n + 1
count = ReactiveVar 1

count\transform add_one
print test\get! -- prints '2'

count\transform add_one
print test\get! -- prints '3'
local ReactiveVar = require 'mmm.component'.ReactiveVar

local function add_one(x) return x + 1 end
local count = ReactiveVar(1)

count:transform(add_one)
print(test:get()) -- prints '2'

count:transform(add_one)
print(test:get()) -- prints '3'

Now, so far we haven't really seen anything useful - this is all just behaving like a normal variable. The :subscribe(callback) method is what makes ReactiveVars interesting: Whenever the value changes, the ReactiveVar calls each of the registered handlers, passing the new as well as the previous value:

import ReactiveVar from require 'mmm.component'

add_one = (n) -> n + 1
count = ReactiveVar 1
count\subscribe (new, old) ->
  print "changing from #{old} to #{new}!"


count\transform add_one -- changing from 1 to 2
count\set "a string"    -- changing from 2 to a string
local ReactiveVar = require 'mmm.component'.ReactiveVar

local function add_one(x) return x + 1 end
local count = ReactiveVar(1)
cout:subscribe(function(new old)
  print("changing from " .. old .. " to " .. new)
end)

count:transform(add_one) -- changing from 1 to 2
count:set("a string")    -- changing from 2 to a string

This allows other code (such as ReactiveElements) to react to value changes and update themselves, as we will see in a minute. Often you will want to derive state from other state. To make this easy while keeping everything reactive, mmm.component includes the :map(fn) method.

:map(fn) applies the function fn to the current value, just as :transform(fn) would, but it doesn't update the value itself - it rather returns a new ReactiveVar instance that is already set up to update whenever the original one changes.

import ReactiveVar from require 'mmm.component'

fruit = ReactiveVar "apple"
loud_fruit = fruit\map string.upper

print fruit\get!      -- prints 'apple'
print loud_fruit\get! -- prints 'APPLE'

fruit\set "orange"
print loud_fruit\get! -- prints 'ORANGE'
local ReactiveVar = require 'mmm.component'.ReactiveVar

local fruit = ReactiveVar("apple")
local loud_fruit = fruit:map(string.upper)

print(fruit:get())      -- prints 'apple'
print(loud_fruit:get()) -- prints 'APPLE'

fruit:set("orange")
print(loud_fruit:get()) -- prints 'ORANGE'

ReactiveElements

ReactiveElement is a wrapper around DOM elements that allows you to use ReactiveVars to specify attributes and children of the element. Internally it :subscribe()s to each of the ReactiveVars so that it can update whenever any of the values change.

ReactiveElements can be instantiated using the ReactiveElement constructor, but usually you will want to use the shorthand functions that you can pull out of the elements 'magic table', as they will make your code much more legible. This table provides functions for creating any HTML element based on it's name. The elements you use are automatically cached so you can either pull out only the ones you want to use into your local namespace or use the table itself.

Each of the node creation functions behave just like the counterparts in mmm.dom, except for the fact that each child or attribute value can also be provided as a ReactiveVar instance and will be unwrapped and tracked automatically:

import ReactiveVar, elements from require 'mmm.component'
import div, input, br from elements

text = ReactiveVar "your text here"

handler = (e) => text\set e.target.value

div {
  input value: text\get!, oninput: hander
  br!
  input disabled: true, value: text
}
local comp = require 'mmm.component'
local div, input, br = comp.elements.div, comp.elements.input, comp.elements.br

local text = comp.ReactiveVar "your text here"

return div{
  input{
    value = text:get(),
    oninput = function (_, e)
      text:set(e.target.value)
    end,
  },
  br(),
  input{
    disabled = true,
    value = text,
  },
}

Note that when you want to pass non-elements (e.g. strings or numbers) to an element as children, they will automatically be escaped using text, but this breaks reactivity, so you should :map(text) these values before passing them to an element, like this:

import ReactiveVar, text, elements from require 'mmm.component'
import div, button from elements

count = ReactiveVar 0

div {
  button '-', onclick: () -> count\transform (c) -> c - 1
  " count is: "
  count\map text
  " "
  button '+', onclick: () -> count\transform (c) -> c + 1
}
local comp = require 'mmm.component'
local div, button = comp.elements.div, comp.elements.button

local count = comp.ReactiveVar(0)

return div {
  button {
    '-',
    onclick = function () count:set(count:get() - 1) end
  },
  " count is: ",
  count:map(comp.text),
  " ",
  button {
    '+',
    onclick = function () count:set(count:get() + 1) end
  },
}
count is: 0