Fish

In this section, I'll give some of my framework design philosophy and opinions on what makes a good framework. I don't think my opinions are particularly unusual or novel, but I thought I'd lay them out here nonetheless, as that might give you an idea of the approach Modulo is taking.

Let's do a thought experiment. Imagine the most minimalist JS component framework possible. Let's call it "Fish" (probably already a thing, but bare with me), by Tux the Penguin (because hey, why not?). Here is the complete code for the "Fish" framework:

// Fish Framework - (C) 2091 - Tux the Penguin function Fish(el, key, value) { el.state = el.state || JSON.parse(el.getAttribute("x-state") || "{}") el.state[key] = value; el.innerHTML = el.innerHTML.replace(/\$\{([^\)]+)?\}/g, (a, b) => el.state[b]); }

Now, Tux could just publish this by itself, and no one would ever want to use it, since it it's just several lines of useless, undocumented code. People copy & paste far more code from Stack Overflow just for simple tasks!

However, now imagine Tux wrote documentation for it. Imagine Tux wrote a massive recipe book of hundreds of examples, ranging from API usage to common UI tasks. For an example, skim (no need to actually thoroughly read) the following "excerpt" of the imaginary "Fish" documentation:

<!-- Fish Framework - Documentation --> <!-- BEGINNER EXAMPLE: BUTTON --> <!-- Look how easy it is to make a button that counts: --> <button onClick='Fish(this, "count", this.state.count++)' x-state='{"count": 3}'> <p>I ate {count} delicious herrings</p> </button> <!-- Okay, lets break it down: - By using the x-state directive, the core directive of Fish (see Chapter 2 on directives and how to create your own), we can set the initial state of an element. - What is state? An object that you can mutate to affect how your elements are displayed. Use it to store data from the DB, or elsewhere. - ... snip ... --> <!-- ADVANCED EXAMPLE: API --> <!-- Lets see another example of state, this time with API: --> <div x-state='{"search": ""}'> <p>Information about account found: Their name is {name}</p> <input onChange='Fish(this.parentNode, "search", this.value)' placholder="Type in a GitHub username..." /> <button onClick=' fetch(`https://api.github.com/users/${this.parentNode.state.search}`) .then(response => response.json()) .then(({name}) => Fish(this.parentNode, "name", name)) '>Search GitHub</button> </div> <!-- Okay, lets break it down: - In this example, you can see how ........... (- snip -) --> <!-- UI TOOLKIT EXAMPLE: ACCORDIAN TOGGLE --> <!-- Or, look how you can make toggle accordians elements just by using .... --> <nav x-state='{}'> <li onClick='Fish(this.parentNode, "info_class", "visible")'> Show Info </li> <li onClick='Fish(this.parentNode, "contact_class", "visible")'> Show Contact </li> <li onClick='Fish(this.parentNode, "blog_class", "visible")'> Show Blog </li> <div class="{info_active}">Hi, this is info about my website...</div> <div class="{contact_class}">I live in Antarctica and write free software...</div> <div class="{blog_class}">Thoughts on fish: Why fish is actually...</div> </nav>

Now this is looking more and more like a complete framework! The missing ingredient was documentation and examples. In fact, that was even more important than the original code: The original code could be anything, just setting up a stack, checking up some attributes, whatever, but without the patterns of usage around the original code it is meaningless nonsense.

In other words, when people think of a framework, they don't think of how it's coded, they think what they can do with it. Thus, a framework can be better thought of as a bundle of best practices and workflow, or a recipe book. The "code" of the framework is immaterial, as long as the documented examples work. In this case, the code was only a few lines long, yet the examples showed that it could be used for a myriad of tasks. In fact, these sorts of snippets in the documentation can be thought of as being as much part of the framework as anything else: That is how "best practices" and "common patterns" are established.

In my view, non-opinionated frameworks are incomplete to the extent they don't have opinions. That's not to say they are bad or useless, "unopinionated" has it's time and place, but it's just to acknowledge that they are BYO ("bring your own") for anything that the documentation doesn't explicitly establish patterns of usage. Just look at the dozens of ways that Redux is implemented in the industry, for an example. Just saying that you use "Redux" tells little about the code structure or practices, or the myriad of middleware that is typically incorporated to handle various common use-patterns. Redux Toolkit (which is actually opinionated) can make projects look entirely different than the "BYO" Redux projects! Once you "bring your own" patterns to fill in these gaps, only then the framework is "truly complete", using the nomenclature I'm using in this document, at least. One Redux setup might be so different than another due to patterns of usage, that the lines defining them are a bit fuzzier, in some cases to the point where it's difficult to share code between different interpretations of the same framework, making it almost like a different framework altogether.

With any project, very quickly the "userland" (e.g. developer's) code eclipses the framework code in size and performance impact, which means these best practices and pattern recipes can have a much bigger impact on the resulting product than internal implementations. If the documentation shows many slow performing examples, or if it doesn't suggest anything at all and developers tend to go for slow implementations, I think it's perfectly fair to say that the framework is "slow", regardless of the internal operations. (An adjective that no doubt applies to Modulo as well, to be fair!)

Modulo's goal is short code and long docs. It's basically "Fish", but multiplied times 250. It's as though Tux continued hacking away, bringing it from 5 lines to about 1000-2000 lines, and then kept on hacking away at docs, examples, and use-patterns, keeping them still proportionally much longer.

Coincidentally, this is actually somewhat how various parts of Modulo originally were developed. Modulo itself had an earlier incarnation called MoEdCo (Modular Editor Components), which was a Web Components framework in less than 500 lines for Electron.js. That, in turn, evolved out of a demo that I live coded for a class I was teaching on React JavaScript back in 2017. The in-class demo was a "React-like" in just a few dozen lines of code, to explain the core philosophy and patterns used by React without the extra baggage of the new JSX syntax and complicated build environment (webpack, babel, etc). Similarly, the Modulo Template Language evolved out of TinyTiny, which was an expanded version of an even shorter "codegolf" challenge that I had done for fun (now about a decade ago).


Summary: A framework isn't just code, it's an entire workflow and way of doing things. The code is important, but less important than the other aspects.

Also, for clarity's sake: The "Fish" code above is untested, incomplete, and almost certainly has deal-breaking bugs, but could work in principle with a tweak or two and the right context. This doesn't matter either way for our purposes. Also, I'm very far from the first person to approach framework development this way — in fact, the increasingly popular Alpine.js follows a similar principle! It also happens to resemble my silly "Fish" example, but is actually real and truly useful, so if you want an actually production-ready version of this, check it out. If the "component-oriented" structure of Modulo is not your cup of tea, but you still want a tiny, modern, drop-in JS framework, Apline.js could be a good option!