Now that we have a few components, we need some convenient ways to manipulate them. Components are just nodes in the APL DOM with a Name
property. Note the uppercase N
, not to be confused with the HTML name
attribute. Currently the existence of Name
in an element is the only thing that identifies it as a component.
We can get a component by name, using the GetComponent
function:
GetComponent←{
⍝ ⍺ ←→ APL DOM node
⍝ ⍵ ←→ Component Name (or already a component (pass through)
9=⎕NC'⍵':⍵
e←⍺ GetElementsWith'Name'
i←e.Name⍳⊂⍵
i=≢e:6 ⎕SIGNAL⍨'Component "',⍵,'" not found'
i⊃e
}
The left argument is a DOM node, perhaps the whole document, or more likely a <dialog>
element. the right argument is the name of the component, or if a namespace, assumed to be a component already, and just passed though. This is a convenience for other functions that accept either a component or a component name as an argument. We simply find all the elements in the node with a Name
property and pick the one we want.
We can get a namespace with all of the components in a node:
GetComponents←{
⍝ ⍵ ← APL DOM node
⍝ ⍺ ←→ 0 for namespace result, 1 for array
⍝ ← ←→ Namespace of components
⍺←0
e←⍵ GetElementsWith'Name'
⍺:e
0=≢⍵:()
()⎕VSET(↑e.Name)e
}
This allows us to access all the components under a node by name with dot syntax (or get an array of components back).
We can set the value of a component using SetComponentValue
:
SetComponentValue←{
⍝ ⍺ ←→ APL DOM Node
⍝ ⍵ ←→ (Name|Component) Value
⍺←⊢
c←⍺ GetComponent 0⊃⍵
c(⍎c.class).SetValue 1⊃⍵
}
Here the right argument is name or component itself, and the value. If the name is provided, then the left argument must be provided. If the component is provided, the left argument should be omitted. There is no need for a corresponding GetComponentValue
function, as the value may be directly accessed with dot syntax.
We also have GetComponentValues
and SetComponentValues
, which we have introduced before but now renamed to reflect that they work on components. These functions take or return a namespace of values. Finally we have SetComponentDisabled
for making a component, in ⎕WC
terms, active or inactive:
SetComponentDisabled←{
⍝ ⍺ ←→ DOM node
⍝ ⍵ ←→ Name or Component name, Boolean 1/0
⍺←⊢
c←⍺ GetComponent 0⊃⍵
e←Elements c
d←e/⍨e.Tag∊'button' 'fieldset' 'optgroup' 'option' 'select' 'textarea' 'input'
0=≢d:0
0⊣d{⍺ SetBoolean'disabled'⍵}¨1⊃⍵
}
The explicit list of HTML elements are those that take the disabled
attribute.
Let's play around with the functions. Consider a dialog box with a few input fields:
OnFileSettings←{
d←⍵.Document
s←A.FieldSet.New''
i←s A.DropList.New'Separator' 'Separator:' ''('Comma' 'Pipe' 'Tab')
i←s A.DropList.New'TextQualifier' 'Text qualifier:'('Options'('DoubleQuote' 'Quote')
i←s A.NumberInput.New'MaxRows' 'Maximum Rows:' 256 'CI15'
i←s A.CheckBox.New'AutoConvert' 'Auto convert'
_←d.FileSettings A.InitValues s
p←⎕NS''
p.Title←'CSV Import Settings'
p.Contents←s
p.OnOK←A.FQP'OnFileSettingsOK'
b←A.DialogBox.New p
d A.ShowModal b
}
When the OnFileSettingsOK
callback fires we are given the <dialog>
element as an argument. Let's call this d
. Then we can grab a component and see its value:
c←d A.GetComponent 'Separator'
c.Value
Pipe
Then set its value, updating the APL DOM and the browser:
A.SetComponentValue c 'Comma'
0
c.Value
Comma
A namepsace of components:
A.GetComponents d
p←A.GetComponents d
p.MaxRows.Value
1024
p.AutoConvert.Value
1
Get a namespace of values, make some changes, refresh the screen:
v←A.GetComponentValues d
⎕JSON v
{"AutoConvert":1,"MaxRows":1024,"Separator":"Comma","TextQualifier":"DoubleQuote"}
v.(AutoConvert MaxRows)←1 1234567
v A.SetComponentValues d
0
v←A.GetComponentValues d
⎕JSON v
{"AutoConvert":0,"MaxRows":1234567,"Separator":"Pipe","TextQualifier":"Quote"}
An array of components:
a←1 A.GetComponents d
a.Name
Separator TextQualifier MaxRows AutoConvert
a.Value
Pipe Quote 1234567 0
Deactivate all the components:
A.SetComponentDisabled¨a,¨1
0 0 0 0
We have used these verbose functions names, all including the word component, to distinguish them from the functions used to operate on plain elements, as everything sits all together in the #.Abacus.Main
namespace. It would be nice to just use properties with real classes and dispense with exposing all these functions, and we may do that in the future. We are going to see how many functions we will need to work with components over the coming months.
In APL we can have at most two arguments to a function, ⍺
and ⍵
, left and right.
This is good, because we should never have more than two argument to a function. In fact that is usually one too many. However real life gets in the way, and we often find ourselves in need of more. With nested arrays, namespaces, and simply the interpretation of, say, a simple numeric vector of length 3, the meaning of "one" is in the eye of the beholder.
If we have a vector of expenses as the right argument to a functions that sums them up, we say the right argument, ⍵
, is a vector of expenses. It's one thing. On the other hand, if we have a function that computes the level payment of a mortgage, it takes a term, a balance, and a rate. Three things. If we pass these as the right argument ⍵
, we say the function takes three arguments. This is not strictly true. It takes one argument, a vector of three items. Informally we might often speak of 3, 4, 5 or more arguments but we really mean, usually, are distinct items of a possibly nested vector.
Let's use the word parameter to refer to an item of the argument when the argument consists of unique, identifible, nameable, elements.
In our hypothetical mortgage function the first line might look like:
(b r t)←⍵
Here we have unpacked the argument ⍵
into 3 parameters b
, r
and t
.
Often we want to have some of the less important parameters be optional and default to a given value. To make the term optional and default to 360 we might do:
(b r t)←3↑⍵,360
With two or three parameters this technique is manageable, but with more it becomes unwieldy. First, we get a proliferation of usually ad hoc named local variables. Second we can only default trailing parameters.
What can we do about this?
We can take a page from the design of Dyalog's venerable ⎕WC
and use named parameters. This is what we have done in Abacus for components. This technique is useful for the pulbic API of libraries that will be used by other programmers. It's probably overkill for private functions.
This technique has many benefits:
- Parameters are formally named, yielding better documentation and clearer code.
- Parameter names are optional on a parameter by parameter basis.
- A namespace may be provided as the argument, containing the parameters (an improvement over
⎕WC
).
- Every parameter may have a default value. We can even provide no parameters at all.
- Local parameter names are encapsulated in a namespace so less name clutter in the function.
We take a strict approach to a vector argument: the tally is always the number of parameters provided. Therefore when providing only one parameter it usually must be enclosed, the exception being a scaler valued parameter provided with no name.
Consider this contrived example:
Sum←{
p←(
'One' 1
'Two' 2
'Three' 3
'Four' 4
'Five' 5
)Default ⍵
+/p.(One Two Three Four Five)
}
First we layout all the parameters, each on its own line, in order, with their default values, using V20 array notation. It is important that we use an array rather than a namespace for the default values, as order matters. This array is passed as the left argument to our Default
utility function, which takes the user provided parameters as its right argument:
Default←{
⍝ ⍺ ←→ Default name/value pairs
⍝ ⍵ ←→ Given argument
⍝ ← ←→ A new space with ⍺ overiddden by ⍵
d←()⎕VSET ⍺
9=⎕NC'⍵':d ⎕NS ⍵
n←' '~⍨¨(≢⍵)↑⊃¨⍺
p←n{(2=≢⍵)∧1≠≡⍵:⍵
⍺≡'':⍺
⍺ ⍵}¨⍵
d ⎕NS()⎕VSET p~⊂''
}
The first thing we do is create a new namespace d
with all of the default name/value pairs ⍺
. This will be our result in all cases.
Then, if we are given a namespace as the right arg, we inject the user supplied names over the default names, and return the space d
; we are done. (This uses another nice feature of V20, which finally allows a reference as the left argument to ⎕NS
.) Otherwise ⍵
is a vector where each item is either a value or a name/value pair. If an item is only a value, we assume the name based on its position. These names are then injected into d
, overriding defaults, and d
is returned.
We can call the Sum
function in all of the following ways (We have inserted ⎕←⎕JSON p
to see what is going on):
Sum ''
{"Five":5,"Four":4,"One":1,"Three":3,"Two":2}
15
Sum 100 ('Five' 50)
{"Five":50,"Four":4,"One":100,"Three":3,"Two":2}
159
Sum ('Three' 333) ('DoesNot' 'Exist')
{"DoesNot":"Exist","Five":5,"Four":4,"One":1,"Three":333,"Two":2}
345
Sum (Three:333 ⋄ Two:222)
{"Five":5,"Four":4,"One":1,"Three":333,"Two":222}
565
This Default
function does no error checking. Mispelled names (not useful) or additional names (often useful) are happily are accepted. There is no type checking, and no checking for whether or not a parameter is optional. This is fine for the purposes of Abacus, where we assume consenting adults are using the library.
Some checking can be done from the name alone. But if we want to check for mandatory parameters or types, we need to add more items to our default vector. To check for mandatory parameters we can add an optional Boolean:
p←(
'One' 1 1
'Two' 2 1
'Three' 3
'Four' 4
'Five' 5
)
Here we have specified that parameters One
and Two
are required. Now we need to add some code to the default function to do some checking. We take the approach of adding a new function, rather than mucking up our existing code too much:
Default←{
⍝ ⍺ ←→ Default name/value pairs (Optional 1 for required)
⍝ ⍵ ←→ Given argument
⍝ ← ←→ A new space with ⍺ overiddden by ⍵
d←()⎕VSET 2↑¨⍺
9=⎕NC'⍵':d ⎕NS ⍺ Verify ⍵
n←' '~⍨¨(≢⍵)↑⊃¨⍺
p←n{(2=≢⍵)∧1≠≡⍵:⍵
⍺≡'':⍺
⍺ ⍵}¨⍵
d ⎕NS ⍺ Verify()⎕VSET p~⊂''
}
where the Verify
function is:
Verify←{
(n v r)←↓⍉↑3↑¨⍺,¨0
m←⍵.⎕NL ¯2
~∧/m∊⍨r/n:11 ⎕SIGNAL⍨'Required parameter: ',⊃r/n
0≠≢m~n:11 ⎕SIGNAL⍨'Invalid parameter name: ',⊃m~n
⍵
}
Additional type information could be specified and checked for. Here we begin to run into meta problem of needing parameter names for our parameter specification. Ugh. Let's stay away from that.
Now that we have a little framework for handling arguments, and have used it for our <input>
components, we should probably redesign the Menu
component. Let's get rid of the current Add
function:
Add←{
m←⍺
9=⎕NC'⍵':m AddSubmenu ⍵
v←⍵,(≢⍵)↓'' '' '' 0 1 0
cm←(5⊃v)/⎕UCS 10004
c←A.New¨{'span'⍵}¨cm(0⊃v)(1⊃v)''
i←m A.New'li'(A.New'div'c)
i.(Label Shortcut Function Separator Active Checked)←v
i.Function←1 A.FQP i.Function
i.Type←'Item'
i.tabindex←'0'
i.Selected←i=⊃m.Content
i.class←i.Selected/'sel-item'
i.class,←(~i.Active)/' inactive'
i.class,←i.Separator/' separator'
i
}
This function adds a menu item or a (sub)menu to a menu. Instead, let's just have an AddItem
function to add a menu item, essentially creating a MenuItem
component but keeping it all under the Menu
namespace:
NewItem←{
i←(⍺ A.New'li')⎕NS(
'Label' ''
'Shortcut' ''
'Function' ''
'Separator' 0
'Active' 1
'Checked' 0
)A.InitProps ⍵
cm←i.Checked/⎕UCS 10004
c←A.New¨{'span'⍵}¨cm i.Label i.Shortcut''
i.Content←A.New'div'c
i.Function←1 A.FQP i.Function
i.Type←'Item'
i.tabindex←'0'
i.Selected←i=⊃⍺.Content
i.class←i.Selected/'sel-item'
i.class,←(~i.Active)/' inactive'
i.class,←i.Separator/' separator'
i
}
Much nicer! Clearer to read and see the defaults, much more flexible to call. The Menu.New
function can take over adding a (sub)menu to a menu:
New←{
⍺←0
m←(A.New'menu')⎕NS(
'Label' ''
'Separator' 0
)A.InitProps ⍵
m.Content←m.Label
m.class←'menu'
m.popover←'auto'
m.Toggled←0
0=⍺:m ⍝ Top Level menu
b←'>'
c←A.New¨{'span'⍵}¨''(m.Content)''(⊂⊂b)
i←⍺ A.New'li'(A.New'div'(c,m))
i.Type←'Menu'
i.tabindex←0
i.class←m.Separator/'separator'
i.Selected←0
m.Content←''
m
}
~~~
Now we can build a menu like so:
BuildMenu←{
NewMenu←A.Menu.New
NewItem←A.Menu.NewItem
m←NewMenu''
s←m NewMenu⊂'File'
i←s NewItem'Open' 'Ctrl+O' 'OnFileOpen'
i←s NewItem'Save' 'Ctrl+S' 'OnFileSave'
i←s NewItem'Save As...' 'Ctrl+A' 'OnFileSaveAs'
s←m NewMenu⊂'Edit'
i←s NewItem'Cut' 'Ctrl+X' 'OnCut'
i←s NewItem'Copy' 'Ctrl+C' 'OnCopy'
i←s NewItem'Paste' 'Ctrl+V' 'OnPaste'
s←m NewMenu⊂'View'
i←s NewItem'List' 'Ctrl+Q' 'OnViewList'
s2←s NewMenu⊂'Icons'
i←s2 NewItem'Small' 'Ctrl+L' 'OnViewLarge'
i←s2 NewItem'Medium' 'Ctrl+M' 'OnViewMedium' ('Checked' 1)
i←s2 NewItem'Large' 'Ctrl+S' 'OnViewSmall' ('Active' 0)
m
}