Last active
March 4, 2020 06:21
-
-
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!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
Author
@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
What module is
linq? 🤔