JavaScript Markup Language (pronounced Jamilih–Beauty, in Arabic)
MIT License
(see also licenses for dev. deps.)
If you are seeking an even lighter version (e.g., for inclusion in a stand-alone library) while still getting some benefits of the syntax-highlighter-friendly pure JS approach for DOM construction, see Jamilih Lite.
Note that it is our intent to move the XML-specific features into a new file.
For templating,
separation_of_concerns !== separation_of_syntaxes
!
One can very legitimately build HTML DOM using Jamilih as with any other
JavaScript function and maintain separation of concerns. Just because
the syntax is JavaScript does not mean it
isn't suitable for building structural and styling design logic. On
the contrary, it provides flexibility for designers to utilize their
own JavaScript (and/or your own custom template functions) as long as
the designer can maintain the discipline to avoid adding business
logic. (A future "JSON mode" should allow more security but less control.)
The following functions are available:
jml()
- For building DOM objects (and optionally appending intojml.toJML(objOrString, config)
- For converting HTML in DOM or string form intoconfig
is an object and which supports a stringOutput
propertytrue
in order to JSON-stringify the convertedjml.toJMLString(objOrString, config)
- Works like jml.toJML
butjml.toHTML()
- Works like jml()
except that the resulting DOMjml.toXML()
- Works like jml()
except that the resulting DOM objectjml.toDOM()
- An alias for jml()
.jml.toDOMString()
- An alias for jml.toHTML()
(for parity with toJMLString
).jml.toXMLDOMString()
- An alias for jml.toXML()
(for parity with toJMLString
).jml.command
- Invoke commands on element-attached symbol or Map function or methodsjml.sym
- Alias for jml.symbol
jml.for
- Alias for jml.symbol
jml.symbol
- Utility for getting a symbol attached to an element.jml.setWindow
- Setter of the window
object. Used internally and requiresDOMParser
, XMLSerializer
, and document
objects. Set automatically forjml.getWindow
- Getter for the setter.jml.weak(obj, ...args)
- Returns a two-item array with the first item as a new jml.WeakMap
objectobj
and a Jamilih element created out of passingargs
to jml()
and the second item is the new Jamilih elemnetjml.strong(obj, ...args)
- Same as jml.weak
but creates a new jml.Map
object instead of a jml.WeakMap
.jml.WeakMap()
- a WeakMap
subclass with an invoke
method that should be passed a DOM elementjml
or jml.weak()
), the name of a method to invoke (on an objectjml.weak()
)), and any number ofthis
valueinvoke
, it will have the element itself supplied as the first argument. Thisget
and set
methods enhanced to accept a string selector to represent thejml.Map()
- Same as jml.WeakMap
but is a subclass of Map
instead.<script src="node_modules/core-js-bundle/minified.js"></script>
<script src="node_modules/jamilih/dist/jml.js"></script>
jml(...args);
If compiling, select from any or all of jml
, $
, $$
, nbsp
, and body
:
import 'core-js-bundle';
import {jml, $, $$, nbsp, body} from 'jamilih';
If not compiling:
import './node_modules/core-js-bundle/minified.js';
import {jml, $, $$, nbsp, body} from './node_modules/jamilih/dist/jml-es.js';
For backward compatibility, a default export is provided, but this is now deprecated:
import './node_modules/core-js-bundle/minified.js';
import jml from './node_modules/jamilih/dist/jml-es.js';
If for browser only (the core-js-bundle
is for any polyfilling needed):
npm install jamilih core-js-bundle
If for Node use:
npm install jamilih core-js-bundle jsdom request
require('core-js-bundle');
const jml = require('jamilih');
Note that while we check for preexisting globals (window
, document
, and XMLSerializer
),
we attempt to maintain modularity by not injecting our own global. If you want to
import Jamilih and then operate on the same window
, etc. that we create, use the methods,
getWindow
, getDocument
, and getXMLSerizlier
. There are also corresponding setters.
Simple element...
const input = jml('input');
Simple element with attributes...
const input = jml('input', {type: 'password', id: 'my_pass'});
Simple element with just child elements...
const div = jml('div', [
['p', ['no attributes on the div']]
]);
Simple element with attributes and child elements...
const div = jml('div', {class: 'myClass'}, [
['p', ['Some inner text']],
['p', ['another child paragraph']]
]);
Simple element with attributes, child elements, and text nodes...
const div = jml('div', {class: 'myClass'}, [
'text1',
['p', ['Some inner text']],
'text3'
]);
DOM attachment...
const simpleAttachToParent = jml('hr', body);
Returning first element among siblings when appending them to a DOM element (API unstable)...
const firstTr = jml(
'tr', [
['td', ['row 1 cell 1']],
['td', ['row 1 cell 2']]
],
'tr', {className: 'anotherRowSibling'}, [
['td', ['row 2 cell 1']],
['td', ['row 2 cell 2']]
],
table
);
Returning element siblings as an array (API unstable)...
const trsFragment = jml(
'tr', [
['td', ['row 1 cell 1']],
['td', ['row 1 cell 2']]
],
'tr', {className: 'anotherRowSibling'}, [
['td', ['row 2 cell 1']],
['td', ['row 2 cell 2']]
],
null
);
Inclusion of regular DOM elements...
const div = jml(
'div', [
$('#DOMChildrenMustBeInArray')[0]
],
$('#anotherElementToAddToParent')[0],
$('#yetAnotherSiblingToAddToParent')[0],
parent
);
Document fragments addable anywhere within child elements...
jml('div', [
'text0',
{'#': ['text1', ['span', ['inner text']], 'text2']},
'text3'
]);
You can also use the JsonML style for fragments:
jml('div', [
'text0',
['', [
'text1', ['span', ['inner text']], 'text2'
]],
'text3'
]);
Event attachment...
const input = jml('input', {
// Contains events to be added via addEventListener or
// attachEvent where available
$on: {
click: [function () {
alert('worked1');
}, true] // Capturing
}
});
const input2 = jml('input', {
style: 'position:absolute; left: -1000px;',
$on: {
click () {
alert('worked2');
},
focus: [function () {
alert('worked3');
}, true]
}
}, body);
The events attached via $on
are added through addEventListener
.
Comments, processing instructions, entities, decimal and hexadecimal character references, CDATA sections...
Note that the last three types, relying as they do on innerHTML
,
will not work properly in the innerHTML
build (they will use
textContent
instead).
const div = jml('div', [
['!', 'a comment'],
['?', 'customPI', 'a processing instruction'],
// Or with an object of "attributes" (like `xml-stylesheet` `href`)
['?', 'customPIB', {
att1: 'val 1',
att2: 'val 2"'
}],
['![', '&test <CDATA> content'],
['&', 'copy'],
['#', '1234'],
['#x', 'ab3']
]);
Namespace definitions (default or prefixed)...
jml('abc', {xmlns: 'def'});
jml('abc', {xmlns: {prefix1: 'def', prefix2: 'ghi'}});
jml('abc', {xmlns: {prefix1: 'def', prefix2: 'ghi', '': 'newdefault'}});
The $shadow
property can be added to an element to attach Shadow DOM content.
(Note: This is not currently supported in jsdom or
certain browsers.)
Its allowable properties include:
true
. May also be set in place of content
(with the same allowable values) to serve as the shadow DOM contents.open
. Defaults to false
. May also be used (as with open
) to directly build the contents (see open
).template
is not present, this optional array of arguments will be passed as fragment contents to jml()
for direct attachment to the shadow root of this element. May also be set to a string or DOM element in which case, it is passed to jml()
as the first argument (the element or element name).template
may optionally be present to indicate a template for cloning. If template
is a string selector or a DOM <template>
element, the indicated element will be cloned and added as the shadow root contents. If template
is an array, its contents will be passed to jml()
for first creating a <template>
element, and then it will be appended to the document body, and then it will be cloned for use with the shadow DOM. If the first (or only) item in the array is a regular object, these will become the attributes of the <template>
element while the subsequent item in the array will be passed as the template children. If the first item is not a regular object, the whole array will be assumed to represent the <template>
children (without attributes).jml('div', {
id: 'myElem',
$shadow: {
open: true, // Default (can also use `closed`)
template: [
{id: 'myTemplate'},
[
['style', [`
:host {color: red;}
::slotted(p) {color: blue;}
`]],
['slot', {name: 'h'}, ['NEED NAMED SLOT']],
['h2', ['Heading level 2']],
['slot', ['DEFAULT CONTENT HERE']]
]
]
}
}, [
['h1', {slot: 'h'}, ['Heading level 1']],
['p', ['Other content']]
], body);
jml('div', {
id: 'myElem',
$shadow: {
// Could also define as `open: []`
content: [
['style', [`
:host {color: red;}
::slotted(p) {color: blue;}
`]],
['slot', {name: 'h'}, ['NEED NAMED SLOT']],
['h2', ['Heading level 2']],
['slot', ['DEFAULT CONTENT HERE']]
]
}
}, [
['h1', {slot: 'h'}, ['Heading level 1']],
['p', ['Other content']]
], body);
One may attach functions or objects to elements via a $symbol
attribute
which accepts a two-item array, with the first item either being a string
to be used with Symbol.for()
or a Symbol
instance, and the second
item being the function or object. If a function is supplied, its this
will be set to the element on which the symbol was added, while if an
object is supplied, its this
will remain as the object itself, but an
elem
property will be added to the object which can be used to get the
element on which the symbol was added. If you do not wish to add such a
reference, consider using a symbol with $custom
.
jml('input', {
id: 'symInput1',
$symbol: ['publicForSym1', function (arg1) {
console.log(
(this.id + ' ' + arg1) === 'symInput1 arg1'
);
}]
}, body);
// Then elsewhere get and use the symbol function for the DOM object
$('#symInput1')[Symbol.for('publicForSym1')]('arg1');
// Or using the `jml.sym` utility (accepting selector or
// DOM element as first argument):
jml.sym($('#symInput1'), 'publicForSym1')('arg1');
jml.sym('#symInput1', 'publicForSym1')('arg1');
Or using an example with a (private) Symbol
instance and
an object instead of a function:
const privateSym = Symbol('a private symbol');
jml('input', {id: 'symInput3', $symbol: [privateSym, {
localValue: 5,
test (arg1) {
console.log(this.localValue === 5);
console.log(
(this.elem.id + ' ' + arg1) === 'symInput3 arg3'
);
}
}]}, body);
// Obtaining the element with symbol or using the utility:
$('#symInput3')[privateSym].test('arg3');
jml.sym('#symInput3', privateSym).test('arg3');
Symbol attachment is particularly convenient for templates where you wish to keep a lot of inline children (avoiding defining the children separately, adding the symbol to the variables, and then reassembling them together) and without the overhead of defining a custom element.
jml('div', [
['input', {id: 'symInput1', $symbol: ['publicForSym1', function (arg1) {
console.log(
(this.id + ' ' + arg1) === 'symInput1 arg1'
);
}]}],
['div', {id: 'divSymbolTest', $on: {
click () {
// Can supply element or selector
jml.sym(this.previousElementSibling, 'publicForSym1')('arg1');
jml.sym('#symInput3', privateSym).test('arg3');
// Or use symbols directly:
this.previousElementSibling[Symbol.for('publicForSym1')]('arg1');
}
}}],
['input', {id: 'symInput3', $symbol: [privateSym, {
localValue: 5,
test (arg1) {
console.log(this.localValue === 5);
console.log(
(this.elem.id + ' ' + arg1) === 'symInput3 arg3'
);
}
}]}]
], body);
For attachment of custom properties (or setting of standard properties) to an element, supply
an object with the desired properties (including symbols) to $custom
.
The advantage of this approach is that one doesn't need to manage symbols, maps, or define elements,
and the this
works as expected to refer to the element (including the other properties on the
object which will also be added to the element instance), but one disadvantage is that the
properties (like methods) will be added to each instance of the element rather than to a prototype.
(In such a case, you can extend, the relevant HTMLElement
interface like HTMLAnchorElement
.)
const mySelect = jml('select', {
id: 'mySelect',
$custom: {
test () {
return this.id;
},
test2 () {
return this.test();
}
}
}, body);
console.log(mySelect.test() === 'mySelect');
console.log(mySelect.test2() === 'mySelect');
Another disadvantage of the above is that the methods/object properties
could also conflict with future standard ones of the same name added
to the built-in element. While our example does not do so, you might
therefore wish to protect consumers of your methods from naming that
could conflict with future standard names. Per this comment,
a safe option would be to merely add $
in front of the custom method
names or properties (e.g., it would become $test
and $test2
in
the example). Another advantage of doing so is that consumers can easily
discern which methods are standard (and thus can be queried online) and
which are specific to your API.
While symbols are somewhat more convenient to use, you may wish to
associate elements with any number of Map
or WeakMap
instances
and take advantage of those objects' methods (or our enhanced
version of these methods jml.Map
and jml.WeakMap
).
(TODO: Adapt examples from tests)
Commands are a convenience to invoke a function (optionally with arguments) associated with an element via symbol or map.
(TODO: Adapt examples from tests)
(Note: This is not currently supported in jsdom or certain browsers.)
While there is some extra overhead to creating a custom element (in
terms of performance at registering an element and for the need to
give a unique name), among other benefits, custom elements allow
its methods to have this
not only reference the element, but also
to call other custom methods on the element in the same manner (unlike
the approach we use with maps and symbols).
You have a number of options.
You may supply an object to have its prototype copied (onto
an empty HTMLElement
-extending constructor):
const myEl = jml('my-el', {
id: 'myEl',
$define: {
test () {
return this.id;
}
}
}, body);
console.log(myEl.test() === 'myEl');
You may supply a (plain) function to be used within a HTMLElement
-extending
constructor (it will be executed after a call to the dynamically-created class'
super
):
let constructorSetVar2;
jml('my-el2', {
id: 'myEl2',
$define () {
constructorSetVar2 = this.id;
}
}, body);
console.log(constructorSetVar2 === 'myEl2');
You may supply a class (though it must extend HTMLElement
and invoke super()
as
per (autonomous) custom element requirements).
It may be an inline class expression or a reference to a class declaration.
let constructorSetVar3;
jml('my-el3', {
id: 'myEl3',
$define: class extends HTMLElement {
constructor () {
super();
constructorSetVar3 = this.id;
}
}
}, body);
console.log(constructorSetVar3 === 'myEl3');
You may supply a two-element array with the function (or class) and prototype methods.
let constructorSetVar4;
const myel4 = jml('my-el4', {
id: 'myEl4',
$define: [function () {
constructorSetVar4 = this.id;
}, {
test () {
console.log(this.id === 'myEl4');
},
test2 () {
this.test();
}
}]
}, body);
console.log(constructorSetVar4 === 'myEl4');
myel4.test();
myel4.test2();
Plugins may be supplied within an array passed on an object as the first
argument to Jamilih. Plugins must contain both a name
and set
property
and the name must begin with $_
. When used within jamilih, the value
for the plugin property can be set to a string, an object, or whatever
you prefer.
const options = {$plugins: [
{
name: '$_myplugin',
set ({element, attribute: {name, value}}) {
// Add code here to modify the element
element.setAttribute(name, value + 'Changed');
if (value.blueAndRed) {
element.style.color = 'blue';
element.style.backgroundColor = 'red';
}
}
}
]};
jml(options, 'div', {id: 'myDiv', $_myplugin: {
blueAndRed: true
}}, body);
// If reusing, you may wish to bind the options
const j = jml.bind(null, options);
// Then you can reuse without needing to resupply the
// options (including its plugins)
j('div', {id: 'myDiv', $_myplugin: {
blueAndRed: true
}}, body);
In addition to element
and attribute
, opts
is available,
including its state
property (set to root
, element
,
fragment
, children
, or fragmentChildren
).
For a list of plugins, see docs/PLUGINS.md.
The following are for small but very frequently used template items. I do not expect to make Jamilih into a full-blown template utility library, but I believe some very common uses ought to be available out of the box.
$(selector)
This is just a alias for document.querySelector
(which is often
needed within templates for attaching behaviors).
$$(selector)
This is just a alias for document.querySelectorAll
, with the return
result converted to an array.
nbsp
This is just equivalent to U+00a0
(or in HTML,
), the
non-breaking-space. As a very frequently needed item in templates,
this tiny item is easily available by default as well.
body
This is an alias for document.body
if available. Frequently used as
a target for appending.
glue(arr, glu)
This takes an array (any array) and intersperses the glu
(a suitable
repeating item for Jamilih, including a string, DOM element, or
Jamilih array). Useful, e.g., for joining elements with nbsp
, a comma,
or some recurring item, without the need for a special map
or reduce
.
jml()
must be either:
null
as the last argument.)!
followed by a string to create a comment&
followed by an HTML entity reference (e.g., copy
)#
followed by a decimal character reference as a string or number, e.g., 1234
#x
followed by a hexadecimal character reference as a string, e.g., ab3
?
followed by a processing instruction target string and string value (XML)'![
followed by CDATA content as a string (XML), e.g., &test <CDATA> content
#
indicating a document fragment; see array children below for allowable$text
set to a string to create a bare text node (this is only necessary if$
has a special purpose and if it begins with $_
,$document
set to an object with properties childNodes
. In place of childNodes
, one mayhead
and body
. One may also add a string title
property in which case, a <head>
will be automatically created, with a <meta charset="utf-8"/>
element (as expected by HTML5) and a <title>
element, and any additionally supplied head
array items appended to that <head>
. If head
,body
, or title
are supplied, an empty "html" DOCTYPE will be auto-created (as expected by HTML5) as well as an<html>
element with the XHTML namespace. If head
is supplied, a <meta charset="utf-8">
will also be added as<head>
.$DOCTYPE
object with properties name
, and, where present,publicId
and systemId
.$attribute
set to an array of a namespace, name, and value (for a$on
expects a subject of event types mapped to a function or to an arrayselected
, checked
, defaultSelected
,defaultChecked
, readonly
, disabled
, indeterminate
), making them useful inundefined
),setAttribute
which wouldclass
, for
, innerHTML
, value
,defaultValue
, style
(Note that innerHTML
won't work on theclassName
and htmlFor
are also provided to avoid the need for quoting the reservedclass
and for
.on
followed by any string will be set as a property (for events).xmlns
for namespace declarations (not needed in HTML)
dataset
is a (nestable) object whose keys are hyphenated or camel-cased properties used to set the dataset property (note that no polyfill for older browsers is provided out of the box)#
with an array of children (followingnull
is currently undefined behavior and should not be used;jml()
is usually the parent node to which tonull
(at the end) will cause an element or fragment to be returnednull
is the lastA tentative JSON Schema is available here.
Provide round-trippable JSON/JavaScript serialization as with JsonML, but with all items at a given array level being the same type of item (unless marked with a deeper level of nesting) and with a slightly more template-friendly capacity to inline insert fragments or child nodes (e.g., as by function return).
I originally named the project JML (for JavaScript or Json Markup Language) and have still kept the abbreviation when used as a global in a browser (and in the filename and examples), but as other projects have used the name or similar ones, I am renaming the project to "Jamilih" for the Arabic word meaning "Beauty". It is named in honor of the Arabic name of my family's newly-born daughter.
The only work which comes close to meeting these goals as far as I have been able to find is JsonML. JsonML even does a better job of goal #1 in terms of succinctness than my proposal for Jamilih (except that Jamilih can represent empty elements more succinctly). However, for goal #3, I believe Jamilih is slightly more flexible for regular usage in templates, and to my personal sensibilities, more clear in goal #8 (and with a plan for goal #5 and #7?).
defaultChecked
,defaultSelected
, defaultValue
select
value
can take place after the options areMap
/WeakMap
Templates to define and invoke$symbol
to accept array of arrays for attaching multiple symbolsTo build the source code, you may use the latest version of npm (7) and the
minimum version of Node in the engines
field of package.json
. Presently
requires a *Nix type of OS to build.
npm i -g pnpm
to install pnpm
(saves a lot of hard drivedist
folder (if not, add one)pnpm install
to ensure devDependencies are installedpnpm rollup
dist
folderNote to browser add-on reviewers, the dist/jml-es-noinnerh.js
file is the
one copied into the add-on (it strips out innerHTML
capabilities for
security reasons and for simplification of the review process). The only
actual source file used in that file should be src/jml.js
.