Skip to content

Instantly share code, notes, and snippets.

@apples
Last active March 4, 2020 06:21
Show Gist options
  • Select an option

  • Save apples/1eb76dd522ba5aa38717d0a7e9c1d174 to your computer and use it in GitHub Desktop.

Select an option

Save apples/1eb76dd522ba5aa38717d0a7e9c1d174 to your computer and use it in GitHub Desktop.
Lua virtual DOM implementation (based on v1.0 of https://github.com/pomber/didact). NOW WITH HOOKS!
local linq = require('linq')
local class = require('class')
local component_base = class()
local function assign(dest, source)
for k,v in pairs(source) do
dest[k] = v
end
return dest
end
local function is_event_name(str)
return string.sub(str, 1, 3) == 'on_'
end
local function update_widget_properties(widget, prev_props, next_props)
for k,v in pairs(prev_props) do
if is_event_name(k) then
widget[k] = nil
else
widget:set_attribute(k, nil)
end
end
for k,v in pairs(next_props) do
if is_event_name(k) then
widget[k] = v
else
widget:set_attribute(k, type(v) == 'string' and v or tostring(v))
end
end
end
local function is_vdom_element(element)
return type(element) == 'table' and
element.type ~= nil and
element.props ~= nil and
element.children ~= nil
end
local function is_component_type(cls)
if type(cls) ~= 'table' then return false end
local mt = cls
while mt ~= nil and mt ~= component_base do
mt = getmetatable(mt).__index
end
return mt == component_base
end
local function is_component_instance(com)
return type(com) == 'table' and
is_component_type(getmetatable(com))
end
local function create_element(element_type, config, ...)
assert(type(element_type) == 'string' or is_component_type(element_type))
assert(type(config) == 'nil' or type(config) == 'table')
local props = assign({}, config or {})
local children = linq({...}, select('#', ...))
-- Flatten children
:reduce(
linq({}),
function (acc, child)
if type(child) == 'table' and #child > 0 then
-- Flatten children that are arrays
return acc:concat(linq(child))
elseif is_vdom_element(child) or type(child) == 'string' then
-- Leave single children alone
return acc:concat(linq({child}))
else
-- Ignore nil/false
return acc
end
end)
-- Convert text to elements
:select(
function (child)
return type(child) == 'table' and
child or
create_element('_TEXT_ELEMENT_', { node_value = child })
end)
:tolist()
for _,child in ipairs(children) do
assert(is_vdom_element(child))
end
return {
type = element_type,
props = props,
children = children,
}
end
local function create_component_instance(element, instance)
assert(is_vdom_element(element))
assert(type(instance) == 'table')
local element_type = element.type
local props = element.props
local children = element.children
local component_instance = element_type.new(props, children)
component_instance.internal_instance = instance
return component_instance
end
local function instantiate(element, parent_widget)
assert(is_vdom_element(element))
local element_type = element.type
local props = element.props
local children = element.children
if type(element_type) == 'string' then
-- Instantiate widget element
if element_type == '_TEXT_ELEMENT_' then
error('_TEXT_ELEMENT_ not supported')
end
local widget = parent_widget:create_widget(element_type)
update_widget_properties(widget, {}, props)
local child_instances = linq(children)
:select(function (c) return instantiate(c, widget) end)
:tolist()
local child_widgets = linq(child_instances)
:select(function (child_instance) return child_instance.widget end)
:tolist()
for _,child_widget in ipairs(child_widgets) do
widget:add_child(child_widget)
end
return {
widget = widget,
element = element,
child_instances = child_instances
}
else
local instance = {}
local component_instance = create_component_instance(element, instance)
local child_element = component_instance:render()
local child_instance = instantiate(child_element, parent_widget)
instance.widget = child_instance.widget
instance.element = element
instance.child_instance = child_instance
instance.component_instance = component_instance
return instance
end
end
local function is_instance(instance)
return type(instance) == 'table' and
instance.widget ~= nil and
is_vdom_element(instance.element)
end
local function is_widget_instance(instance)
return is_instance(instance) and
type(instance.child_instances) == 'table'
end
local function is_component_instance(instance)
return is_instance(instance) and
is_instance(instance.child_instance) and
is_component_instance(instance.component_instance)
end
local reconcile -- forward
local function reconcile_children(instance, element)
assert(is_widget_instance(instance))
assert(is_vdom_element(element))
local widget = instance.widget
local child_instances = instance.child_instances
local next_child_elements = element.children
local new_child_instances = {}
local count = math.max(#child_instances, #next_child_elements)
for i = 1, count do
local child_instance = child_instances[i]
local child_element = next_child_elements[i]
local new_child_instance = reconcile(widget, child_instance, child_element)
if new_child_instance then
new_child_instances[#new_child_instances + 1] = new_child_instance
end
end
return new_child_instances
end
local function cleanup(instance)
instance.widget = nil
if instance.child_instance then
cleanup(instance.child_instance)
end
if instance.child_instances then
for _,v in ipairs(instance.child_instances) do
cleanup(v)
end
end
end
reconcile = function (parent_widget, instance, element)
assert(parent_widget ~= nil)
assert(instance == nil or is_instance(instance))
assert(element == nil or is_vdom_element(element))
if instance == nil then
-- Create instance
local new_instance = instantiate(element, parent_widget)
parent_widget:add_child(new_instance.widget)
return new_instance
elseif element == nil then
-- Remove instance
parent_widget:remove_child(instance.widget)
cleanup(instance)
return nil
elseif instance.element.type ~= element.type then
-- Replace instance
local new_instance = instantiate(element, parent_widget)
parent_widget:replace_child(instance.widget, new_instance.widget)
cleanup(instance)
return new_instance
elseif type(element.type) == 'string' then
-- Update instance
update_widget_properties(instance.widget, instance.element.props, element.props)
instance.child_instances = reconcile_children(instance, element)
instance.element = element
return instance
else
-- Update composite instance
instance.component_instance.props = element.props
local child_element = instance.component_instance:render()
local old_child_instance = instance.child_instance
local child_instance = reconcile(parent_widget, old_child_instance, child_element)
instance.widget = child_instance.widget
instance.child_instance = child_instance
instance.element = element
return instance
end
end
local function render(element, container)
assert(is_vdom_element(element))
return reconcile(container, nil, element)
end
function component_base:constructor(props)
self.props = props
self.state = self.state or {}
end
function component_base:set_state(partial_state)
assert(type(partial_state) == 'table')
local new_state = {}
assign(new_state, self.state)
assign(new_state, partial_state)
self.state = new_state
local internal_instance = self.internal_instance
local parent_widget = internal_instance.widget:get_parent()
local element = internal_instance.element
reconcile(parent_widget, internal_instance, element)
end
local function component()
return class(component_base)
end
return {
component = component,
create_element = create_element,
render = render
}
local linq = {}
local where_iter = {}
local select_iter = {}
local concat_iter = {}
local drop_iter = {}
-- linq base
linq.__index = linq
setmetatable(
linq,
{
__call = function (cls, ...)
return cls.new(...)
end
}
)
function linq.new(seq, len)
if not seq then
error("seq must not be null", 2)
end
local self = setmetatable({}, linq)
self.source = seq
self.index = 0
self.length = len or #seq
return self
end
function linq:__call()
self.index = self.index + 1
if self.index <= self.length then
return self.source[self.index]
end
end
-- methods
function linq:first()
return self()
end
function linq:tolist()
local list = {}
for v in self do
list[#list + 1] = v
end
return list
end
function linq:reduce(accum, func)
for v in self do
accum = func(accum, v)
end
return accum
end
function linq:where(pred)
return where_iter(self, pred)
end
function linq:select(mapper)
return select_iter(self, mapper)
end
function linq:concat(seq)
return concat_iter(self, seq)
end
function linq:drop(count)
return drop_iter(self, count)
end
-- where iterator
where_iter.__index = where_iter
setmetatable(
where_iter,
{
__index = linq,
__call = function (cls, ...)
return cls.new(...)
end
}
)
function where_iter.new(source, pred)
local self = setmetatable({}, where_iter)
self.source = source
self.predicate = pred
return self
end
function where_iter:__call()
local x = self.source()
while x ~= nil do
if self.predicate(x) then
return x
else
x = self.source()
end
end
end
-- select iterator
select_iter.__index = select_iter
setmetatable(
select_iter,
{
__index = linq,
__call = function (cls, ...)
return cls.new(...)
end
}
)
function select_iter.new(source, mapper)
local self = setmetatable({}, select_iter)
self.source = source
self.mapper = mapper
self.index = 0
return self
end
function select_iter:__call()
self.index = self.index + 1
local x = self.source()
if x ~= nil then
return self.mapper(x, self.index)
end
end
-- concat iterator
concat_iter.__index = concat_iter
setmetatable(
concat_iter,
{
__index = linq,
__call = function (cls, ...)
return cls.new(...)
end
}
)
function concat_iter.new(source1, source2)
local self = setmetatable({}, concat_iter)
self.sources = {source1, source2}
self.current = 1
return self
end
function concat_iter:__call()
local x = self.sources[self.current]()
while x == nil and self.current < #self.sources do
self.current = self.current + 1
x = self.sources[self.current]()
end
if x ~= nil then
return x
end
end
-- drop iterator
drop_iter.__index = drop_iter
setmetatable(
drop_iter,
{
__index = linq,
__call = function (cls, ...)
return cls.new(...)
end
}
)
function drop_iter.new(source, count)
local self = setmetatable({}, drop_iter)
self.source = source
self.count = count
return self
end
function drop_iter:__call()
while self.count > 0 do
self.source()
self.count = self.count - 1
end
return self.source()
end
return linq
Copy link

ghost commented Feb 3, 2019

What module is linq? 🤔

@apples
Copy link
Author

apples commented Mar 4, 2020

@mrtnpwn Apparently Gists don't (didn't?) have notifications, so I didn't see this until just now 😬

I've uploaded linq.lua here: https://gist.github.com/apples/ebfc94c04f38447cbe0a654f442969da.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment