This page looks best with JavaScript enabled

One-to-many tables & #vardefine

 ·  ☕ 12 min read

Note: Throughout this article the only content page I discuss is Dragon. If you see a reference to a different page name, it’s a template, although sometimes I have omitted the text Template: in the interest of readability. I also assume you have the #or parser function of Extension:ParserPower. See Dealing with defaults for alternatives if you don’t.

The situation

Say you have Template:Infobox Enemy (as in a gaming wiki with information about enemies you fight in the game). You want to include the enemy’s name, which usually will be the same as the page name but occasionally might be hardcoded in the case of a disambiguation page or something. So you write {{#or:{{{name|}}}|{{PAGENAME}}<!-- -->}} near the top of the infobox.

But you actually want to access this name several times over the course of the template:

  • When you display the infobox’s title
  • When you store to the Cargo table Enemies later on
  • And also when you store to Cargo in the EnemyDrops table, which can have multiple rows for each Enemy.

The third one looks something like this when you call the infobox from the page Dragon:

{{Infobox Enemy
|name=Elder Dragon
|drops={{Infobox Enemy/Drops|item=Dragon Tooth|rarity=5|chance=2}}<!--
-->{{Infobox Enemy/Drops|item=Dragon Tooth|rarity=4|chance=20}}<!--
-->{{Infobox Enemy/Drops|item=Dragon Tooth|rarity=3|chance=78}}
}}

And Template:Infobox Enemy/Drops looks something like this:

<li>{{{rarity}}}★ [[{{{item}}}]] ({{{chance}}}%)</li><!--

-->{{#if:{{NAMESPACE}}||{{#cargo_store:_table=EnemyDrops
|Enemy=<!--  how do we figure this out? -->
|Item={{{item}}}
|Rarity={{{rarity}}}
|Chance={{{chance}}}
}}<!-- end if -->}}

It’s retrieved in Template:Infobox Enemy like this:

