Magic Metal Productions Blog

HTML Helper Functions

HTML Helper Functions
 

Programming for Miva Merchant involves creating a lot of menus, checkboxes, and other form elements for user input. For many years, my most-used piece of MM documentation was the page that describes the functions DrawCheckbox and DrawRadio. Somehow I could never remember the parameter order for these functions. They're just different enough that I always had to refresh my memory on which one is written which way.

Eventually, I wrote my own "HTML helper" functions, and made the parameters as consistent as I could, given the inherent differences among the various types of inputs. For each function, the name parameter, if any, comes first, followed by the value or content-holding parameter, and then the "status" parameter that determines the current state of the input, such as checked/unchecked. Some functions don't need all three of these, but the order remains the same for whichever ones are present: name, value, status. After these come less-common parameters that are only used by one or two functions. As long as I can remember "name, value, status," I can write any of these functions correctly without looking at a document to get the parameter order.

All the functions have an additional, final parameter for custom HTML attributes, such as classes or event handlers. Since these aren't used very often, I also created shortcut versions of each function, with shorter names and one less parameter.

While I was at it, I threw in a few other enhancements. I wrote functions for creating more input types such as text boxes and dropdown menus. I wrote them to pass the generated HTML code to the caller as their return value; they don't contain any MvEVAL's or in-line HTML. This makes the code more concise, and makes it easy to store the HTML in a variable for later use, or write several of the function calls into a single string expression.

The complete set of function names and parameters is:

Long version Short version
Checkbox(name, value, status, attrs)

RadioButton(name, value, status, attrs)

Option(value, status, prompt, attrs)

DropdownMenu(name, options, attrs)

TextBox(name, value, rows, columns, attrs)

PasswordBox(name, value, columns, attrs)
Chkbx(name, value, status)

Radio(name, value, status)

Opt(value, status, prompt)

Menu(name, options)

TxtBx(name, value, rows, columns)

PassBx(name, value, columns)

The parameter usages are similar to the functions from the MM utility library; you should find them familiar and easy to work with.

Parameter Function
name The name for the HTML form element.
value The data or content that the form element will send to the server when the form is submitted.
status A value that determines the current state of the form element, such as checked or unchecked. Radio-button and menu-options elements will be selected if the status parameter is equal to the value parameter. Checkbox elements will be checked if status is "true," i.e. anything other than zero or the empty string.
prompt Text that is displayed to users on the menu option.
options HTML code for one or more menu options, possibly created by the Option() or Opt() functions. (See example below)
rows The number of rows to display for a multi-line text box (textarea).
columns The number of columns (characters) to display for a text box (single-line, multi-line, or password).
attrs Additional HTML attributes to be included in the form element, such as CSS classes, Javascript event handlers, etc.

None of these functions have a parameter for an optional text caption or label, as the MM functions do. I've found with experience that I usually prefer to write the caption separately; it gives me more flexibility about the layout. Also, nowadays, the caption is likely to include a <span> or <label> tag, which will have attributes with quotes. Writing that as a function parameter requires adding another layer of quotes, with backslashes to escape the nested quotes. That's too much typing for my taste, and too many chances for a mis-typed character to produce a weird syntax error for which the compiler will give a misleading error message.

For example, writing a basic checkbox with the helper function comes out about like this:

<MvEVAL EXPR="{ Chkbx('XXXX_enabled', 1, g.XXXX_enabled) }"> Click me

If you want to add an event handler to the checkbox, you can write it with the long-form function call that has one more parameter:

<MvEVAL EXPR="{ Checkbox('XXXX_enabled', 1, g.XXXX_enabled, 'onclick=\"DoSomethingWith(this);\"') }">

Here's another example of using these functions to simplify coding, in this case for a dropdown menu:

<MvEVAL EXPR="{ Menu('XXXX_color',
Opt('R', g.XXXX_color, 'Red') $ Opt('G', g.XXXX_color, 'Green') $ Opt('B', g.XXXX_color, 'Blue') ) }">

I hope these functions will help you with your projects. I should mention that, although I've been using them for some time, I haven't tested them exhaustively; there may still be a few bugs to be found.


A new Miva Script sorting function

A new Miva Script sorting function
 

From time to time in my work, I run into a project where I need to sort some data in memory, instead of relying on a database index to do the sorting for me. I've written a few different sorting functions over the years; and so have a lot of other people, according to this article at MivaScript.com. Recently I came up with a function that I thought was worth sharing. It has a number of useful features.

The call format is:

MultiSort(source_array, colspecs, direction)

For example, suppose you have an array containing data about customers that's been read from the store's s01_Customers table; and you want to sort it in the classic phone-book order, by last name and first name. You can write a call such as:

<MvASSIGN NAME="l.sorted" VALUE="{ MultiSort(l.customers, 'bill_lname, bill_fname', '') }">

The colspecs parameter specifies the two-column sort by last name and first name of the billing address.

The function does alphabetic sorting by default. If a column contains numeric data, you indicate this by adding a suffix to the column name. For example, you may have an array of data about completed orders, and you want to sort it by the total amount purchased, with the largest orders coming first:

<MvASSIGN NAME="l.sorted" VALUE="{ MultiSort(l.orders, 'total#10', 'D') }">

In this case, the direction parameter contains a D to specify descending order (highest values first). The column name ends with a # character, followed by a number. The number specifies how many digits there are in the largest value that will be sorted. You need to enter this so that the function can pad the numbers wih leading zeroes, which is necessary to make them sort correctly. 10 digits is enough for most e-commerce applications. Note that padding is only needed to the left of the decimal point. If you want to sort some numbers that have up to 10 digits before the decimal point, and 2 digits after it, you can specify #10, not #12.

