This page looks best with JavaScript enabled

Frame, wtf?

 ·  ☕ 16 min read

This post is an introduction to the “frame” object in Lua modules in MediaWiki. I often tell people “don’t try to learn anything about frame on day 1,” and I still believe that’s the best course of action - there’s a lot of detail here you really do not need to know when you could instead be learning, “how can I iterate over all the items in a table?” or “how can I pattern-match my user input the way I need to?” or other way more useful things.

That said, “frame” is kind of thrown in your face from the get-go, and you do eventually need to learn what it is and how it works (at least to some extent). Indeed it’s possible that in some use cases you might want to leverage frames very early on in your Lua-writing career. So I won’t fault you for wanting to learn about them early, but do be warned that this post contains a lot of detail that may seem daunting, and it is totally not required reading!! Feel free to go away and come back later!

Most code in this post is example toy code that I wrote for this blog post; “real” code is noted explicitly.

A terminology note

What’s the difference between “parameter” and “argument”? That StackOverflow answer explains, but in practice I’ve never seen anyone distinguish, and I’m gonna use them pretty interchangeably. In fact I didn’t know the difference until I searched to see if there even was a difference when writing this note! So TIL~

What exactly is a frame object?

From the Scribunto docs:

The frame object is the interface to the parameters passed to {{#invoke:}}, and to the parser.

Cool, so, there’s a couple technical words with specific meanings here.

  • object - it has methods aka functions (like :callParserFunction()) - remember in Lua you call object methods with a : - and some attributes (like .args)
  • interface - not in the Object-Oriented Programming sense of the word, but it’s, like, a portal if you will, by which you access a bunch of black magic shit from MediaWiki-land (i.e. outside of Lua). This is how we get a whole bunch of “actually-we-really-do-live-inside-of-a-wiki stuff”
  • parameters - all the “stuff!”
  • invoke - the parser function that brings you from MediaWiki-land to Lua-land
  • parser - in addition to being an interface to our parameters, it’s an interface to the MediaWiki parser (we have :callParserFunction(), :expandTemplate() methods, etc - we’ll talk about these later!)

Let’s now rewrite the docs definition in totally non-technical language.

The frame thing is a portal to all the stuff that MediaWiki gives us when we call a Lua module (from a MediaWiki page). It’s also a way that we can access MediaWiki-land stuff that was NOT deliberately passed to us, even though we’re in Lua-land.

If that makes it sound incredibly magical and overpowered, good. It is in fact incredibly magical and overpowered - to the point that you should NOT use all of its functionality! More on that in the “Best practices” section later on :)

How do we get a frame object?

Also from the docs:

Note that there is no frame library, and there is no global variable named frame. A frame object is typically obtained by being passed as a parameter to the function called by {{#invoke:}}, and can also be obtained from mw.getCurrentFrame().

Let’s write this short module at Module:HelloWorld (if you have a test wiki available, I recommend actually copying code samples to it - or even better, retyping them yourself - and following along):

1
2
3
4
5
6
local p = {}
function p.main(kittens)
    local puppies = mw.getCurrentFrame()
    return 'hello world'
end
return p

If we run {{#invoke:HelloWorld|main}} (i.e. type this on a normal, non-module wiki page and preview/save it), then, did we have any frame object?

Yes! There’s no variable explicitly named frame, but we had TWO instances of a frame! I just called one kittens and the other puppies.

As the documentation says, any time you have an invoke, precisely ONE parameter is sent to the function that you invoked: a frame object. By convention, we always write p.main(frame), and so our frame variable will always just be called frame, but that’s just a convention. The frame variable is:

  • Not global
  • Only called frame if you say it is
  • Only scoped to where you say it is
  • Available by calling mw.getCurrentFrame() from any point in any function in any module
  • Provided by #invoke to the function that you invoke, as the only parameter to that function

Back to our HelloWorld example, with our normal conventions and not creating any “useless” frame objects we would instead write:

1
2
3
4
5
local p = {}
function p.main(frame)
    return 'hello world'
end
return p

And this is a typical hello world module with no eccentricities.

What about args?

Now, let’s invoke HelloWorld with {{#invoke:HelloWorld|main|you=Arya Stark}}. The new part is |you=Arya Stark - this looks a lot like having a template parameter that we could access like {{{you|}}} - and it is! But in Lua-land, we need to use our frame-portal-thing to access the parameter value. Take a look:

1
2
3
4
5
6
local p = {}
function p.main(frame)
    local args = frame.args
    return 'hello ' .. args.you
end
return p

Here’s an equivalent way to write that:

1
2
3
4
5
6
local p = {}
function p.main(frame)
    local args = frame['args']
    return 'hello ' .. args['you']
end
return p

For the rest of the article, I’m gonna use exclusively the . notation instead of the [''] notation, but they both work, as long as there’s no spaces or special characters in the thing you’re accessing (if there are, then you need to use the [''] notation).

Our output will be hello Arya Stark.

But what if we wanted to make this a template? Try creating a template called Template:HelloWorld, with the code <includeonly>{{#invoke:HelloWorld|main}}</includeonly>, and then call {{HelloWorld|you=Arya Stark}}.

We still want this to work! After all, users don’t want to see invokes all over the place, that looks a lot more confusing/messier than template calls; also TemplateData with VisualEditor requires templates, and we want to be interfacing through templates so that reports like Special:UnusedTemplates function.

Let’s run it (i.e. preview or save a page with {{HelloWorld|you=Arya Stark}} on it):

Lua error in Module:HelloWorld at line 4: attempt to concatenate field 'you' (a nil value).

Uh oh. What’s going on?

Different frames

The you parameter holding the value Arya Stark is still in a frame object, but it’s not in the same frame object. That’s right – there’s more than one frame object associated with our code!

Specifically, there’s one frame object that holds the args directly passed to the invoke. That’s the one we’ve been working with so far. Then there’s another one holding any args passed to the template containing the invoke. The second frame is called the “parent” of the first (by extension, the original is the “child” of its parent), and we can access it like this:

1
2
3
4
5
6
7
local p = {}
function p.main(frame)
    local parentFrame = frame:getParent()
    local parentArgs = parentFrame.args
    return 'hello ' .. parentArgs.you
end
return p

Now when we run it, we get hello Arya Stark once again. Yay!

Here’s a table summarizing:

Thing Args passed to invoke Args passed to template
Type of value Provided by the “code” (the template author) Provided by the “user” (the wiki editor)
Example `{{#invoke:HelloWorld main
Informal name of frame Child frame Parent frame
How to access the frame frame frame:getParent()
How to access the args frame.args frame:getParent().args

What if we want to collect both?

This situation, where we want to know the params from both the child frame AND the parent frame, arises frequently. Here’s a real-world example from Leaguepedia:

In Template:Champion, I have the following code (plus documentation and categories):

<includeonly>{{#invoke:InstantiateEntity|main|entityType=Champion}}</includeonly>

The parameter |entityType=Champion is hidden from editors and supplied directly to the module via the default (“child”) frame. Then, when a user calls this template, they can write for example:

{{Champion|Sona}}

Which will display a picture of Sona next to her name and link to her wiki page.

Inside the module, I want a single args table that combines the args from both frame objects, the parent and the child. How can I do this?

Well, I have two options:

  • In the case of a conflict (the same value is defined by both the child & the parent), I can use the value provided by the parent (i.e. the template) - this is useful when I want to provide a default value
  • In the case of a conflict, I can use the value provided by the child (i.e. the invoke) - this is useful when I want to sanitize user input prior to passing it to the module

In my Module:ArgsUtil, I have code that accomplishes both of these. And here’s where we get to the point of, “if you want you can just ignore this, copy it to your wiki, and use it happily” (this code is licensed under CC BY-SA 3.0):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
local p = {}
-- snip
function p.merge()
	local f = mw.getCurrentFrame()
	local origArgs = f.args
	local parentArgs = f:getParent().args

	local args = {}
	
	for k, v in pairs(origArgs) do
		v = mw.text.trim(tostring(v))
		if v ~= '' then
			args[k] = v
		end
	end
	
	for k, v in pairs(parentArgs) do
		v = mw.text.trim(v)
		if v ~= '' then
			args[k] = v
		end
	end
	
	return args
end

function p.overwrite()
	local f = mw.getCurrentFrame()
	local origArgs = f.args
	local parentArgs = f:getParent().args

	local args = {}
	
	for k, v in pairs(parentArgs) do
		v = mw.text.trim(v)
		args[k] = v
	end
	
	for k, v in pairs(origArgs) do
		v = mw.text.trim(tostring(v))
		args[k] = v
	end
	
	return args
end
-- snip
return p

The first function, p.merge(), collects args with the parent’s (template’s) args taking precedence in the case of a conflict. The second function, p.overwrite(), collects args with the child’s (invoke’s) args taking precedence in the case of a conflict. They’re named like this because I consider the first case to be the standard way to merge the two sets of args together, whereas it’s a very “violent” action to overwrite a user-supplied parameter with one from the child frame; generally the only time this should be done is if you are actually taking the user-supplied parameter into account when overwriting it.

Here’s another summary table:

Function: p.merge p.overwrite
In case of conflict: Uses parent (aka template) Uses child (aka invoke), and ignores user-specified parameters
Should be used: By default Only in special circumstances; otherwise the user may get confused
Should respect parameters: Doesn’t matter All parameters in the invoke should be reformatting the user’s parameters from the template (not an absolute rule, but a good convention)

As an example, we could decide we want a lowercase “hello world.” Consider this code at Template:HelloWorld on our sandbox wiki (notice I’m commenting my closing braces!):

<includeonly>{{#invoke:HelloWorld|main|you={{lc:{{{you}}}<!-- end lc -->}}<!-- end invoke-->}}</includeonly>

And at Module:HelloWorld:

1
2
3
4
5
6
7
local util_args = require('Module:ArgsUtil')
local p = {}
function p.main(frame)
    local args = util_args.overwrite()
    return 'hello ' .. args.you
end
return p

Now when we call {{HelloWorld|you=Arya Stark}}, overwrite function will combine the two frames’ args tables into a single args table. First it will find the parent frame, which has capitalized Arya Stark, but then it will overwrite with the child frame’s lowercased arya stark, and our output will be hello arya stark.

You may notice that I don’t pass the frame object to util_args.overwrite() here, and instead I prefer to call mw.getCurrentFrame() on its own inside the method. I’ll talk about that a bit more later on.

The standard way to do this

Just for the sake of completeness, here’s the above code using util_args.merge() instead. The merge function is the more standard one to use, so if you’re looking for copyable code for your main module, this is it.

1
2
3
4
5
6
7
local util_args = require('Module:ArgsUtil')
local p = {}
function p.main(frame)
    local args = util_args.merge()
    return 'hello ' .. args.you
end
return p

Frame methods

We’ll now switch gears completely, and talk about some of the methods on the frame object. This isn’t an exhaustive list; see the Frame object docs for complete information.

frame:callParserFunction

This method lets us interface to any parser function we want. Cool! I never remember the syntax, I just look it up every time I need it, and you should too.

If you find yourself requiring one specific parser function a lot (for example DPL might be a contender), I would suggest making a utility library just for this parser function. For example, you can see my DPL utility library on Leaguepedia. It doesn’t have to be anything fancy, but it lets you use a much more compact syntax in your main body code, which is super nice.

frame:expandTemplate

This function is really important when you’re in the process of converting a template to Lua, but it’s not great to use in a “finished product” if possible - ideally, you want ALL of your code to be in Lua, so this method shouldn’t be necessary. There are some exceptions, though; for example, on Leaguepedia I use frame:expandTemplate() to include navboxes above my automated tournament tabs. Navboxes have to be defined in templates, so there’s not really any getting around this, and I really want the navboxes to be part of the Lua code.

While you’re in the middle of converting a template to Lua, please feel free to use this method copiously! It will make your life a LOT easier because it stops the conversion process from being all-or-nothing and indeed allows your refactor to be a refactor instead of a rewrite-from-scratch. This method is awesome.

frame:extensionTag

Similar to frame:callParserFunction(), except instead of parser functions (syntax like {{#dpl:}}) it works for extension tags (syntax like <dpl>). Again, check out the docs for more info.

frame:preprocess

This method directly expands the content inside of it and should be avoided at all costs. In fact one of my “future blog post topics” is “don’t use frame:preprocess().” So I won’t go into too much detail about why you shouldn’t use it here, but the gist is that it’s extremely unsafe and hard to test or validate that anything you put inside this is accurate, and it’s also hard for other people to read or understand code that you put inside of it.

Similar to frame:expandTemplate(), this method can be extremely valuable at times when refactoring templates into modules; however, I would encourage you to stick to frame:expandTemplate() where possible.

Occasionally you may be in a situation where it’s unavoidable to use frame:preprocess(). One notable instance is if you are displaying user input, for example as I do in Module:CollapsedContent.

Best practices

That completes our introduction to what a frame is and what it does. In the last section, I’m going to go over some advice for working with frame objects.

Frame objects are “messy”

Remember our initial definition of a frame object? They’re an interface from Lua to MediaWiki etc - these things should be as encapsulated as possible. That means:

  • Don’t pass them from function to function. If you need one, grab it with mw.getCurrentFrame() (then get its parent if you need to). Very rarely is your code actually dealing with frame objects; the proper abstraction level will be args, or teams, or levels, or episodes, or what have you - the frame is completely irrelevant to the workings of your program, and your functions should not know about this crazy magical “thing.”
  • As much as possible, wrap anything that deals with frame objects in some wrapper/library/etc. Not just because the syntax of literally every frame method is bullshit annoying to remember, but also because we want to preserve this magical isolation from the complications of MediaWiki life.

Scope

Remember to scope your args! (And also every variable you define.) The local keyword is really important. local args, not just args.

Making main functions available from other modules

What if we wanted our HelloWorld main function to be available from other modules, too?

As a reminder, the last time we edited it, it looked like this:

1
2
3
4
5
6
7
local util_args = require('Module:ArgsUtil')
local p = {}
function p.main(frame)
    local args = util_args.overwrite()
    return 'hello ' .. args.you
end
return p

But other Lua modules won’t be sending a frame object, they’d just be sending an args table directly.

Something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
local util_args = require('Module:ArgsUtil')
local p = {}
function p.main(frame)
    local args = util_args.overwrite()
    return p._main(args)
end

function p._main(args)
    return 'hello ' .. args.you
end

return p

My personal convention is generally not p._main - the underscore typically signifies “private” and this isn’t really private as other modules will be accessing it, but this is a convention that some people use, so you can use it if you want. I personally prefer to give up on the name main altogether if I’m in a situation like this and call things like p.fromLua and p.fromArgs. (Another use for p.fromArgs is in contrast to p.fromCargo if I have two ways to acquire data.) It’s OK for you to pick your own convention, just be consistent! And document it! A page like Project:Lua style guide is a great thing to have!

Note that you would never, ever, ever, ever pass a frame object from one module to another; while this is technically possible to do; the code won’t crash and you could get something working, you’ll end up with confused, messy results. One entry point, one frame object, and that frame object never goes anywhere else. Only pass args.

Passing a frame object from one module to another also makes for extremely untestable code. Do not do it!!!

Should I treat an args table as read-only once it’s been created?

This one is up to you. I don’t, personally; in fact, after I create my args table, the next line of code I have is almost always h.castArgs(args), and then I do a bunch of stuff like convert text booleans to actual booleans and such. However, there are times when I do indeed need a read-only copy of my args (for example if I’m working with PageForms), and then I’ll mw.clone() my args table and work with an args2 that I modify in place. Again, be consistent with your approach, and make a Project:Lua style guide!

Conclusion

Frame objects do a lot of heavy lifting in Scribunto, and they’re pretty magical. Knowing the difference between the parent frame and child frame may be a bit confusing, and is likely to be unimportant for the first several modules you write, but at some point it will likely start to matter, and you can do a bunch of cool stuff if you leverage it properly. Keep your frame objects isolated and encapsulated as much as you possibly can, and don’t use frame:preprocess() unless you absolutely must!

Share on

river
WRITTEN BY
River
River is a developer most at home in MediaWiki and known for building Leaguepedia. She likes cats.


What's on this Page