Allow form views to reference the data model
Integrates Placeholder into Variation and Manager code to record attempts to access the data model and evaluate them when it is available. - Model data is accessed through the `model` namespace. - Elements are now available only through the `ui` namespace. - Variant properties may be Placeholders or functions; both are automatically evaluated at render time.
This commit is contained in:
parent
4f839ce39e
commit
4849ded39a
@ -3,7 +3,8 @@ allow_defined_top = true
|
||||
|
||||
globals = {
|
||||
"table",
|
||||
"minetest"
|
||||
"minetest",
|
||||
"model"
|
||||
}
|
||||
|
||||
read_globals = {
|
||||
@ -14,8 +15,8 @@ read_globals = {
|
||||
"vector", "ItemStack",
|
||||
"dump", "DIR_DELIM", "VoxelArea", "Settings",
|
||||
|
||||
"libuix", "import",
|
||||
"libuix", "import", "ui",
|
||||
|
||||
-- libuix elements
|
||||
"list", "listring", "listcolors", "image", "text",
|
||||
-- libuix operator replacement functions
|
||||
"eq", "ne", "lt", "gt", "le", "ge", "land", "lor", "lnot"
|
||||
}
|
||||
|
12
README.md
12
README.md
@ -16,13 +16,13 @@ Let's create and show a very simple formspec with just three elements:
|
||||
|
||||
```lua
|
||||
uix:formspec("example") { w = 5, h = 5 } {
|
||||
field { x = 0, y = 1, w = 5, h = 1, label = "Message:", _model = "message" },
|
||||
button { x = 0, y = 2.5, w = 5, h = 1, label = "Submit", _click = "submit" },
|
||||
text { x = 0, y = 4, _if = "message ~= ''", _text = "You said: ${message}" }
|
||||
ui.field { x = 0, y = 1, w = 5, h = 1, label = "Message:", bind = model.message },
|
||||
ui.button { x = 0, y = 2.5, w = 5, h = 1, label = "Submit", click = model.submit },
|
||||
ui.text { x = 0, y = 4, visible = ne(model.message, ""), text = "You said: " .. model.message }
|
||||
} {
|
||||
message = "",
|
||||
submit = function(self)
|
||||
print("Hey! " .. self._player_name .. " submitted our form!")
|
||||
submit = function()
|
||||
print("Hey! " .. model._player_name .. " submitted our form!")
|
||||
end
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ And just in case you're not a fan of having to type the name of each property, y
|
||||
|
||||
```lua
|
||||
{
|
||||
field { 0, 1, 5, 1, "Message:", _model = "message" }
|
||||
ui.field { 0, 1, 5, 1, "Message:", bind = model.message }
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -224,7 +224,7 @@ function benchmark.container_element()
|
||||
local populated
|
||||
Benchmark("Prepare container:", BENCHMARK_COUNT, function()
|
||||
populated = Elements.container { x = 0, y = 0 } {
|
||||
text { x = 0, y = 0, text = "Hello world!" }
|
||||
ui.text { x = 0, y = 0, text = "Hello world!" }
|
||||
}
|
||||
end)
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
local utility = import("utility.lua")
|
||||
local Placeholder = import("placeholder.lua")
|
||||
|
||||
---------------------
|
||||
-- Variation Class --
|
||||
@ -25,8 +26,11 @@ function Variation:new(parent, name, fields, options, child_elements)
|
||||
table.constrain(options.contains, {
|
||||
{"validate", "string|function"},
|
||||
{"environment", "table|function", required = false},
|
||||
{"environment_namespace", "string", required = false},
|
||||
{"environment_ready", "boolean", required = false},
|
||||
{"render", "function"},
|
||||
{"render_target", "string", required = false}
|
||||
{"render_target", "string", required = false},
|
||||
{"bind_model", "boolean", required = false}
|
||||
})
|
||||
|
||||
if options.contains.render_target then
|
||||
@ -43,6 +47,10 @@ function Variation:new(parent, name, fields, options, child_elements)
|
||||
end
|
||||
end
|
||||
|
||||
if options and options.contains and not options.contains.environment_namespace then
|
||||
options.contains.environment_namespace = "ui"
|
||||
end
|
||||
|
||||
local instance = {
|
||||
parent = parent,
|
||||
name = name,
|
||||
@ -76,9 +84,28 @@ function Variation:__call(def)
|
||||
self.options.contains.environment = self.options.contains.environment(self)
|
||||
end
|
||||
|
||||
local original_env
|
||||
-- if an environment table is provided, add it to the global environment
|
||||
if self.options.contains.environment then
|
||||
setmetatable(self.options.contains.environment, { __index = _G })
|
||||
if not self.options.contains.environment_ready then
|
||||
-- if the environment namepsace is not disabled, nest the environment in a namespace
|
||||
if self.options.contains.environment_namespace ~= "" then
|
||||
local env = self.options.contains.environment
|
||||
self.options.contains.environment = {}
|
||||
self.options.contains.environment[self.options.contains.environment_namespace] = env
|
||||
end
|
||||
|
||||
-- if model binding is not explicity disabled, add Placeholder listeners to the environment
|
||||
if self.options.contains.bind_model ~= false then
|
||||
Placeholder.new_listener(self.options.contains.environment)
|
||||
else -- otherwise, inherit the entire global environment
|
||||
setmetatable(self.options.contains.environment, { __index = _G })
|
||||
end
|
||||
|
||||
self.options.contains.environment_ready = true
|
||||
end
|
||||
|
||||
original_env = getfenv(2)
|
||||
setfenv(2, self.options.contains.environment)
|
||||
end
|
||||
|
||||
@ -93,7 +120,7 @@ function Variation:__call(def)
|
||||
|
||||
-- if an environment table is provided, remove it from the global environment
|
||||
if self.options.contains.environment then
|
||||
setfenv(2, getmetatable(self.options.contains.environment).__index)
|
||||
setfenv(2, original_env)
|
||||
end
|
||||
|
||||
return self:populate_new(def, items)
|
||||
@ -137,15 +164,18 @@ function Variation:validate()
|
||||
validate_err:throw("%s property '%s' is not optional", self.name, field[1])
|
||||
end
|
||||
|
||||
-- if the value of the data stored in the definition field does not match the expected type, throw an error
|
||||
if def_field ~= nil and type(def_field) ~= field[2] then
|
||||
validate_err:throw("%s property '%s' must be a %s (found %s)", self.name, field[1], field[2], type(def_field))
|
||||
end
|
||||
-- if the value is a function or Placeholder, we can just ignore it
|
||||
if not utility.check_type(def_field, "function|Placeholder") then
|
||||
-- if the value of the data stored in the definition field does not match the expected type, throw an error
|
||||
if def_field ~= nil and type(def_field) ~= field[2] then
|
||||
validate_err:throw("%s property '%s' must be a %s (found %s)", self.name, field[1], field[2], type(def_field))
|
||||
end
|
||||
|
||||
-- if the variation requires a specific value, check it and throw an error if it isn't satisfied
|
||||
if field[3] and def_field ~= field[3] then
|
||||
validate_err:throw("%s property '%s' must be a %s with value %s (found %s with value %s)", self.name, field[1],
|
||||
field[2], dump(field[3]), type(def_field), dump(def_field))
|
||||
-- if the variation requires a specific value, check it and throw an error if it isn't satisfied
|
||||
if field[3] and def_field ~= field[3] then
|
||||
validate_err:throw("%s property '%s' must be a %s with value %s (found %s with value %s)", self.name, field[1],
|
||||
field[2], dump(field[3]), type(def_field), dump(def_field))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -160,12 +190,70 @@ function Variation:validate()
|
||||
return true
|
||||
end
|
||||
|
||||
local evaluate_env = {
|
||||
eq = Placeholder.is.eq, ne = Placeholder.is.ne, lt = Placeholder.is.lt, gt = Placeholder.is.gt, le = Placeholder.is.le,
|
||||
ge = Placeholder.is.ge, land = Placeholder.logical.l_and, lor = Placeholder.logical.l_or,
|
||||
lnot = Placeholder.logical.l_not
|
||||
}
|
||||
local function_call_env = {}
|
||||
setmetatable(evaluate_env, { __index = _G })
|
||||
setmetatable(function_call_env, { __index = _G })
|
||||
local evaluate_err = utility.ErrorBuilder:new("Variation:evaluate")
|
||||
-- Takes the index of a field in self.fields and returns the corresponding value from the definition. If the value is a
|
||||
-- placeholder or function the value is extracted and checked for validity. If the value is nil and the field is to be
|
||||
-- generated, a new ID is fetched from the parent form. An error is thrown if anything is invalid.
|
||||
function Variation:evaluate(form, field_index)
|
||||
local field_def = self.fields[field_index]
|
||||
local value = self.def[self.field_map[field_index]]
|
||||
evaluate_env.model = form.model
|
||||
function_call_env.model = form.model
|
||||
|
||||
while utility.check_type(value, "Placeholder|function") do
|
||||
-- if the value is a function, call it until it isn't
|
||||
while type(value) == "function" do
|
||||
setfenv(value, function_call_env)
|
||||
value = value()
|
||||
end
|
||||
|
||||
-- if the value is a placeholder, evaluate it
|
||||
if utility.type(value) == "Placeholder" then
|
||||
value = Placeholder.evaluate(evaluate_env, value, function_call_env)
|
||||
end
|
||||
end
|
||||
|
||||
-- if the value should be generated, fetch a new ID from the parent form and return immediately
|
||||
if field_def.generate and (field_def.hidden or value == nil) then
|
||||
self.generated_def[field_def[1]] = tostring(form:new_id())
|
||||
return self.generated_def[field_def[1]]
|
||||
end
|
||||
|
||||
-- if the value is nil and the field is required, throw an error
|
||||
if value == nil and field_def.required ~= false and field_def.hidden ~= true then
|
||||
evaluate_err:throw("%s property '%s' evaluated to nil but the property is not optional", self.name, field_def[1])
|
||||
end
|
||||
|
||||
-- if the new value does not match the expected type, throw an error
|
||||
if value ~= nil and type(value) ~= field_def[2] then
|
||||
evaluate_err:throw("%s property '%s' evaluated to a %s but the property requires a %s", self.name, field_def[1],
|
||||
type(value), field_def[2])
|
||||
end
|
||||
|
||||
-- if a specific value is required, check it and throw an error if it isn't satisfied
|
||||
if field_def[3] and value ~= field_def[3] then
|
||||
evaluate_err:throw("%s property '%s' evaluated to a %s with value %s but the property requires a %s with value %s",
|
||||
self.name, field_def[1], type(value), dump(value), field_def[2], field_def[3])
|
||||
end
|
||||
|
||||
if value == nil then return ""
|
||||
else return value end
|
||||
end
|
||||
|
||||
local render_err = utility.ErrorBuilder:new("Variation:render")
|
||||
-- Renders a variation given a form as context.
|
||||
function Variation:render(form)
|
||||
if utility.DEBUG then utility.enforce_types({"Form"}, form) end
|
||||
|
||||
-- Obey _if visibility control.
|
||||
-- Obey visibility control.
|
||||
if self.def._if and not form.model:_evaluate(self.def._if) then
|
||||
return ""
|
||||
end
|
||||
@ -187,13 +275,7 @@ function Variation:render(form)
|
||||
handle_container_target = false
|
||||
contained = ""
|
||||
elseif field.internal ~= true then
|
||||
local def_index = self.field_map[index]
|
||||
local value = self.def[def_index]
|
||||
if field.generate and (field.hidden or value == nil) then
|
||||
self.generated_def[field[1]] = tostring(form:new_id())
|
||||
value = self.generated_def[field[1]]
|
||||
elseif value == nil then value = "" end
|
||||
fieldstring = fieldstring .. tostring(value) .. separator
|
||||
fieldstring = fieldstring .. tostring(self:evaluate(form, index)) .. separator
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -3,6 +3,7 @@ local utility = import("utility.lua")
|
||||
local Form = import("formspec/form.lua")
|
||||
local Model = import("formspec/model.lua")
|
||||
local Elements = import("formspec/elements.lua")
|
||||
local Placeholder = import("placeholder.lua")
|
||||
|
||||
---------------------------
|
||||
-- FormspecManager Class --
|
||||
@ -16,8 +17,10 @@ function FormspecManager:new(parent)
|
||||
local instance = { parent = parent, forms = {} }
|
||||
setmetatable(instance, FormspecManager)
|
||||
|
||||
instance.elements = Elements(instance)
|
||||
setmetatable(instance.elements, { __index = _G })
|
||||
local elements = Elements(instance)
|
||||
instance.elements = { ui = elements }
|
||||
Placeholder.new_listener(instance.elements)
|
||||
instance.original_env = getfenv(2)
|
||||
|
||||
return instance
|
||||
end
|
||||
@ -33,7 +36,7 @@ function FormspecManager:__call(name)
|
||||
return function(elements)
|
||||
-- Accept data model table.
|
||||
return function(model)
|
||||
setfenv(2, getmetatable(self.elements).__index) -- Remove Elements from the global environment.
|
||||
setfenv(2, self.original_env) -- Remove Elements from the global environment.
|
||||
self.forms[#self.forms + 1] = Form:new(self, name, options, elements, Model:new(model))
|
||||
end
|
||||
end
|
||||
|
@ -6,6 +6,8 @@ local manager = mock.FormspecManager:new(nil, require("formspec/elements"))
|
||||
local form = mock.Form:new()
|
||||
local Variation = require("formspec/element/variation")
|
||||
local Model = require("formspec/model")
|
||||
local Placeholder = require("placeholder")
|
||||
local utility = require("utility")
|
||||
|
||||
local field_name = { "name", "string" }
|
||||
local Example = Variation:new(manager, "variation_spec", {
|
||||
@ -72,6 +74,47 @@ describe("Variation", function()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("evaluate", function()
|
||||
local Evaluate = Variation:new(manager, "evaluate_spec", {
|
||||
{ "type", "number", 1 },
|
||||
{ "x", "number" },
|
||||
{ "name", "string", hidden = true, generate = true },
|
||||
{ "text", "string" },
|
||||
{ "visible", "boolean", required = false }
|
||||
})
|
||||
|
||||
local env = mock.Form:new(nil, {
|
||||
another = true, control = Placeholder.index("model").another,show = function() return model.control end
|
||||
})
|
||||
|
||||
it("returns a field from the definition table", function()
|
||||
local populated = Evaluate {
|
||||
type = 1, x = 10, text = function() return function() return "Hello!" end end,
|
||||
visible = Placeholder.index("model").show
|
||||
}
|
||||
|
||||
assert.are.equal(1, populated:evaluate(env, 1))
|
||||
assert.are.equal(10, populated:evaluate(env, 2))
|
||||
assert.are.equal("0", populated:evaluate(env, 3))
|
||||
assert.are.equal("Hello!", populated:evaluate(env, 4))
|
||||
assert.is_true(populated:evaluate(env, 5))
|
||||
end)
|
||||
|
||||
it("throws an error when values do not conform to variant rules", function()
|
||||
local populated = Evaluate {
|
||||
type = function() return 2 end, x = Placeholder.index("model").show; text = Placeholder.index("model").message
|
||||
}
|
||||
|
||||
assert.has_error(function() populated:evaluate(env, 1) end, "libuix->Variation:evaluate: evaluate_spec property "
|
||||
.. "'type' evaluated to a number with value 2 but the property requires a number with value 1")
|
||||
assert.has_error(function() populated:evaluate(env, 2) end, "libuix->Variation:evaluate: evaluate_spec property 'x' "
|
||||
.. "evaluated to a boolean but the property requires a number")
|
||||
assert.has_error(function() populated:evaluate(env, 4) end, "libuix->Variation:evaluate: evaluate_spec property "
|
||||
.. "'text' evaluated to nil but the property is not optional")
|
||||
assert.are.equal("", populated:evaluate(env, 5))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("render", function()
|
||||
it("autogenerates necessary fields", function()
|
||||
local last_id = form.last_id + 1
|
||||
@ -137,10 +180,13 @@ describe("Variation", function()
|
||||
local populated
|
||||
assert.has_no.error(function()
|
||||
populated = Example { x = 15, y = 7 } {
|
||||
text { x = 0, y = 0, text = "Hello!" }
|
||||
ui.text { x = 0, y = model.text_y, text = "Hello!" }
|
||||
}
|
||||
end)
|
||||
assert.are.equal("container_spec[15,7]label[0,0;Hello!]container_spec_end[]", populated:render(form))
|
||||
|
||||
assert.are.equal("Placeholder", utility.type(populated.items[1].def.y))
|
||||
local env = mock.Form:new(nil, { text_y = 0 })
|
||||
assert.are.equal("container_spec[15,7]label[0,0;Hello!]container_spec_end[]", populated:render(env))
|
||||
end)
|
||||
|
||||
it("encapsulating custom structures", function()
|
||||
|
@ -81,7 +81,7 @@ describe("'container' element", function()
|
||||
local populated
|
||||
(function()
|
||||
populated = manager.elements.container { x = 2, y = 2 } {
|
||||
text { x = 0, y = 0, text = "Hello!" }
|
||||
ui.text { x = 0, y = 0, text = "Hello!" }
|
||||
}
|
||||
end)()
|
||||
assert.are.equal("container[2,2]label[0,0;Hello!]container_end[]", populated:render(form))
|
||||
|
@ -10,12 +10,13 @@ describe("FormspecManager", function()
|
||||
local instance = FormspecManager:new(UIXInstance:new("unit_test"))
|
||||
|
||||
it("collects formspec name, options, elements, and model for addition to the instance", function()
|
||||
assert.has_no.error(function()
|
||||
-- assert.has_no.error(function()
|
||||
instance("manager_spec") { w = 5, h = 10 } {
|
||||
text { x = 0, y = 0, text = "Hello!" }
|
||||
} {}
|
||||
end)
|
||||
ui.text { x = 0, y = model.text_y, text = "Hello!" }
|
||||
} { text_y = 0 }
|
||||
-- end)
|
||||
|
||||
assert.are.equal("Placeholder", utility.type(instance.forms[1].elements[1].def.y))
|
||||
assert.are.equal("Hello!", instance.forms[1].elements[1].def.text)
|
||||
end)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user