{{#if:{{{drops|}}}|<ul>{{{drops}}}</ul><!-- end if -->}}

What we want

We’d love to do something like this at the beginning of Template:Infobox Enemy where we display our title:

{{#vardefineecho:Name|{{#or:{{{name|}}}|{{PAGENAME}}<!-- end or -->}}<!-- end vde -->}}

And then later on at Template:Infobox Enemy/Drops we would write:

|Enemy={{#var:Name}}

Why we can’t

But this doesn’t work; notice that the calls to Infobox Enemy/Drops are inside the call to Infobox Enemy. When we evaluate this page, the parsing order looks like this:

  1. Evaluate each of the args of Infobox Enemy/Drops
  2. Send these pre-computed args to Infobox Enemy/Drops
  3. Evaluate the logic of Infobox Enemy

Since there’s a #var: call in Infobox Enemy/Drops with a #vardefine: call in Infobox Enemy, that means that the #var: evaluates before the #vardefine:, and our code doesn’t work.

Oh no! How can we fix this?

Solution 1: Force the vardefine to evaluate earlier

We can introduce a new template, called SetName with this logic:

{{#vardefineecho:Name|{{#or:{{{1|}}}|{{PAGENAME}}<!-- end or -->}}<!-- end vde -->}}

Then, at Dragon we can write this:

{{Infobox Enemy
|name={{SetName|Elder Dragon}}
|drops={{Infobox Enemy/Drops|item=Dragon Tooth|rarity=5|chance=2}}<!--
-->{{Infobox Enemy/Drops|item=Dragon Tooth|rarity=4|chance=20}}<!--
-->{{Infobox Enemy/Drops|item=Dragon Tooth|rarity=3|chance=78}}
}}

Now, the order of operations will be:

  1. Evaluate the call to Template:SetName (here is where the #vardefine happens)
  2. Evaluate all of the calls to Template:Infobox Enemy/Drops (here is where the #var happens)
  3. Send all these params to Template:Infobox Enemy
  4. Evaluate Template:Infobox Enemy (there is another #var call here when you store to the Enemies table)

And it works. Great!

Solution 2: Force the vardefine to evaluate earlier (v2)

Alternatively, we could make Template:SetName do just that: set the name, and nothing else. In this case, our logic at Template:SetName would be this:

{{#vardefine:Name|{{#or:{{{1|}}}|{{PAGENAME}}<!-- end or -->}}<!-- end vardefine -->}}

Notice that we are using #vardefine and not #vardefineecho.

Now, at Dragon we can write this (notice there is no name parameter):

{{SetName|Elder Dragon}}{{Infobox Enemy
|drops={{Infobox Enemy/Drops|item=Dragon Tooth|rarity=5|chance=2}}<!--
-->{{Infobox Enemy/Drops|item=Dragon Tooth|rarity=4|chance=20}}<!--
-->{{Infobox Enemy/Drops|item=Dragon Tooth|rarity=3|chance=78}}
}}

Then at Template:Infobox Enemy instead of printing {{{name|}}} as the title, we print {{#var:Name}} as the title.

The logic goes in this order:

  1. Evaluate SetName (here is where the #vardefine happens)
  2. Move on to evaluating Infobox Enemy, starting with evaluating its arguments
  3. During this evaluation, evaluate Infobox Enemy/Drops (here is where the #var happens)
  4. Evaluate Infobox Enemy (there is another #var call here)

This also works.

Solution 3: Use Lua for this parameter

Alternatively, we could use Lua, using a multi-instance subtemplates pattern.

So at Template:Infobox Enemy/Drops, we would put something like this:

{{{item|}}};;;{{{rarity|}}};;;{{{chance|}}}:::

Then at Template:Infobox Enemy, instead of displaying {{#if:{{{drops|}}}|<ul>{{{drops|}}}</ul>}} we would write {{#invoke:InfoboxEnemyDrops|main|{{{drops|}}}<!-- end invoke -->}}.

Here’s the Lua code (note, I am using the legacy version of a #cargo_store here; in the most recent Cargo version there is now a native Lua cargo_store method):

 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
48
49
50
local ARGS_ORDER = { 'Item', 'Rarity', 'Chance' }
local OUTERSEP = '%s*:::%s*'
local INNERSEP = '%s*;;;%s*'

local p = {}
local h = {}
p.h = h

function p.main(frame)
    local arg = mw.text.trim(frame.args[1] or frame:getParent().args[1])
    if not arg then return end
    local args = h.splitArgsArray(arg)
    local out = mw.html.create('ul')
    for _, row in ipairs(args) do
        row._table = 'EnemyDrops'
        row[1] = ''
        -- here is where we retrieve the Name variable for the Name field in the cargo_store
        row.Enemy = frame:callParserFunction('#var', { 'Name' })
        frame:callParserFunction('#cargo_store', row)
        out:tag('li')
            :wikitext(('%s★ [[%s]] (%s%%)'):format(row.Rarity, row.Item, row.Chance))
    end
    return out
end

function h.splitArgsArray(input)
	if not input or input == '' then return {} end
    local tbl = mw.text.split(input, OUTERSEP)
    for i, v in ipairs(tbl) do
        tbl[i] = h.splitArgs(v)
    end
	return tbl
end

function h.splitArgs(input)
	if not input or input == '' then return end
	local result = {}
	local inputTbl = mw.text.split(input, INNERSEP)
	for i, v in ipairs(ARGS_ORDER) do
		if not inputTbl[i] then
			error(('Missing parameter %s - maybe wrong child template?'):format(v))
		end
		if inputTbl[i] ~= '' then
			result[v] = inputTbl[i]
		end
	end
	return result
end

return p

This Lua code is a very minimal example; were I to implement this in a live wiki, the functions splitArgsArray and splitArgs would be part of Module:ArgsUtil, I’d have a Cargo wrapper instead of calling frame:callParserFunction directly in this code, and I’d be using a generic merge function to extract arg from a merged frame.args (args to the invoke) & frame:getParent().args (args to the template calling the invoke). The line p.h = h aids testing in the Scribunto console.

Note also that local ARGS_ORDER = { 'Item', 'Rarity', 'Chance' } at the top of the page must contain the names of the fields as expected by the Cargo table EnemyDrops. The order must be the same as the order in which we provide the parameters at Template:Infobox Enemy/Drops.

At the page Dragon when we create our infobox on a content page, we can now write:

{{Infobox Enemy
|name=Dragon
|drop={{Infobox Enemy/Drops|item=Dragon Tooth|rarity=5|chance=2}}<!--
-->{{Infobox Enemy/Drops|item=Dragon Tooth|rarity=4|chance=20}}<!--
-->{{Infobox Enemy/Drops|item=Dragon Tooth|rarity=3|chance=78}}
}}

Template:Infobox Enemy/Drops is as defined above, and Template:Infobox Enemy displays {{#invoke:InfoboxEnemyDrops|main|{{{drops|}}}<!-- end invoke -->}}. Assuming you have properly declared the Cargo table EnemyDrops, this code will work.

In this setup, here is the order in which things happen:

  1. We call to Infobox Enemy, evaluating the args first
  2. Args evaluation starts
  3. Infobox Enemy/Drops is parsed, outputting {{{item|}}};;;{{{rarity|}}};;;{{{chance|}}}::: and not using any variables
  4. Evaluation of the source at Infobox Enemy starts
  5. The #vardefine happens when we print the title at the top of the infobox ({{#vardefineecho:Name|{{#or:{{{Name|}}}|{{PAGENAME}}<!-- end or -->}}<!-- end vde -->}})
  6. Continuing execution of the code at Infobox Enemy, we evaluate {{#invoke:InfoboxEnemyDrops|main}} with the plaintext that we generated in step 3
  7. Lua execution of Module:InfoboxEnemyDrops begins
  8. During this execution, we frame:callParserFunction('#var') to retrieve the value of Name
    • Note that using the extension VariablesLua is equivalent to this callParserFunction
  9. With the value of #var:Name safely retrieved, we now store to Cargo.

Solution 4: Use Lua for everything

If you are comfortable with Lua, you can create your entire infobox in Lua. The way you do this may vary wildly from wiki to wiki, so I won’t discuss this other than to note that it’s possible.

Solution 5: Like the Lua method, but with wikitext

This method is a little bit for funsies, I do not recommend it. It’s similar in philosophy to the Lua example, but it’s worse. It depends on Template:Counter being defined like this:

{{#if:{{{get|}}}
    |{{#var:counter-{{{1|}}}<!-- end var -->}}
    |{{#vardefineecho:counter-{{{1|}}}|{{#expr:{{#var:counter-{{{1|}}}|0<!-- end var -->}}+1<!-- end expr -->}}<!-- end vde -->}}
<!-- end if -->}}

This template allows us to write something like this:

# {{counter|enemydrops}}
# {{counter|enemydrops}}
# {{counter|enemyabilities}}
# {{counter|enemydrops}}
# {{counter|enemydrops|get=yes}}

Which will output a list like this:

  1. 1
  2. 2
  3. 1
  4. 3
  5. 3

In other words, we get the ability to make multiple counters that start at 1 and continue upwards.

The other important concept is that if we write {{#var:Item{{counter|enemydrops|get=yes}}<!-- end var -->}}, and {{counter|enemydrops|get=yes}} is currently 3, we’ll output the value of {{#var:Item3}}.

So, let’s make Template:Infobox Enemy/Drops with this code:

{{#vardefine:Item{{Counter|ed}}|{{{item|}}}<!-- end vardefine -->}}<!--
-->{{#vardefine:Rarity{{Counter|ed|get=yes}}|{{{rarity|}}}<!-- end vardefine -->}}<!--
-->{{#vardefine:Chance{{Counter|ed|get=yes}}|{{{chance|}}}<!-- end vardefine -->}}

At our page Dragons, we will still write:

|drops={{Infobox Enemy/Drops|item=Dragon Tooth|rarity=5|chance=2}}<!--
-->{{Infobox Enemy/Drops|item=Dragon Tooth|rarity=4|chance=20}}<!--
-->{{Infobox Enemy/Drops|item=Dragon Tooth|rarity=3|chance=78}}

The result of this will now be that nothing is output, but the following variables are defined:

  1. #var:Item1 is Dragon Tooth
  2. #var:Rarity1 is 5
  3. #var:Chance1 is 2
  4. #var:Item2 is Dragon Tooth
  5. #var:Rarity2 is 4
  6. #var:Chance2 is 20
  7. #var:Item3 is Dragon Tooth
  8. #var:Rarity3 is 3
  9. #var:Chance3 is 78

Now, at our infobox, when we display {{{drops|}}}, we’ll also display the contents of a template designed to evaluate the variables we just defined. So we’ll write {{{drops|}}}{{DisplayEnemyInfoboxDrops}}.

Let’s now write Template:DisplayEnemyInfoboxDrops. We’ll need to use the Loops extension.

<ul>{{#while:
  | {{#var:Item{{Counter|ed2}}<!-- end var -->}}<!-- 
  as soon as this evaluates to an empty string, exit the loop. 
  in our example that will happen when `counter|ed2` gets to 4, since `#var:Item4` is not defined.
  -->
  | <li>{{#var:Rarity{{Counter|ed2|get=yes}}<!-- end var -->}}★ <!--
  -->[[{{#var:Item{{Counter|ed2|get=yes}}<!-- end var -->}}]] <!--
  -->({{#var:Chance{{Counter|ed2|get=yes}}<!-- end var -->}}%)</li>{{#if:{{NAMESPACE}}||
    {{#cargo_store:
      _table=EnemyDrops
      |Name={{#var:Name}}
      |Rarity={{#var:Rarity{{Counter|ed2|get=yes}}<!-- end var -->}}
      |Item={{#var:Item{{Counter|ed2|get=yes}}<!-- end var -->}}
      |Chance={{#var:Chance{{Counter|ed2|get=yes}}<!-- end var -->}}
    }}
  }}
}}</ul>

Another equivalent way to write this is:

<ul>{{#while:
  | {{#var:Item{{Counter|ed2}}<!-- end var -->}}<!-- 
  as soon as this evaluates to an empty string, exit the loop. 
  in our example that will happen when `counter|ed2` gets to 4, since `#var:Item4` is not defined.
  -->
  | <li>{{#vardefineecho:Rarity|{{#var:Rarity{{Counter|ed2|get=yes}}<!-- end var -->}}<!-- end vde -->}}★ <!--
  -->[[{{#vardefineecho:Item|{{#var:Item{{Counter|ed2|get=yes}}<!-- end var -->}}<!-- end vde -->}}]] <!--
  -->({{#vardefineecho:Chance|{{#var:Chance{{Counter|ed2|get=yes}}<!-- end var -->}}<!-- end vde -->}}%)</li>{{#if:{{NAMESPACE}}||
    {{#cargo_store:
      _table=EnemyDrops
      |Name={{#var:Name}}
      |Rarity={{#var:Rarity}}
      |Item={{#var:Item}}
      |Chance={{#var:Chance}}
    }}
  }}
}}</ul>

With this method, here is the order of evaluation:

  1. We start evaluating each of the arguments sent to Infobox Enemy
  2. When we evaluate |drops=, we save a bunch of variables, indexed by the counter ed which is incremented by 1 for each instance of Infobox Enemy/Drops
  3. We start evaluating the code at Infobox Enemy
  4. At the top of the template, we display the title & save a value for #var:Name
  5. We get to the call to DisplayEnemyInfoboxDrops
  6. We begin a loop
  7. We start a new counter named ed2 which will start out at 1 and increase for each iteration of the loop
  8. At each iteration, we print the <li> item and store the appropriate Cargo, recalling back to #var:Name for the Name field of the table

One small thing to note is that we never actually need to print anything with the parameter {{{drops|}}}. If there’s a possibility that an enemy has no drops, and we don’t want to output anything from this parameter, we might want to make Template:Infobox Enemy/Drops output something, say a . after all the #vardefines. And then at Infobox Enemy, instead of saying {{{drops}}}{{DisplayEnemyInfoboxDrops}}, we could write:

{{#if:{{{drops|}}}|{{DisplayEnemyInfoboxDrops}}<!-- end if -->}}

Then we won’t get an empty <ul></ul> printed in the event that there are no drops.

Solution 6: A similar template option, but with arraymaptemplate

Instead of using a loop, this time we’ll use the parser function arraymaptemplate, provided by Extension:Page Forms. If you only need to store 1 or 2 params alongside Name in your Cargo table, this solution is pretty reasonable, but if you have a lot of parameters there, it gets out of hand pretty fast.

We’re going to use the same Template:Infobox Enemy/Drops as we did for the Lua example, namely:

{{{item|}}};;;{{{rarity|}}};;;{{{chance|}}}:::

And at Dragon, when we build the infobox, we will still write:

|drops={{Infobox Enemy/Drops|item=Dragon Tooth|rarity=5|chance=2}}<!--
-->{{Infobox Enemy/Drops|item=Dragon Tooth|rarity=4|chance=20}}<!--
-->{{Infobox Enemy/Drops|item=Dragon Tooth|rarity=3|chance=78}}

In our infobox, when we display this information, we’ll now write:

{{#if:{{{drops|}}}|
  <ul>{{#arraymaptemplate:{{{drops}}}|DisplayEnemyInfoboxDrops|:::|}}</ul>
}}

And at Template:DisplayEnemyInfoboxDrops, we’ll write this:

{{#vardefine:item|{{#explode:{{{1}}}|;;;|0}}<!-- end vardefine -->}}<!--
-->{{#vardefine:rarity|{{#explode:{{{1}}}|;;;|1}}<!-- end vardefine -->}}<!--
-->{{#vardefine:chance|{{#explode:{{{1}}}|;;;|2}}<!-- end vardefine -->}}<!--
--><li>{{#var:rarity}}★ [[{{#var:item}}]] ({{#var:chance}}%)</li>{{#if:{{NAMESPACE}}||
  {{#cargo_store:
    _table=EnemyDrops
    |Enemy={{#var:Name}}
    |Item={{#var:item}}
    |Rarity={{#var:rarity}}
    |Chance={{#var:chance}}
  }}
<!-- end if -->}}

Solution 7: Abusing frame:preprocess

The final solution I will present is arguably the easiest to implement, and it’s also arguably the worst. It’s an example of being very clever to your own detriment because you make extremely confusing code that no one will be able to maintain in the future.

Remember that in the beginning, what we wanted to write at Template:Enemy Infobox/Drops was this:

<li>{{{rarity}}}★ [[{{{item}}}]] ({{{chance}}}%)</li><!--

-->{{#if:{{NAMESPACE}}||{{#cargo_store:_table=EnemyDrops
|Enemy=<!--  how do we figure this out? -->
|Item={{{item}}}
|Rarity={{{rarity}}}
|Chance={{{chance}}}
}}<!-- end if -->}}

Let’s change that a bit; we will instead write:

<li>{{{rarity}}}★ [[{{{item}}}]] ({{{chance}}}%)</li><!--

-->{{#if:{{NAMESPACE}}||{{((}}#cargo_store:_table=EnemyDrops
{{!}}Enemy={{((}}#var:Name{{))}}
{{!}}Item={{{item}}}
{{!}}Rarity={{{rarity}}}
{{!}}Chance={{{chance}}}
{{))}}<!-- end if -->}}

At Template:(( we write {{ and at Template:)) we write }}.

If we just evaluate this straight-up, we’ll get something slightly disgusting that is including as literal text:

{{#cargo_store:_table=EnemyDrops |Enemy={{#var:Name}} |Item=Dragon Tooth |Rarity=5 |Chance=2 }}

This is most definitely not what we want, but let’s write a very quick Lua module which we’ll call Module:Preprocess:

local p = {}
function p.main(frame)
    return frame:preprocess(frame.args[1] or frame:getParent().args[1])
end
return p

And then instead of printing {{{drops|}}} in Enemy Infobox, we print:

{{#if:{{{drops|}}}|{{#invoke:Preprocess|main|{{{drops}}}<!-- end invoke -->}}<!-- end if -->}}

And voila, that plaintext cargo_store that we generated above is preprocessed by Lua and everything “just works”!

Invalid solution: var_final

You might be wondering why I didn’t suggest to write this at Template:Enemy Infobox/Drops

<li>{{{rarity}}}★ [[{{{item}}}]] ({{{chance}}}%)</li><!--

-->{{#if:{{NAMESPACE}}||{{#cargo_store:_table=EnemyDrops
|Enemy={{#var_final:Name}} <!-- use the var_final parser function here -->
|Item={{{item}}}
|Rarity={{{rarity}}}
|Chance={{{chance}}}
}}<!-- end if -->}}

Well, if we only wanted to display #var:Name, this could actually work. But let’s try a really minimal example with a #cargo_store:

{{#vardefine:Name|dragon}}{{#cargo_store:_table=EnemyDrops|Enemy={{#var_final:Name}}}}

Here’s what happens:

The #cargo_store will evaluate before the Variables extension inserts the final value of #var_final:Name, and everything breaks.

Which method should you use?

The answer depends a lot on your comfort level with writing wiki code (and with lua code), and on how much burden of knowledge you want to give your editors.

My true recommendation is to do everything in Lua; if you’re comfortable with Lua you should do that. If not, then probably options 1 and 2 are the best. But if you have deep concerns that your editors won’t understand what’s going on with that extra SetName template, I might opt for either the arraymaptemplate option or the frame:preprocess version, whichever one makes more sense to you. If they both make equal sense, arraymaptemplate is probably a bit safer.

Happy coding, and try not to abuse the MediaWiki parser too much!

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