If the source array contains single values, not objects, leave the colspecs parameter empty for alphabetic sorting. For numeric sorting on an array of numbers, use a column spec that has a suffix but no name, e.g. #10 for sorting 10-digit numbers.

If the first character of the direction parameter is the letter D (upper or lower case), the function will sort in descending order. For any other value, the function will sort in ascending order. So you can use a variety of values such as ASC and DESC for SQL compatibility, or Up and Down for readability, or just empty-string and d to minimize the typing.

The source code for the function is included below. I've also written a demo/test script which you can try here. or download the source code here.

The test script runs several different types of sorts on small tables, and displays the results before and after sorting. Some of the data in the tables is random; the script will generate new data each time you refresh the page. For demo purposes, MultiSort saves the key value it computed for each array element, and displays them as an extra column in the results. Outputting the key can also be helpful for debugging.

Also, the script can optionally run some tests on larger tables, so you can try some speed tests. The larger tests are controlled by URL parameters:


MSON: JSON for Miva

MSON:  JSON for Miva
 

I like working with Miva Script, but it does have a few downsides. For one thing, it's a very verbose language. This statement:

<MvASSIGN NAME="l.a" VALUE="{ l.b + l.c }">

-- takes 43 characters, whereas in most programming languages it looks more like:

a = b + c;

-- which takes 10. Every character you enter is another opportunity for a typo; so I'm always looking for shortcuts to make my code more concise.

When I need to create an object with several members, I used to write a stack of assignment statements, such as this example from one of my shipping modules:

-- which would be followed by a call to Basket_Charge_Insert(). Recently, I created an object-building function that lets me rewrite the code like this:

The format is a bit cryptic with all the quotes and other delimiters, but it's still less typing. Would you like to have a copy of Object() for your own use? Here you go:

The format that Miva Script uses for serializing data is plain text, comma-delimited. It's flexible enough to allow some white space for readbility. It's conceptually similar to JSON, which makes it worthy of the name "MSON:" Miva Script Object Notation.

If you run miva_array_serialize() on the shipping-charge object I just created, you'll get back a string that looks like this:

:amount=12.34,:descrip=Shipping%3A+My+shipping+module,:disp_amt=12.34,:module_id=123,
:tax_exempt=0,:type=SHIPPING

It's a comma-delimited list of name-value pairs, or maybe "path-value pairs" is a better description. The part before the equal sign is the member name that specifies the location of the data. The part after the equal sign is the value of the data. Note that string values may need to be URL-encoded (using the Miva built-in function encodeattribute()) in case they contain any of the reserved characters such as the comma and colon. Letters and digits are safe, and don't need to be explicitly URL-encoded. White-space characters can be added after the commas, and in some other places as well, for readbility.

In the above example, each path is a colon and a member name. But this notation works for arrays too, and for more complex structures. For instance, here's the serialization of an array containing a couple of BasketItem objects. It's an array of two elements, each of which is an object with a lot of named members. This data came from the page template, so it's loaded up with things like formatted_price that aren't in the DB table.

Each item has a member named :options, which itself is a nested array of objects about the sizes and colors of the item, copied from the BasketOptions table. The MSON notation gives the complete path for each piece of data; for instance, the price of the 3rd option of the 2nd item has the path [2]:options[3]:price.

By the way, serialization and deserialization work fine on single values; they're not just for arrays and objects. For example, if you serialize the number 12345, the result is the string =12345. The path in this case is empty, since the value has no elements or members.

Serializing an empty string returns an empty string, and the same is true for de-serializing. I think earlier versions of the VM used to return a single equal sign when they serialized an empty string. Deserializing a string consisting of a single equal sign still works, and returns an empty string.

Note that when you serialize an array or object, elements or members that contain the empty string will be completely dropped from the result. This saves some space, and doesn't have much effect on results, since in Miva Script, a variable containing an empty string is largely equivalent to one that hasn't been assigned any value.

So we have this nice notation that's already supported by the language. It's more verbose than JSON, but it does have a few advantages. It can handle sparse arrays, and arrays that start with element number 1 instead of zero, which JSON can't do. And it shows you the complete path for every piece of data, instead of requiring you to count brackets to figure out where something is stored.

Besides serializing data, what's it good for? The Object() function comes in handy from time to time. And whenever I'm working on a Miva module or script, I find it very helpful to put debugging messages in the code that display data in this format. I've written a function that will serialize a variable, clean up the data a bit, and put in the delimiter of my choice. For debugging, I write MvEVALs that use this function to display data on my screen in MSON format.

I usually use a semicolon and a space as the delimiter. The semicolon helps with clarity, since it doesn't often occur in the data; and the space allows my browser to break long lines to fit on the screen. When I want more readability, I use a <br/> tag as the delimiter, to put each path-value pair on a separate line of the screen, as in the above BasketItem example.


Miva-JSON conversion, revisited

Miva-JSON conversion, revisited
 

In 2011, I wrote some functions for converting between Miva Script objects and JSON text. In the years since then, JSON has become more common for many types of data transfer and storage; it's not just for AJAX any more. It's replacing XML as a popular format for exchanging data over the Internet, and for storing it in a way that's compatible with many platforms and programs.

The Miva VM and compiler now support a built-in function, miva_json_decode(), for converting JSON to a Miva Script object. But there is no built-in function for going the other way, from Miva to JSON. So the function I wrote for that is still useful. Ray Yates and Timothy Bolton have made some bug fixes and improvements. A copy of the latest version is listed below.

My original article is still on-line at http://TheMagicM.com/jsonize.mvc. It includes a demo script so you can try the conversions yourself. It also has a link to download the source code for both the functions and the demo script.