modulo.js

The Declarative HTML Web Component Framework

  • Beginner-friendly features inspired by React, Svelte, and Vue.js
  • Only 2000 lines of dependency-free, self-building JavaScript
  • A “no fuss” drop-in for existing web apps or Jamstack static sites
<Template> <button @click:=script.countUp>Hello {{ state.num }}</button> </Template> <State num:=42 ></State> <Script> function countUp() { state.num++; } </Script>
<Template> Components can use any number of <strong>CParts</strong>. Here we use only <em>Style</em> and <em>Template</em>. </Template> <Style> em { color: darkgreen; } * { text-decoration: underline; } </Style>
<!-- Use StaticData CPart to include JSON from an API or file --> <Template> <strong>Name:</strong> {{ staticdata.name }} <br /> <strong>Site:</strong> {{ staticdata.homepage }} <br /> <strong>Tags:</strong> {{ staticdata.topics|join }} </Template> <StaticData -src="https://api.github.com/repos/modulojs/modulo" ></StaticData>
<!-- The classic "To-Do" app in Modulo --> <Template> <ol> {% for item in state.list %} <li>{{ item }}</li> {% endfor %} <li> <input [state.bind] name="text" /> <button @click:=script.addItem>Add</button> </li> </ol> </Template> <State list:='["Milk", "Bread", "Candy"]' text="Coffee" ></State> <Script> function addItem() { state.list.push(state.text); // add to list state.text = ""; // clear input } </Script>
USAGE
<p>Once defined, Modulo Web Components are used (and re-used) as if they were "real" HTML:</p> <x-DemoModal><p>Oh look, I am in a modal!</p></x-DemoModal> <hr /> <x-DemoModal button="Actor Info" title="Nic Cage"> <img src="https://i.imgur.com/hJwIMx7.png" /> <p>Nicholas Cage is a prolific Hollywood actor</p> <x-DemoModal button="Birthday">January 7th</x-DemoModal> <x-DemoModal button="Height">6ft (183cm)</x-DemoModal> </x-DemoModal>
<!-- This component generates a "type-as-you-go" Markdown preview. It uses -src= as a quick way to bring in the needed NPM dependency from the unpkg CDN. --> <Template> <textarea [state.bind] name="text"></textarea> <div>{{ script.html|safe }}</div> </Template> <State text="### Markdown Using `-src` to *quickly* add a **markdown parser**" ></State> <Script -src="https://unpkg.com/snarkdown"> const { snarkdown } = this; // (this === window) function prepareCallback() { return { // Every rerender, convert markdown to HTML html: snarkdown(state.text), }; } </Script> <Style> textarea, div { border: none; width: 95%; padding: 2%; min-height: 80px; } </Style>
<!-- A much more complicated example application. Note it's use of multiple Templates, and more complicated script tag. --> <Template> {# If the cards array has been populated, show game #} {% if state.cards.length %} {% include game_template %} {% else %} {% include menu_template %} {% endif %} </Template> <Template -name="menu_template"> <h3>Memory Game</h3> <p>Choose your difficulty:</p> <button @click:=script.setup payload:=8>2x4</button> <button @click:=script.setup payload:=16>4x4</button> <button @click:=script.setup payload:=36>6x6</button> </Template> <Template -name="game_template"> <div class="board {% if state.cards.length gt 16 %}hard{% endif %} ">{# Loop through each card in the "deck" (state.cards) #} {% for card in state.cards %} <div @click:=script.flip payload:="{{ card.id }}" class="card {% if card.id in state.revealed %}flipped{% endif %}" style="{% if state.win %} {# The cascading effect uses ids as offsets #} animation: flipping 0.5s infinite alternate; animation-delay: {{ card.id }}.{{ card.id }}s; {% endif %}"> {% if card.id in state.revealed %} {{ card.symbol }} {% endif %} </div> {% endfor %} </div> <p style="{{ state.failed|yesno:'color: red' }}"> {{ state.message }} </p> </Template> <State message="Good luck!" win:=false cards:=[] revealed:={} last:=null failed:=null ></State> <Script> const symbolsStr = "%!@#=?&+~÷≠∑µ‰∂Δƒσ"; // 16 options function setup(count) { // This function takes the number of "cards" the user // selected (see payload=), and populates state.cards let symbols = symbolsStr.substr(0, count / 2).split(""); symbols = symbols.concat(symbols); // duplicate cards let id = 0; while (id < count) { const index = Math.floor(Math.random() * symbols.length); const symbol = symbols.splice(index, 1)[0]; state.cards.push({symbol, id}); id++; } } function failedFlipCallback() { // Remove both from revealed obj & set to null delete state.revealed[state.failed]; delete state.revealed[state.last]; state.failed = null; state.last = null; state.message = ""; element.rerender(); } function flip(id) { if (state.failed !== null) { return; } if (id in state.revealed) { return; // Double click, ignore } else if (state.last === null) { // First click state.revealed[id] = true; state.last = id; // Record this ID number } else { state.revealed[id] = true; // Otherwise 2nd click const last = state.cards[state.last]; const current = state.cards[id]; if (current.symbol === last.symbol) { // Did it match? checkForWinCondition(); } else { showMessageAndFlipBack(id); } } } function checkForWinCondition() { // Successful match! Check for win. const { revealed, cards } = state; if (Object.keys(revealed).length === cards.length) { state.message = "You win!"; state.win = true; // Show win animation } else { state.message = "Nice match!"; } state.last = null; // Forget our last match } function showMessageAndFlipBack(id) { state.message = "No match."; state.failed = id; // Save id so we can flip it later setTimeout(failedFlipCallback, 1000); } </Script> <Style> h3 { background: #00000088; border-radius: 8px; text-align: center; color: white; font-weight: bold; } .board { display: grid; grid-template-rows: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr); grid-gap: 2px; width: 100%; height: 150px; width: 150px; } .board.hard { grid-gap: 1px; grid-template-rows: repeat(6, 1fr); grid-template-columns: repeat(6, 1fr); } .board > .card { background: #B90183; border: 2px solid black; border-radius: 1px; cursor: pointer; text-align: center; min-height: 15px; transition: background 0.3s, transform 0.3s; transform: scaleX(-1); padding-top: 2px; color: #B90183; } .board.hard > .card { border: none !important; padding: 0; } .board > .card.flipped { background: #FFFFFF; border: 2px solid #B90183; transform: scaleX(1); } @keyframes flipping { from { transform: scaleX(-1.1); background: #B90183; } to { transform: scaleX(1.0); background: #FFFFFF; } } </Style>

Easy to start

Writing your first component takes only 3 steps. You can follow these steps in your text editor by either 1) creating a new, blank HTML file or 2) opening an HTML file from one of your existing web apps or projects:

