Introducing Macro Web Components

Are you longing to be able to use Web Components? Realizing that Polymer is not really going to be usable outside of Chrome any time soon?

Me too… I decided to fix the problem.

Macro Web Components, while not quite as elegant as Polymer components, are every bit as powerful. You can use them now, on any browser that supports the CSS and Javascript you use in your component. They will organize your code in a way that should make it easy to transition to full-blown web components when they truly become available. Unlike Polymer, they do not force you to use technologies you don’t need in a component and will work nicely with your favorite event management, data-binding and templating systems.

When I was debugging a Polymer app in Chrome DevTools, a funny thought struck me… It seemed to me that the Polymer team had created the most complex (and inefficient) macro expansion system I had ever seen. All the “DIVs” were still there, but numerous additional HTML Elements and Shadow DOMs were now in the tree. While Polymer had done a good job of encapsulating component data and styles. It had actually made debugging harder and theming truly painful. Recursive, parameterized, macros seemed to me the obvious solution.

These days, macros need explaining because in the world of Object-Orientation, they came to be seen as archaic. If you’ve seen the C programming language, you’ve seen macros… that’s what a #define is. But don’t let the numerous abuses of C macros turn you off to them. Likewise, the various templating packages for web development are a simple, less structured form of macro processing. Essentially, a macro takes text of one form and expands it into a more detailed form. Macros are most interesting when the transformation is structured. Back in the days when one often had to program in assembly language, macros provided an elegant and indispensable way of writing code. The best example was in the calling sequence of a subroutine. Certain instructions had to be written in the same way every time any routine was called. Coding this by hand was tedious to write and difficult to maintain. Macros provided you with the facility to get it right every time. These macros were structured to look and act like normal assembly language op codes.

I have created Macro Web Components to capture the flavor of the old assembly language macros for HTML. Here is an example fragment of an MWC-enhanced HTML (.mwch) file:

1
2
3
4
5
6
<menu@ id="Menu" icon="hamburger">
<mi@ action="alpha" label="Alpha"/>
<mi@ action="beta" label="Beta"/>
<ms@/>
<mi@ action="omega" label="Omega"/>
</menu@>

The tags with ‘@’ appended are macro invocations. The mwc preprocessor will parse the .mwch file and automatically read and expand each macro into the appropriate HTML, CSS and JavaScript files. Any time, I need to write a menu in my web app, I can use this concise notation. Below are the three invoked MWC component (.mwcc ) files in my mwc_components subdirectory.

For the popular Bootstrap package, these component files would look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- menu.mwcc -->
<div class="dropdown">
<button id="${uid@}" class="btn btn-primary btn-lg" type="button" data-toggle="dropdown" aria-haspop="true" aria-expanded="false">
#if icon == "hamburger"
<span class="glyphicon glyphicon-menu-hamburger" aria-hidden="true"></span>
#elif defined(label)
${label}
#else
#error "Must specify either icon or label attribute"
#endif
</button>
<ul class="dropdown-menu" role="menu" aria-labeledby="${uid@}">
<content@/>
</ul>
</div>

1
2
<!-- ms.mwcc -->
<li class="divider"></li>
1
2
<!-- mi.mwcc -->
<li role="presentation"><a role="menuitem" tabindex="-1" href="#">${label}</a></li>

After expansion, the resulting HTML file would contain:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div class="dropdown">
<button id="Menu" class="btn btn-primary btn-lg" type="button" data-toggle="dropdown" aria-haspop="true" aria-expanded="false">
<span class="glyphicon glyphicon-menu-hamburger" aria-hidden="true"></span>
</button>
<ul class="dropdown-menu" role="menu" aria-labeledby="Menu">
<li role="presentation">
<a role="menuitem" tabindex="-1" href="#">
Alpha
</a>
</li>
<li role="presentation">
<a role="menuitem" tabindex="-1" href="#">
Beta
</a>
</li>
<li class="divider">
</li>
<li role="presentation">
<a role="menuitem" tabindex="-1" href="#">
Omega
</a>
</li>
</ul>
</div>

For real world usage, you’ll need to modify the <mi@> component, to either use a class and JavaScript to add an event listener, or perhaps use onclick to trigger an event in your favorite publish/subscribe package. (If you are using this as a front-end to Polymer or WinJS, you could use their native mechanisms). In other words, you need to make the selected menu item actually do something. Any per component styling or JavaScript can be added using #style and #script sections inside the .mwcc file.

Writing web apps with macros is clearly shorter. It puts all the detailed HTML in one place for easier maintenance. Much of the work is done statically, reducing startup time and battery usage. Also, with the increasing use of web technologies for desktop/mobile apps, macros let you use one enhanced HTML file to deliver apps with different look and feel on the different platforms.

See examples/three-platforms for an example using the menu@ macro above and only one .mwch file to support Bootstrap, Polymer and WinJS.

I expect that fans of Web Components to be up in arms at this point… because web components are “so much more powerful”. But that really isn’t true. Web Components (as defined on WebComponents.org) are simply more object-oriented. Using standard JavaScript techniques, you can write anything you want. Here is Polymer’s my-counter example as an MWC component:

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
<div class="my-counter ${class?}" ${attributes-as-args@}>
<label><content@></label>
<div>Value: <span id="${uid@}-.span"/></div>
<button id=${uid@}-.button>Increment</button>
</div>

#style

.my-counter {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.my-counter button {
font-weight: bold;
}
.my-counter label {
font-weight: bold;
}

#script

hlp.initComponentClass('my-counter', function(scope, attrs) {
var uid = attrs['uid@'];
scope.counter = attrs.counter;
if (scope.counter === undefined) {
scope.counter = 0;
} else {
scope.counter = parseInt(scope.counter, 10);
}
hlp.$(uid+'-.span').textContent = scope.counter.toString();
scope.increment = function () {
scope.counter++;
hlp.$(uid+'-.span').classList.add('my-counter-highlight');
hlp.$(uid+'-.span').textContent = scope.counter.toString();
};
hlp.$(uid+'-.button').addEventListener("click", function(e) {
scope.increment();
});
});

You can find the code for the hlp helper functions above in the examples/my-counter/js/hlp.js subdirectory of the MWC distribution.

I didn’t need Shadow DOM because I name-spaced my CSS. I didn’t need a template for this component. I didn’t need HTML imports. Using Object.observe would be major overkill. I didn’t even need registerElement.

I would also expect to have Web Component advocates to complain that these components cannot be dynamically instantiated. But again not true [though significantly less elegant than Polymer].

Consider doing something like this <template@> macro:

1
2
3
<script id={$id} type="text/template">
<content@>
</script>

Now invoke with <template@ id="mc1"><my-counter@ id="mc1-ctr"/></template@> and then use the old-fashioned way of doing templates:

1
2
3
var template = document.getElementById("mc1").innerHTML;
var d = document.createElement('div');
d.innerHTML = template;

You will need to walk the DOM element tree of d and replace the mc1 with something unique on all the ids. You’ll also need to arrange to trigger the function in my-counter‘s initComponentClass for the dynamically created component. Regardless, it wouldn’t take much thought to make a convenience function to do it all automatically.

MWC has many more macro features than I’ve shown above. You can find a reference manual and examples in its Github repository. It can also be installed with npm:

1
npm install -g mwc