Step 1: Define Modulo

<script Modulo src="https://unpkg.com/mdu.js"> </script>

Step 2: Define Component

<script Modulo src="https://unpkg.com/mdu.js"> <Component name="Greet"> <Template> <h2>Hey there, <slot></slot>!</h2> </Template> </Component> </script>

Step 3: Use Elsewhere

<div> <x-Greet>Modulo</x-Greet> <x-Greet> my new <tt>HTML</tt> web component </x-Greet> </div>

Next Step: Continue Learning

Start Tutorial »

NPM

Run in shell to download template & start fresh:

npm init modulo

Why Modulo?

Zero set-up - Like the "golden age" of jQuery, just drop it into any HTML file. Unlike jQuery, that 1 file packs in many of the creature-comforts of a modern component framework, without any dependencies or new syntax to learn. You can even compile to a single JS file, directly from your browser!

A gentler learning curve - Modulo's declarative, synchronous HTML-first approach was designed to be easier for coding newbies to pick-up—in fact, useful components can be developed without writing any JavaScript code!

Progressive Web Apps Progressive Enhancement - Modulo makes few assumptions, and renders components mixed with plain HTML for easier integration into backend apps. SPA's and PWA's are fine and all, and Modulo could be used for these as well, but what about all our existing server-side apps in Ruby on Rails, Python, Go, or that one home-grown PHP monster that will never go away? Sprinkle in some Modulo components to make your "traditional" web apps shine.

Component Parts - Modular "Lego blocks" for your components! Combine Script, Style, State, Props, and Templates to make components that with these features.

Everything is transparent and swappable - Component Parts (CParts) are a flexible form of component middleware: You can create custom CParts to support your backend APIs, "mix and match" your favorite JS templating languages (e.g., JSX or Handlebars), or fine-tune DOM-resolution behavior, either globally, or on a per-component basis. Lifecycle callbacks are especially powerful, allowing you to hook your own code into the DOM or any steps of the rendering process.

A novel approach to component development - Modulo's separation of component middleware development allows JavaScript developers to write pure JS to wrap APIs, and component developers to tie it all together with "low-code".

"It's like React or Vue" - Modulo is "batteries-included" and uses familiar terminology. You get encapsulated components with state management, two-way data binding, props, computed values, smart DOM resolution, event directives, and more. You even get a simple importing and namespacing system, which can "self-pack" your components into a single JS file!

Clean & concise - The code-base of Modulo itself is intentionally kept short & sweet: It ways in at roughly 1500 SLOC, (mostly) 80 char line limit, 4 spaces of indent, and a low-complexity imperative style. When something goes wrong (which it probably will since it's new), it's easier to "git blame" Modulo.

A basis for whatever comes next - Writing your own framework? Modulo's core for loading & mounting CParts is only ~500 SLOC. Read the source code, fork away, and swap the "batteries-included" for your own!