Book Title: SimpliJS: Web Development for the Anti-Build Era
Subtitle: Build Modern, Reactive Web Apps with Zero Configuration and Pure HTML/JavaScript
Target Audience: Absolute beginners with no prior JavaScript or web development experience.
Core Teaching Philosophy: This book will teach modern web development by building real projects with SimpliJS. We will learn by doing, starting with simple HTML and progressively adding complexity, mirroring the framework's own philosophy.
Part 1: The Foundation - Understanding the Web & SimpliJS
Chapter 1: The Web Browser is Your New IDE
A simple history of the web: from static documents to dynamic applications.
The problem with modern web development: build tools, configuration, and complexity.
Introducing the "Anti-Build Manifesto": Why development should happen in the browser.
Your first lines of code: Setting up a project with zero configuration (just an HTML file!). We'll include SimpliJS via a CDN.
Chapter 2: Getting Started with SimpliJS
The three ways to install SimpliJS (as explained in the README): NPM, CDN, Local Download.
Creating your first "Hello, World!" application using the s-app directive.
Understanding the <script type="module"> tag and ES Modules (ESM).
Your first taste of reactivity: A simple counter without writing a single line of JavaScript, using s-state and s-click.
Chapter 3: The HTML-First Engine - Thinking Declaratively
The core concept: bringing logic back into HTML with s-* directives.
What are directives? (e.g., s-if, s-for, s-bind).
The power of {} Interpolation: Displaying dynamic data within your HTML.
Exercise: Build a personal profile card that dynamically displays your name and a "click count".
Part 2: Core Concepts - Building Reactivity
Chapter 4: Reactive State Management with s-state
Deep dive into s-state: Defining data as simple JavaScript objects directly in HTML.
How SimpliJS's "Direct-to-Proxy" engine works (conceptual, not code-heavy).
Working with different data types in state: strings, numbers, booleans, arrays, and objects.
Hands-on Project: A "To-Do List" application using only HTML-First features (s-state, s-for, s-bind, s-click).
Chapter 5: Control Flow in HTML
Conditional rendering with s-if, s-else, s-show, and s-hide.
When to use s-show vs. s-if (CSS display: none vs. DOM removal).
Rendering lists with the powerful s-for directive.
Using s-key for optimized list rendering and s-index to access the loop position.
Exercise: Enhance your To-Do List by adding the ability to filter tasks (e.g., show Active/Completed).
Chapter 6: User Interaction and Events
Handling user input with event directives: s-click, s-input, s-change, s-submit.
Accessing event data (like event.target.value) directly in your HTML expressions.
Form submission and automatic preventDefault() with s-submit.
Project: Build a simple "Reaction Tester" game that measures how fast you can click a button that moves.
Chapter 7: Two-Way Data Binding and Forms
s-bind: The simplest way to connect input fields to your state.
s-model: Handling complex form elements like checkboxes, radio buttons, and select dropdowns.
Introduction to built-in form validation with s-validate and s-error.
Hands-on Project: A "User Registration Form" with live validation feedback.
Chapter 8: Styling and Attributes
Dynamically manipulating element attributes with s-attr:src, s-attr:disabled, etc.
Reactive CSS classes with s-class: toggling classes based on state.
Inline styles with s-style.
Exercise: Build a theme switcher (light/dark mode) for your To-Do List app.
Part 3: The JavaScript Layer - Going Deeper
Chapter 9: Your First JavaScript Component
Moving beyond pure HTML: Introducing the component() function.
The anatomy of a SimpliJS component: render function and lifecycle.
Registering a custom HTML element (e.g., <my-counter>).
Replacing your first HTML-First app with a JavaScript-powered component.
Chapter 10: Programmatic Reactive State with reactive
Creating reactive objects with the reactive() function.
How Proxy-based reactivity works (a simple, beginner-friendly explanation).
Passing data into components with Props (props object).
Project: Convert your To-Do List app to use a main <todo-app> component with a reactive state.
Chapter 11: Component Logic: Methods, Events, and Refs
Defining methods inside your component and calling them from HTML (e.g., @click="myMethod").
Gaining direct access to DOM elements with ref() and the ref directive.
Handling component events with @click and similar (the on: syntax).
Exercise: Build an "Auto-focus Input" component that focuses itself when the page loads.
Chapter 12: Computed Properties and Watchers
Deriving values with computed() for efficient, cached data transformations.
Reacting to state changes with watch() to perform side effects (like saving to localStorage or API calls).
Exercise: Add a task summary to your To-Do List (e.g., "5 tasks remaining") using a computed property. Add an auto-save feature using a watcher.
Chapter 13: Component Lifecycle and Communication
Tapping into a component's life: onMount, onUpdate, onDestroy.
Communicating between components using the Global Event Bus (emit and on).
Content projection with Slots (<slot> and s-slot).
Project: Build a reusable <modal-dialog> component that uses slots for its title and body, and emits a "close" event.
Part 4: Advanced Features & The Plugin Ecosystem
Chapter 14: 🌉 The Bridge - A Universal Component Importer
The problem of ecosystem lock-in.
How use.react(), use.vue(), and use.svelte() work.
Importing and using a complex React component (like a chart or calendar) directly in your SimpliJS app without a build step.
Hands-on: Import a Confetti effect from React and trigger it when a user completes all To-Do items.
Chapter 15: 🕰️ The Time Vault - State Time Travel
What is time travel debugging?
Creating a "vault" state with reactive.vault().
Using .back(), .forward(), and .share() for powerful debugging and session sharing.
Exercise: Add "Undo/Redo" buttons to your To-Do List using The Time Vault.
Chapter 16: Building Single Page Applications (SPAs)
Introduction to client-side routing.
Setting up routes with s-route and the router outlet with s-view.
Creating navigation links with s-link.
Project: Convert your To-Do List into a multi-page SPA with "Home," "About," and "My Tasks" routes.
Chapter 17: The Plugin Ecosystem - Power-Ups for Your App
An overview of the 7 official plugins: @simplijs/auth, vault-pro, router, bridge-adapters, devtools, forms, ssg.
Deep Dive: Implementing professional authentication with @simplijs/auth.
Deep Dive: Enhancing forms with @simplijs/forms (auto-save, complex validation).
Using @simplijs/devtools for real-time debugging.
Chapter 18: Fetching Data from the Internet
The s-fetch directive: Making API calls directly from HTML.
Handling loading and error states with s-loading and s-error (fetch version).
Using the JavaScript fetch API within a component for more complex scenarios.
Project: Build a "Weather Dashboard" that fetches data from a public API.
Part 5: Production & Beyond
Chapter 19: SEO and Static Site Generation (SSG)
Why SEO matters for modern websites.
Managing meta tags with setSEO, setJsonLd, and other SEO helpers.
Introduction to the built-in SSG engine (ssg.js).
Generating a fully static, SEO-optimized version of your SPA with sitemaps and preloading.
Step-by-step: Run the SSG tool on your Weather Dashboard project.
Chapter 20: Performance and Optimization
Understanding SimpliJS's tiny footprint (<20KB).
Using s-lazy for lazy-loading images.
Freezing static content with s-once.
Skipping compilation in subtrees with s-ignore.
Running a Lighthouse audit on your app to see its perfect score.
Chapter 21: Security Best Practices
Understanding the security model of new Function() and with().
The golden rule: Never bind unsanitized, user-generated content to directives.
Sanitizing user input before displaying it with s-html.
Building secure applications with confidence.
Chapter 22: Your Journey Ahead
Recap of everything learned: from a complete beginner to a SimpliJS expert.
Contributing to the SimpliJS ecosystem.
Resources: GitHub Discussions, Community, and further learning.
The future of web development: The rise of the "No-Build" era.
Appendix
A. SimpliJS Directive Quick Reference: A complete table of all 54 directives with descriptions and examples.
B. Plugin API Reference: A summary of the 7 plugins and their core methods.
C. Project Source Code: Links to the final code for all major projects in the book.
D. Glossary of Terms: From "Reactivity" to "Tree Shaking" explained in simple language.
This structure provides a step-by-step, project-based learning path. It starts with the simplest HTML concepts and gradually introduces JavaScript and advanced features, ensuring a beginner can follow along and ultimately master SimpliJS. The final chapters on SSG, performance, and security will equip them to build and deploy real-world, production-grade applications.
Part 1: The Foundation - Understanding the Web & SimpliJS
Chapter 1: The Web Browser is Your New IDE
Welcome to the beginning of your journey into modern web development. If you've ever felt intimidated by complex programming setups, long terminal commands, or cryptic error messages about "webpack" and "babel," you're in the right place. This book takes a completely different approach—one that puts the power back where it belongs: in your hands and in your web browser.
1.1 A Simple History: From Static Documents to Dynamic Applications
Let's take a quick trip back in time. When the World Wide Web was first created in the early 1990s, it was designed as a way to share documents. These were simple text files with a special markup language called HTML (HyperText Markup Language). You'd write a document on your computer, save it with a .html extension, and anyone in the world could open it in their browser and read it.
Your first HTML document might have looked something like this:
<!DOCTYPE html>
<html>
<head>
<title>My First Web Page</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>This is a simple document.</p>
</body>
</html>If you saved this as index.html and opened it in a browser, you'd see a heading and a paragraph. That's it. No interactivity, no animations, no complex applications—just static text and images.
The Birth of JavaScript
In 1995, a new language called JavaScript was created to make web pages dynamic. Suddenly, you could add buttons that did things, validate forms, and create simple animations. JavaScript was (and still is) the language that runs in every single web browser on the planet.
Here's what early interactive JavaScript looked like:
<!DOCTYPE html>
<html>
<head>
<title>Interactive Page</title>
</head>
<body>
<h1>Welcome!</h1>
<button onclick=\"alert(\'Hello!\')\">Click Me</button>
<script>
*// This is JavaScript*
let count = 0;
function increment() {
count = count + 1;
document.getElementById(\'counter\').textContent = count;
}
</script>
<p>You\'ve clicked: <span id=\"counter\">0</span> times</p>
<button onclick=\"increment()\">+1</button>
</body>
</html>This worked! But as web applications grew more complex, this approach became messy. You had to manually track which parts of the page needed updating, and your code quickly became a tangled web of getElementById and manual DOM manipulations.
The Rise (and Complexity) of Modern Frameworks
To manage this complexity, developers created frameworks and libraries: React, Vue, Angular, Svelte, and many others. These tools were revolutionary—they made building complex applications much more organized and efficient.
However, they came with a hidden cost. To use them, you suddenly needed:
Node.js installed on your computer
Package managers like npm or yarn
Build tools like Webpack, Vite, or Rollup
Transpilers like Babel to convert modern code to older JavaScript
Configuration files with hundreds of lines of settings
Terminal commands like npm install, npm run dev, npm build
What used to be as simple as "create an HTML file and open it" became a complex ritual of terminal commands and configuration. For beginners, this is often the first and biggest hurdle. For experienced developers, it's hours of debugging build configurations instead of writing actual code.
1.2 The Problem: Build Tools, Configuration, and Complexity
Let's look at what it typically takes to start a new project with a popular framework today:
Step 1: Open your terminal
Step 2: Type something like npx create-react-app
my-app (this downloads thousands of files)
Step 3: Wait 2-5 minutes for everything to
install
Step 4: Navigate into the folder: cd my-app
Step 5: Start the development server: npm start
Step 6: Your browser opens, but... where's my code? You
now have a project with:
40,000+ files in node_modules
5+ configuration files you didn't write
A complex folder structure
No idea how any of it actually works
If something goes wrong (and it often does), the error messages are cryptic: "Module not found: Error: Can't resolve 'something' in..." or "Babel loader configuration is invalid." For a beginner, this is like being asked to fix a car engine before you've even learned to drive.
This is the problem SimpliJS was created to solve.
1.3 Introducing the "Anti-Build Manifesto"
SimpliJS is built on a radical idea: development should happen in the browser, not in a terminal full of build errors. This idea is captured in three core pillars that form the "Anti-Build Manifesto":
Pillar 1: The Anti-Build Movement
We believe that the complexity of modern web development has spiraled out of control. Every minute spent configuring Vite, Webpack, or Babel is a minute lost on your actual product. The browser is incredibly powerful—it understands HTML, CSS, and JavaScript natively. Why force it to consume transformed, compiled, and packaged code when it can run the real thing?
With SimpliJS, you write standard JavaScript modules that run directly in the browser. There's no build step because there's nothing to build. What you write is what runs.
Pillar 2: HTML-First Logic
Traditional frameworks often start with JavaScript. You write JavaScript to create components, which then generate HTML. With SimpliJS, we flip this around. We bring reactivity back to its roots—in the HTML itself.
This means you can build fully reactive applications using simple HTML attributes. JavaScript becomes an enhancement, not a requirement. For beginners, this is revolutionary: you can create interactive apps using skills you already have (HTML) while gradually learning JavaScript.
Pillar 3: Zero-Configuration Excellence
Remember when creating a website was as simple as saving a file? SimpliJS brings that back. There are no configuration files to write, no build tools to install, no terminal commands to memorize. You can start building immediately, and your application will run exactly the same in development and production because there's no transformation layer.
1.4 Your First Lines of Code: Zero Configuration Setup
Enough theory—let's write some code! This is where the magic happens. You're going to create your first SimpliJS application right now, and you won't need to install a single thing.
Step 1: Create a new HTML file
Open any text editor (Notepad, VS Code, Sublime Text, or even TextEdit on Mac) and create a new file called index.html. If you're on Windows, make sure to save it with the .html extension, not .txt.
Step 2: Add the basic HTML structure
Type (or copy) this into your file:
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width,
initial-scale=1.0\">
<title>My First SimpliJS App</title>
</head>
<body>
<h1>Welcome to SimpliJS!</h1>
*<!-- Our SimpliJS app will go here -->*
</body>
</html>Step 3: Include SimpliJS via CDN
This is the key step. We're going to include SimpliJS from a CDN (Content Delivery Network). Think of a CDN as a publicly accessible library on the internet—we can borrow SimpliJS without downloading anything.
Add this line right before the closing </body> tag:
*<!-- Include SimpliJS -->*
<script type=\"module\">
import { createApp } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
console.log(\'SimpliJS loaded successfully!\');
</script>
</body>Your complete file should now look like this:
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width,
initial-scale=1.0\">
<title>My First SimpliJS App</title>
</head>
<body>
<h1>Welcome to SimpliJS!</h1>
*<!-- Our SimpliJS app will go here -->*
<script type=\"module\">
import { createApp } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
console.log(\'SimpliJS loaded successfully!\');
</script>
</body>
</html>Step 4: Open it in your browser
Double-click the index.html file you just created. It will open in your default web browser.
Now, open the browser's Developer Tools:
Chrome/Edge: Right-click anywhere on the page and select "Inspect", then click the "Console" tab
Firefox: Right-click and select "Inspect Element", then click the "Console" tab
Safari: You need to enable Developer Tools in Preferences first, then right-click and select "Inspect Element"
In the console, you should see the message: SimpliJS loaded successfully!
Congratulations! You've just set up a SimpliJS project with zero configuration. No terminal, no npm install, no build errors—just a simple HTML file and a browser.
1.5 Understanding <script type="module"> and ES Modules
You might have noticed something special in our script tag: type="module". This tells the browser to treat this JavaScript as an ES Module (ECMAScript Module). Let's understand what this means in simple terms.
What are Modules?
Think of modules as separate, self-contained pieces of code that can be imported and used where needed. Before modules, if you wanted to use code from another file, you had to add multiple script tags in the correct order, and everything shared the same global space. This led to conflicts and messy code.
With ES Modules, each file has its own scope. You explicitly import what you need, and export what you want to share.
The import Statement
In our code, we wrote:
import { createApp } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';This is saying: "From the SimpliJS library located at this URL, please give me access to the createApp function."
The { createApp } syntax is called destructuring. It means we're only importing that specific item from the library, not the entire library. This keeps things efficient.
Why URLs Work Here
Notice that we're importing from a full URL, not a local file path like ./simplijs.js. This is a powerful feature of ES Modules—they can import code directly from the internet. The browser fetches the module just like it fetches images or CSS files.
This is why we don't need to install anything. The browser does all the work of downloading SimpliJS when it loads our page.
1.6 Your First Taste of Reactivity
Now for the really exciting part. Let's create something interactive without writing any JavaScript logic—just using HTML attributes.
Replace the content inside the <body> tag with this:
<body>
<div s-app s-state=\"{ count: 0 }\">
<h1>My First Reactive Counter</h1>
<p>Current count: {count}</p>
<button s-click=\"count++\">Click Me!</button>
<button s-click=\"count = 0\">Reset</button>
</div>
<script type=\"module\">
import { createApp } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// Initialize the app*
createApp().mount(\'\[s-app\]\');
</script>
</body>Save the file and refresh your browser. You should see:
A heading that says "My First Reactive Counter"
A paragraph showing "Current count: 0"
Two buttons: "Click Me!" and "Reset"
Now click the "Click Me!" button. Watch the number increase! Click "Reset" and watch it go back to zero.
What just happened? Let's break it down:
| Directive | Purpose | What it does |
|---|---|---|
| s-app | App boundary | Tells SimpliJS: "Everything inside here is a reactive application" |
| s-state="{ count: 0 }" | State declaration | Creates a reactive variable called count starting at 0 |
| {count} | Interpolation | Displays the current value of count in the HTML |
| s-click="count++" | Event handler | When clicked, increment the count variable by 1 |
| s-click="count = 0" | Event handler | When clicked, set count back to 0 |
You've just built a fully functional interactive application with:
Zero JavaScript (the directives are HTML attributes)
Zero configuration (just an HTML file)
Reactive updates (the display updates automatically when count changes)
This is the power of SimpliJS's HTML-First approach. You're writing HTML, but you're getting modern, reactive behavior.
Understanding the mount Method
You might be wondering about the last line of our script:
createApp().mount(\'\[s-app\]\');This does two things:
createApp() initializes a new SimpliJS application
.mount('[s-app]') tells SimpliJS to find all elements with the attribute s-app and activate them
The [s-app] is a CSS selector—the same kind you'd use in CSS to style elements. It means "find elements with an attribute called 's-app'." This is how SimpliJS knows which parts of your page should be reactive.
1.7 What Makes This Different?
Let's compare what we just did with what you'd need to do to create the same counter in other frameworks:
React (with Create React App)
text
npx create-react-app my-counter
cd my-counter
npm start
// Then write 15+ lines of component code
// Wait for the dev server to compile
Vue (with Vue CLI)
text
npm install -g @vue/cli
vue create my-counter
cd my-counter
npm run serve
// Write template and script sections
SimpliJS
text
// Create an HTML file
// Add a script tag to import SimpliJS
// Write HTML with s-* directives
// Double-click the file
The difference is profound. With SimpliJS, the barrier to entry is virtually zero. You can go from idea to working prototype in seconds, not minutes or hours.
1.8 Exercise: Personalize Your Counter
Now it's your turn to experiment. Try modifying the example to:
Change the starting value from 0 to 10
Add a step size so each click increases by 2 instead of 1
Add a third button that decreases the count
Display a message when the count reaches a certain number (e.g., "You've reached 10!")
Here's a hint for number 4: you can use JavaScript expressions inside {}. Try something like:
<p s-if=\"count >= 10\">🎉 You\'ve reached 10 or more!</p>Chapter 1 Summary
In this first chapter, you've learned:
The history of the web, from static documents to complex applications
Why modern web development has become unnecessarily complicated
The three pillars of the Anti-Build Manifesto
How to set up a SimpliJS project with zero configuration
What ES Modules are and how imports work
How to create your first reactive application using only HTML directives
How SimpliJS's HTML-First approach makes development accessible to everyone
You've written real, working code and seen immediate results. This is the foundation—in the next chapter, we'll build on it by exploring all the ways you can install and use SimpliJS in your projects.
Remember: every expert was once a beginner. The simplicity you're experiencing now is intentional. It allows you to focus on learning concepts, not fighting tools. As we progress through this book, you'll gradually add JavaScript to your toolkit, always building on this solid foundation.
Chapter 2: Getting Started with SimpliJS
In Chapter 1, we dipped our toes into SimpliJS by creating a simple counter using the CDN approach. Now it's time to systematically explore all the ways you can start using SimpliJS in your projects. By the end of this chapter, you'll understand the three installation methods thoroughly and be comfortable creating basic reactive applications.
2.1 The Three Ways to Install SimpliJS
The SimpliJS README mentions three installation methods. Each serves different needs and use cases. Let's explore them in detail:
Method 1: Using NPM (Recommended for Larger Applications)
NPM (Node Package Manager) is the standard package manager for JavaScript. If you're building a larger application, possibly with a backend, or if you're already working in a Node.js environment, this method makes the most sense.
What you need:
Node.js installed on your computer
Basic familiarity with the terminal/command line
Step-by-step process:
Open your terminal (Command Prompt on Windows, Terminal on Mac/Linux)
Navigate to your project folder (or create one):
mkdir my-simplijs-project
cd my-simplijs-project
3. **Initialize a package.json file** (optional but recommended):
bash
npm init -yThis creates a package.json file that tracks your project dependencies.
npm install \@simplijs/simplijs
5. **Create an HTML file** and import SimpliJS from node_modules:
html
<!DOCTYPE html>
<html>
<head>
<title>NPM Installed SimpliJS</title>
</head>
<body>
<div s-app s-state=\"{ message: \'Hello from NPM!\' }\">
<h1>{message}</h1>
</div>
<script type=\"module\">
import { createApp } from
\'./node_modules/@simplijs/simplijs/dist/simplijs.min.js\';
createApp().mount(\'\[s-app\]\');
</script>
</body>
</html>Pros:
Works offline after installation
Easy to manage with other NPM packages
Version locking ensures consistency
Can be bundled with other assets if desired
Cons:
Requires Node.js installation
Adds an extra step before coding
Creates a node_modules folder (can be large)
Method 2: Using ES Modules via CDN (Recommended for Learning)
This is the method we used in Chapter 1. It's perfect for beginners, prototyping, and sharing code snippets.
What you need:
Step-by-step process:
Create an HTML file anywhere on your computer
Add the SimpliJS import using a CDN URL:
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// Your code here*
</script>
3. **Start coding immediately!**The CDN URL explained:
text
https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js
└──────────┬─────────┘ └───┬───┘ └──────┬──────┘ └─────┬─────┘ └──────┬──────┘
jsDelivr GitHub Username Repository Version File
(CDN) (platform) Name path
Alternative CDN providers:
You can also use other CDNs like unpkg:
<script type=\"module\">
import { createApp } from
\'https://unpkg.com/@simplijs/simplijs@3.2.0/dist/simplijs.min.js\';
</script>Pros:
Zero setup required
Always serves the latest version
No local files to manage
Perfect for learning and prototyping
Code can be shared as a single HTML file
Cons:
Requires internet connection
Slightly slower initial load (but cached after first visit)
Not ideal for offline development
Method 3: Local Download (For Offline Development)
This method gives you complete control. You download the SimpliJS file once and use it locally forever.
Step-by-step process:
Download the SimpliJS file from the GitHub repository:
Navigate to the dist/ folder
Download simplijs.min.js (the minified production version)
Create a project folder and place the downloaded file inside:
text
my-project/
├── index.html
└── simplijs.min.js
<!DOCTYPE html>
<html>
<head>
<title>Local SimpliJS</title>
</head>
<body>
<div s-app s-state=\"{ message: \'Hello from local file!\' }\">
<h1>{message}</h1>
</div>
<script type=\"module\">
import { createApp } from \'./simplijs.min.js\';
createApp().mount(\'\[s-app\]\');
</script>
</body>
</html>Pros:
Works completely offline
Full control over the framework version
No external dependencies
Fastest load times (local file)
Cons:
Manual updates required
Need to manage the file yourself
Not as convenient for quick prototypes
Which Method Should You Choose?
| Your Situation | Recommended Method |
|---|---|
| Complete beginner | CDN (Method 2) |
| Quick prototyping | CDN (Method 2) |
| Building a serious app | NPM (Method 1) |
| No internet connection | Local (Method 3) |
| Want to share code easily | CDN (Method 2) |
| Enterprise environment | NPM or Local |
Throughout this book, we'll primarily use the CDN method because it's the simplest and allows you to focus on learning rather than setup. Every example will be a single HTML file you can copy and run immediately.
2.2 Creating Your First "Hello, World!" Application
Let's create a proper "Hello, World!" application that demonstrates the core concepts. We'll expand on the counter from Chapter 1 and add more elements.
Create a new HTML file called hello-simplijs.html:
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width,
initial-scale=1.0\">
<title>Hello, SimpliJS!</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 40px auto;
padding: 20px;
line-height: 1.6;
}
.greeting {
background-color: #f0f0f0;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
input {
padding: 8px;
font-size: 16px;
width: 100%;
margin: 10px 0;
}
</style>
</head>
<body>
<h1>🌐 Hello, SimpliJS!</h1>
*<!-- Our SimpliJS app container -->*
<div s-app s-state=\"{
name: \'World\',
greeting: \'Hello\',
showInput: true
}\">
*<!-- Dynamic greeting -->*
<div class=\"greeting\">
<h2>{greeting}, {name}!</h2>
<p s-if=\"name === \'World\'\">
👋 This is your first reactive app with SimpliJS!
</p>
<p s-if=\"name !== \'World\'\">
✨ Nice to meet you, {name}!
</p>
</div>
*<!-- Input controls -->*
<div>
<label>
<strong>Your name:</strong>
<input type=\"text\" s-bind=\"name\" placeholder=\"Enter your name\">
</label>
<p>
<strong>Choose a greeting:</strong>
<select s-model=\"greeting\">
<option value=\"Hello\">Hello</option>
<option value=\"Hi\">Hi</option>
<option value=\"Hey\">Hey</option>
<option value=\"Greetings\">Greetings</option>
</select>
</p>
<label>
<input type=\"checkbox\" s-model=\"showInput\">
Show/Hide Input
</label>
</div>
*<!-- Conditional section -->*
<div s-show=\"showInput\" style=\"margin-top: 20px; padding: 15px;
background: #e8f4fd; border-radius: 8px;\">
<h3>✨ Input is visible!</h3>
<p>You can hide this entire section by unchecking the box above.</p>
<p>Notice that the name display above still updates even when this is
hidden.</p>
</div>
*<!-- Counter example from Chapter 1 -->*
<div style=\"margin-top: 30px; padding: 20px; background: #f9f9f9;
border-radius: 8px;\">
<h3>Interactive Counter</h3>
<p s-state=\"{ count: 0 }\">
Current count: {count}
<button s-click=\"count++\">+1</button>
<button s-click=\"count--\">-1</button>
<button s-click=\"count = 0\">Reset</button>
</p>
<p s-if=\"count >= 10\">🎉 You\'ve reached 10 or more!</p>
<p s-if=\"count <= -5\">⚠️ That\'s very negative!</p>
</div>
</div>
<script type=\"module\">
import { createApp } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// Initialize the app*
const app = createApp();
app.mount(\'\[s-app\]\');
console.log(\'Hello, SimpliJS app is running!\');
</script>
</body>
</html>Save this file and open it in your browser. Let's explore what each part does:
Understanding the State Object
Look at this line:
<div s-app s-state=\"{
name: \'World\',
greeting: \'Hello\',
showInput: true
}\">The s-state attribute defines our application's reactive data. It's written in JSON-like syntax:
name: 'World' - a text string with the default value "World"
greeting: 'Hello' - another text string
showInput: true - a boolean (true/false) value
Any of these values can be accessed anywhere inside this div using {variableName} syntax.
Two-Way Binding with s-bind
The input field uses s-bind:
<input type=\"text\" s-bind=\"name\" placeholder=\"Enter your name\">This creates two-way binding. When you type in the input, the name state updates automatically. And whenever name changes, the input's value updates too. This is why the greeting updates as you type—it's all connected!
Select Dropdown with s-model
For the greeting selector, we use s-model:
<select s-model=\"greeting\">
<option value=\"Hello\">Hello</option>
<option value=\"Hi\">Hi</option>
<option value=\"Hey\">Hey</option>
<option value=\"Greetings\">Greetings</option>
</select>
s-model is similar to s-bind but specifically designed for form elements
like selects, checkboxes, and radio buttons.Conditional Rendering with s-if and s-show
We demonstrate two ways to conditionally show content:
s-if completely removes or adds elements to the DOM:
<p s-if=\"name === \'World\'\">
👋 This is your first reactive app with SimpliJS!
</p>
s-show simply hides elements using CSS (display: none), keeping them in
the DOM:
html
<div s-show=\"showInput\">
*<!-- content -->*
</div>The difference: s-if is better for rarely-changing conditions, while s-show is faster for frequently toggled elements.
Nested State
Notice we have two s-state declarations:
One on the main container with three variables
Another on the counter section with just count
Each s-state creates its own scope. The counter's count is separate from the main state and only affects that specific section. This is called scoped reactivity.
2.3 Understanding the <script type="module"> Tag Deeply
Now that you've seen it in action, let's understand what's really happening with our script tag.
What Makes a Module Different?
When you add type="module" to a script tag, several important things happen:
Strict mode is automatically enabled - This means certain unsafe JavaScript patterns are forbidden, making your code more secure and less error-prone.
Variables are scoped to the module - Without modules, variables declared with var or let at the top level become global. With modules, each file has its own scope.
import and export statements become available - You can only use these inside modules.
The module is deferred by default - The script loads in the background and only executes after the HTML is parsed, similar to adding defer attribute.
Modules are cached - If you import the same module from multiple places, it's only loaded once.
Comparing Module vs. Non-Module Scripts
Regular script:
<script src=\"script.js\"></script>
*<!-- Variables become global -->*
*<!-- Executes immediately, blocking HTML parsing -->*
*<!-- Can\'t use import/export -->*Module script:
<script type=\"module\" src=\"script.js\"></script>
*<!-- Variables are scoped to the module -->*
*<!-- Executes after HTML is parsed -->*
*<!-- Can use import/export -->*
*<!-- Can import from URLs -->*Multiple Exports from SimpliJS
In our import statement, we're importing multiple items:
import { createApp, component, reactive } from \'\...\';The curly braces {} mean we're importing specific named exports. SimpliJS exports many functions, and we can choose which ones we need:
createApp - Creates a new application instance
component - Registers new custom components
reactive - Creates reactive state objects
And many more we'll learn later
If you wanted to import everything, you could use:
import * as SimpliJS from \'\...\';
*// Then use SimpliJS.createApp(), SimpliJS.component(), etc.*
But it\'s better practice to import only what you need.2.4 More HTML-First Examples
Now that you understand the basics, let's explore more HTML-First capabilities. Each example demonstrates different directives you can use without writing any JavaScript.
Example 1: Dynamic Styling with s-class and s-style
<div s-app s-state=\"{
isActive: true,
bgColor: \'#ffeb3b\',
fontSize: 16
}\">
<style>
.active { font-weight: bold; color: green; border: 2px solid green; }
.inactive { opacity: 0.7; color: gray; }
.highlight { background-color: yellow; padding: 10px; }
</style>
<h2>Dynamic Styling Demo</h2>
*<!-- Dynamic classes -->*
<div s-class=\"{ active: isActive, inactive: !isActive, highlight: true
}\">This div's classes change based on state
</div>
<!-- Toggle the active state -->
<button s-click="isActive = !isActive">
Toggle Active (currently: {isActive})
</button>
<!-- Inline styles with s-style -->
<div s-style="{
backgroundColor: bgColor,
fontSize: fontSize + 'px',
padding: '20px',
marginTop: '20px',
transition: 'all 0.3s'
}">
<p>This div's styles are reactive!</p>
<p>Background: {bgColor}</p>
<p>Font size: {fontSize}px</p>
</div>
<!-- Control sliders -->
<div>
<label>Background color:</label>
<input type="color" s-bind="bgColor" value="#ffeb3b">
</div>
<div>
<label>Font size: {fontSize}px</label>
<input type="range" min="12" max="32" s-bind="fontSize">
</div>
</div>
Example 2: Working with Lists using s-for
<div s-app s-state=\"{
todos: \[
{ id: 1, text: \'Learn SimpliJS\', done: false },
{ id: 2, text: \'Build a project\', done: false },
{ id: 3, text: \'Master reactivity\', done: false }
\],
newTodo: \'\'
}\">
<h2>📝 Todo List (with s-for)</h2>
*<!-- Add new todo -->*
<div>
<input s-bind=\"newTodo\" placeholder=\"Add a new task\">
<button s-click=\"todos.push({
id: todos.length + 1,
text: newTodo,
done: false
}); newTodo = \'\'\">
Add
</button>
</div>
*<!-- List todos with s-for -->*
<ul>
<li s-for=\"todo, index in todos\"
s-style=\"{
textDecoration: todo.done ? \'line-through\' : \'none\',
opacity: todo.done ? 0.7 : 1
}\">
<span s-click=\"todo.done = !todo.done\">
{index + 1}. {todo.text}
</span>
<button s-click=\"todos.splice(index, 1)\">❌</button>
</li>
</ul>
*<!-- Summary with computed values -->*
<p>
Total: {todos.length} \|
Completed: {todos.filter(t => t.done).length} \|
Remaining: {todos.filter(t => !t.done).length}
</p>
*<!-- Bulk actions -->*
<button s-click=\"todos.forEach(t => t.done = true)\">Complete
All</button>
<button s-click=\"todos = todos.filter(t => !t.done)\">Clear
Completed</button>
</div>Example 3: Form Handling and Validation
<div s-app s-state=\"{
formData: {
username: \'\',
email: \'\',
age: 18,
subscribe: true
},
submitted: false
}\">
<h2>📋 Form Demo</h2>
<form s-submit=\"submitted = true\">
<div>
<label>Username (minimum 3 characters):</label>
<input s-bind=\"formData.username\" s-validate=\"min:3\">
<span s-error=\"formData.username\" style=\"color: red;\"></span>
</div>
<div>
<label>Email:</label>
<input type=\"email\" s-bind=\"formData.email\" s-validate=\"email\">
<span s-error=\"formData.email\" style=\"color: red;\"></span>
</div>
<div>
<label>Age:</label>
<input type=\"number\" s-bind=\"formData.age\" min=\"1\" max=\"120\">
</div>
<div>
<label>
<input type=\"checkbox\" s-model=\"formData.subscribe\">Subscribe to newsletter
</label>
</div>
<button type="submit">Submit</button>
<button type="button" s-click="formData = {
username: '', email: '', age: 18, subscribe: true
}">Reset</button>
</form>
<!-- Live preview of form data -->
<div s-show="formData.username || formData.email"
style="margin-top: 20px; padding: 10px; background: #e3f2fd;">
<h3>Live Preview:</h3>
<pre>{JSON.stringify(formData, null, 2)}</pre>
</div>
<!-- Submission confirmation -->
<div s-if="submitted" style="color: green; margin-top: 10px;">
✅ Form submitted! (Check console)
</div>
</div>
2.5 Debugging Tips for Beginners
As you start building with SimpliJS, you'll occasionally run into issues. Here are common problems and how to solve them:
Problem 1: Nothing is updating reactively
Symptoms: You change an input or click a button, but the display doesn't update.
Solutions:
Check that you have s-app on a parent element
Make sure you called createApp().mount('[s-app]') in your script
Check the browser console (F12) for error messages
Ensure your script has type="module"
Problem 2: Directives aren't working
Symptoms: You added s-click or s-if but nothing happens.
Solutions:
Verify directive spelling (it's s-click, not onclick)
Check that your state variables are defined in s-state
Make sure your expressions are valid JavaScript
Look for missing quotes in attribute values
Problem 3: The dreaded "undefined" or "NaN" in displays
Symptoms: You see "undefined" or "NaN" where a value should be.
Solutions:
Check that the variable name in {} matches the one in s-state
Ensure you're not trying to do math on strings
Use console.log() inside expressions to debug:
<div s-click=\"console.log(\'Clicked!\', count)\">Click</div>Using the Browser's Developer Tools
The browser's developer tools are your best friend. To open them:
Windows/Linux: Press F12 or Ctrl+Shift+I
Mac: Press Cmd+Option+I
Once open, pay attention to:
Console tab: Shows errors and anything you console.log()
Elements tab: Lets you inspect the HTML and see if directives are present
Network tab: Shows if SimpliJS loaded correctly from the CDN
2.6 Exercise: Build a Personal Dashboard
Now it's your turn to combine everything you've learned. Create a personal dashboard that includes:
A profile section with your name, title, and a short bio
A theme selector that changes the page colors (light/dark or custom colors)
A skills list you can add to (use s-for and an input)
A goal tracker with checkboxes for daily goals
A greeting that changes based on time of day (morning/afternoon/evening)
Here's a starter template:
<!DOCTYPE html>
<html>
<head>
<title>My SimpliJS Dashboard</title>
<style>
/* Add your styles here */
body { font-family: Arial; padding: 20px; }
.dashboard { max-width: 800px; margin: 0 auto; }
.card { border: 1px solid #ddd; padding: 15px; margin: 10px 0;
border-radius: 8px; }
/* Add light/dark mode styles */
</style>
</head>
<body>
<div class=\"dashboard\" s-app s-state=\"{
// Initialize your state here
name: \'Your Name\',
theme: \'light\',
skills: \[\'HTML\', \'CSS\'\],
newSkill: \'\',
goals: \[
{ text: \'Learn SimpliJS\', done: false }
\]
}\">
<h1>Welcome, {name}! <span s-if=\"/* time-based greeting */\">Good
{timeGreeting}</span></h1>
*<!-- Your components here -->*
</div>
<script type=\"module\">
import { createApp } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
createApp().mount(\'\[s-app\]\');
</script>
</body>
</html>Bonus Challenges:
Add localStorage persistence (so your data survives page refreshes)
Create a simple "focus timer" with start/pause/reset
Add charts or graphs (hint: you can use s-html to inject HTML from libraries)
Chapter 2 Summary
In this chapter, you've mastered:
The three installation methods for SimpliJS (NPM, CDN, Local)
Creating complete "Hello, World!" applications with multiple interactive elements
Deep understanding of type="module" and ES Modules
Working with various directives: s-bind, s-model, s-if, s-show, s-class, s-style, s-for
Form handling and basic validation
Debugging common issues
Building a personal dashboard project
You're no longer just reading about SimpliJS—you're actively using it to create real, interactive web applications. The foundation is solid. In the next chapter, we'll dive deep into the HTML-First engine and understand exactly how SimpliJS works under the hood, so you can leverage its full power with confidence.
Remember: every feature you use is running natively in your browser, with zero build steps. This is the power of the Anti-Build Movement, and you're now part of it.
Chapter 3: The HTML-First Engine - Thinking Declaratively
Welcome to the heart of SimpliJS. In this chapter, we'll explore the revolutionary concept that sets SimpliJS apart from every other framework: the HTML-First Engine. By the end of this chapter, you'll understand not just how to use HTML-First features, but why they work the way they do and how to think in a declarative way.
3.1 The Core Concept: Bringing Logic Back into HTML
When the web was young, HTML was purely for structure, and JavaScript was for behavior. You'd write your HTML first, then separately write JavaScript to find elements and manipulate them. This separation seemed logical, but it created a problem: your logic (what should happen) became separated from your structure (where it should happen).
The Traditional (Imperative) Way
Here's how you might create a simple counter using traditional JavaScript:
*<!-- HTML: Just the structure -->*
<button id=\"myButton\">Click me</button>
<p>Count: <span id=\"countDisplay\">0</span></p>
*<!-- JavaScript: All the logic -->*
<script>
*// Find elements*
const button = document.getElementById(\'myButton\');
const display = document.getElementById(\'countDisplay\');
*// Initialize state*
let count = 0;
*// Define behavior*
function updateDisplay() {
display.textContent = count;
}
*// Attach event listener*
button.addEventListener(\'click\', function() {
count++;
updateDisplay();
});
*// Initial display*
updateDisplay();
</script>This is called imperative programming. You're telling the browser exactly how to do everything: find this element, attach this listener, update this text. It works, but for complex applications, this becomes a nightmare of disconnected code.
The SimpliJS (Declarative) Way
Now here's the same counter in SimpliJS:
<div s-app s-state=\"{ count: 0 }\">
<button s-click=\"count++\">Click me</button>
<p>Count: {count}</p>
</div>This is declarative programming. You're telling the browser what you want, not how to do it:
"I want a button that increments count when clicked"
"I want the count displayed here, updating automatically"
The "how" (finding elements, attaching listeners, updating displays) is handled by SimpliJS automatically.
3.2 What are Directives? Understanding s-* Attributes
Directives are special HTML attributes that start with s-. They tell SimpliJS to add special behavior to elements. Think of them as instructions that extend HTML's capabilities.
Categories of Directives
SimpliJS has several categories of directives, each serving a different purpose:
| Category | Purpose | Examples |
|---|---|---|
| Data Binding | Connect HTML to state | s-bind, s-text, s-html, s-value |
| Control Flow | Conditionally show/hide content | s-if, s-else, s-show, s-for |
| Event Handling | Respond to user actions | s-click, s-input, s-submit |
| Attribute Binding | Dynamically set attributes | s-attr:src, s-class, s-style |
| Form Handling | Manage form inputs | s-model, s-validate, s-error |
| Component Features | Work with components | s-component, s-prop, s-slot |
How Directives Work (Conceptually)
When your page loads, SimpliJS scans the DOM for elements with s-* attributes. For each directive, it:
Parses the directive value (like count++ or name === 'World')
Creates connections between the element and your state
Sets up observers that watch for state changes
Updates the DOM automatically when state changes
All of this happens behind the scenes. You just write your intentions in HTML.
3.3 The Power of {} Interpolation: Displaying Dynamic Data
One of the most fundamental HTML-First features is interpolation—using {} to insert dynamic values into your HTML.
Basic Interpolation
<div s-app s-state=\"{
firstName: \'John\',
lastName: \'Doe\',
age: 30
}\">
<p>Full name: {firstName} {lastName}</p>
<p>Age next year: {age + 1}</p>
<p>Can vote: {age >= 18 ? \'Yes\' : \'No\'}</p>
</div>
Any valid JavaScript expression can go inside {}. This includes:Variables: {name}
Math: {price * quantity}
String concatenation: {firstName + ' ' + lastName}
Ternary operators: {isLoggedIn ? 'Welcome' : 'Login'}
Function calls: {items.length}, {text.toUpperCase()}
Interpolation in Different Contexts
You can use {} anywhere in your HTML text:
<div s-app s-state=\"{ user: \'Alice\', count: 5 }\">
*<!-- In paragraphs -->*
<p>Hello, {user}!</p>
*<!-- In headings -->*
<h1>Score: {count}</h1>
*<!-- In attributes (special case) -->*
<img src=\"/images/{user}.jpg\" s-attr:src=\"\'/images/\' + user +
\'.jpg\'\">
*<!-- In style tags (with caution) -->*
<style>
.user-{user} { color: blue; }
</style>
*<!-- In button text -->*
<button>Click count: {count}</button>
</div>Important: For attributes, you usually need to use s-attr: instead of direct interpolation because HTML attributes don't evaluate JavaScript expressions. We'll cover this in detail later.
Complex Expressions
You can write fairly complex expressions inside {}:
<div s-app s-state=\"{
items: \[\'apple\', \'banana\', \'orange\'\],
searchTerm: \'\',
showCount: true
}\">
*<!-- Filtered list -->*
<ul>
<li s-for=\"item in items\">
{item.toUpperCase()}
</li>
</ul>
*<!-- Complex display logic -->*
<p>
{items.length} items
{items.length === 0 ? \'(empty)\' : \'\'}
{showCount ? \`- \${items.length} total\` : \'\'}
</p>
*<!-- Nested property access -->*
<p>First item: {items\[0\] \|\| \'none\'}</p>
</div>3.4 Understanding the HTML-First Development Mindset
Switching to HTML-First development requires a shift in how you think about building applications. Let's explore this new mindset.
From "How" to "What"
Old mindset (imperative): "I need to get this element, then when this happens, I'll update that element."
New mindset (declarative): "This element should show this value, and when clicked, it should change this state."
Example: Building a User Profile Card
Let's see this mindset shift in action by building a user profile card.
Imperative approach (what you might have done before):
<div id=\"profile\"></div>
<script>
*// Find container*
const profile = document.getElementById(\'profile\');
*// Create elements*
const nameEl = document.createElement(\'h2\');
const emailEl = document.createElement(\'p\');
const toggleBtn = document.createElement(\'button\');
*// Set initial content*
let user = { name: \'Alice\', email: \'alice@example.com\', visible:
true };
nameEl.textContent = user.name;
emailEl.textContent = user.email;
toggleBtn.textContent = \'Hide Email\';
*// Add event listener*
toggleBtn.addEventListener(\'click\', function() {
user.visible = !user.visible;
emailEl.style.display = user.visible ? \'block\' : \'none\';
toggleBtn.textContent = user.visible ? \'Hide Email\' : \'Show Email\';
});
*// Assemble*
profile.appendChild(nameEl);
profile.appendChild(emailEl);
profile.appendChild(toggleBtn);
</script>Declarative approach (SimpliJS mindset):
<div s-app s-state=\"{
user: {
name: \'Alice\',
email: \'alice@example.com\'
},
showEmail: true
}\">
<div class=\"profile-card\">
<h2>{user.name}</h2>
<p s-show=\"showEmail\">{user.email}</p>
<button s-click=\"showEmail = !showEmail\">
{showEmail ? \'Hide\' : \'Show\'} Email
</button>
</div>
</div>Notice the difference in thinking:
You describe the final UI structure directly in HTML
You declare what should happen (toggle showEmail)
You don't worry about how elements get updated
Benefits of Declarative Thinking
Less code - Typically 50-80% less than imperative approaches
More readable - The HTML shows both structure and behavior
Fewer bugs - No missing event listeners or inconsistent state
Easier maintenance - Everything related to a feature is in one place
Better collaboration - Designers can understand and work with HTML-First code
3.5 Deep Dive: The s-if, s-else, and Conditional Rendering
Conditional rendering is a cornerstone of dynamic applications. Let's explore SimpliJS's conditional directives in depth.
Basic Conditional with s-if
<div s-app s-state=\"{ isLoggedIn: false }\">
<div s-if=\"isLoggedIn\">
<h2>Welcome back, user!</h2>
<button s-click=\"isLoggedIn = false\">Logout</button>
</div>
<div s-if=\"!isLoggedIn\">
<h2>Please log in</h2>
<button s-click=\"isLoggedIn = true\">Login</button>
</div>
</div>Using s-else for Cleaner Logic
<div s-app s-state=\"{ isLoggedIn: false }\">
<div s-if=\"isLoggedIn\">
<h2>Welcome back!</h2>
<button s-click=\"isLoggedIn = false\">Logout</button>
</div>
<div s-else>
<h2>Please log in</h2>
<button s-click=\"isLoggedIn = true\">Login</button>
</div>
</div>The s-else directive automatically attaches to the nearest preceding s-if. You can even chain multiple conditions:
<div s-app s-state=\"{ role: \'admin\' }\">
<div s-if=\"role === \'admin\'\">
<h2>Admin Dashboard</h2>
<p>You have full access.</p>
</div>
<div s-else-if=\"role === \'editor\'\">
<h2>Editor Panel</h2>
<p>You can edit content.</p>
</div>
<div s-else-if=\"role === \'viewer\'\">
<h2>Viewer Mode</h2>
<p>You can only view content.</p>
</div>
<div s-else>
<h2>Guest Access</h2>
<p>Please log in for more features.</p>
</div>
</div>s-if vs s-show: When to Use Which
Both s-if and s-show conditionally display content, but they work differently:
| Aspect | s-if | s-show |
|---|---|---|
| How it works | Adds/removes element from DOM | Toggles display: none CSS |
| Initial render | Elements not rendered if condition is false | All elements rendered initially |
| Toggle cost | Expensive (DOM manipulation) | Cheap (CSS change) |
| Use when | Condition rarely changes | Condition toggles frequently |
| Child components | Destroyed/recreated on toggle | Stay mounted |
Example showing the difference:
<div s-app s-state=\"{ showSlow: false, showFast: false }\">
*<!-- Use s-if for rarely-changing conditions -->*
<div s-if=\"showSlow\">
<p>This section is completely removed from DOM when hidden.</p>
<p>Good for login/logout sections.</p>
</div>
*<!-- Use s-show for frequently-toggled UI -->*
<div s-show=\"showFast\" style=\"border: 1px solid blue;\">
<p>This section stays in DOM, just hidden with CSS.</p>
<p>Good for dropdowns, tooltips, tabs.</p>
</div>
<button s-click=\"showSlow = !showSlow\">Toggle s-if section
</button>
<button s-click="showFast = !showFast">
Toggle s-show section
</button>
</div>
Practical Example: Multi-Step Form
Here's a realistic example using conditional rendering:
<div s-app s-state=\"{
step: 1,
formData: {
name: \'\',
email: \'\',
plan: \'basic\'
}
}\">
<div class=\"form-container\">
*<!-- Step 1: Personal Info -->*
<div s-if=\"step === 1\">
<h2>Step 1: Personal Information</h2>
<label>
Name:
<input s-bind=\"formData.name\" required>
</label>
<label>
Email:
<input type=\"email\" s-bind=\"formData.email\" required>
</label>
<button s-click=\"if(formData.name && formData.email) step = 2\">
Next
</button>
</div>
*<!-- Step 2: Choose Plan -->*
<div s-else-if=\"step === 2\">
<h2>Step 2: Select Your Plan</h2>
<select s-model=\"formData.plan\">
<option value=\"basic\">Basic (\$10/month)</option>
<option value=\"pro\">Pro (\$25/month)</option>
<option value=\"enterprise\">Enterprise (\$50/month)</option>
</select>
<div>
<button s-click=\"step = 1\">Back</button>
<button s-click=\"step = 3\">Next</button>
</div>
</div>
*<!-- Step 3: Confirmation -->*
<div s-else-if=\"step === 3\">
<h2>Step 3: Confirm Your Information</h2>
<p><strong>Name:</strong> {formData.name}</p>
<p><strong>Email:</strong> {formData.email}</p>
<p><strong>Plan:</strong> {formData.plan}</p>
<div>
<button s-click=\"step = 2\">Back</button>
<button s-click=\"submitForm()\">Submit</button>
</div>
</div>
*<!-- Success Message -->*
<div s-else-if=\"step === 4\">
<h2>✓ Thank You!</h2>
<p>Your registration is complete.</p>
<button s-click=\"step = 1\">Start Over</button>
</div>
</div>
</div>3.6 Deep Dive: s-for and List Rendering
Rendering lists is one of the most common tasks in web development. SimpliJS's s-for directive makes it elegant and efficient.
Basic List Rendering
<div s-app s-state=\"{
fruits: \[\'Apple\', \'Banana\', \'Orange\', \'Mango\'\]
}\">
<h2>Fruit List</h2>
<ul>
<li s-for=\"fruit in fruits\">
{fruit}
</li>
</ul>
</div>Accessing the Index
<div s-app s-state=\"{
tasks: \[
\'Wake up\',
\'Brush teeth\',
\'Have breakfast\',
\'Learn SimpliJS\'
\]
}\">
<ol>
<li s-for=\"task, index in tasks\">
{index + 1}. {task}
<button s-click=\"tasks.splice(index, 1)\">✓ Done</button>
</li>
</ol>
</div>Working with Arrays of Objects
<div s-app s-state=\"{
users: \[
{ id: 1, name: \'Alice\', active: true },
{ id: 2, name: \'Bob\', active: false },
{ id: 3, name: \'Charlie\', active: true }
\]
}\">
<h2>User List</h2>
<div s-for=\"user in users\"
s-class=\"{ active: user.active, inactive: !user.active }\"
s-key=\"user.id\">
<span>{user.name}</span>
<span s-if=\"user.active\">🟢 Online</span>
<span s-else>⚫ Offline</span>
<button s-click=\"user.active = !user.active\">
Toggle Status
</button>
</div>
</div>The Importance of s-key
When rendering lists, s-key is crucial for performance. It helps SimpliJS track which items have changed, been added, or been removed.
Without s-key (less efficient):
<li s-for=\"item in items\">{item.name}</li>With s-key (recommended):
<li s-for=\"item in items\" s-key=\"item.id\">{item.name}</li>The key should be a unique identifier for each item. If your data doesn't have a natural ID, you can use the index (but this is less optimal for dynamic lists):
<li s-for=\"item, index in items\" s-key=\"index\">{item.name}</li>Advanced List Operations
<div s-app s-state=\"{
numbers: \[1, 2, 3, 4, 5, 6, 7, 8, 9, 10\],
filter: \'all\',
sortAsc: true
}\">
*<!-- Filter and sort controls -->*
<div>
<label>
Show:
<select s-model=\"filter\">
<option value=\"all\">All</option>
<option value=\"even\">Even</option>
<option value=\"odd\">Odd</option>
</select>
</label>
<label>
<input type=\"checkbox\" s-model=\"sortAsc\">
Sort Ascending
</label>
</div>
*<!-- Computed list (using JavaScript expressions) -->*
<ul>
<li s-for=\"num in numbers
.filter(n => {
if(filter === \'even\') return n % 2 === 0;
if(filter === \'odd\') return n % 2 === 1;
return true;
})
.sort((a, b) => sortAsc ? a - b : b - a)\"
s-key=\"num\">
Number: {num}
</li>
</ul>
*<!-- Statistics -->*
<p>
Total numbers: {numbers.length} \|
Filtered: {numbers.filter(n => {
if(filter === \'even\') return n % 2 === 0;
if(filter === \'odd\') return n % 2 === 1;
return true;
}).length}
</p>
</div>Nested Loops
You can nest s-for directives to render complex data structures:
<div s-app s-state=\"{
categories: \[
{
name: \'Fruits\',
items: \[\'Apple\', \'Banana\', \'Orange\'\]
},
{
name: \'Vegetables\',
items: \[\'Carrot\', \'Broccoli\', \'Spinach\'\]
},
{
name: \'Dairy\',
items: \[\'Milk\', \'Cheese\', \'Yogurt\'\]
}
\]
}\">
<div s-for=\"category in categories\" s-key=\"category.name\">
<h3>{category.name}</h3>
<ul>
<li s-for=\"item in category.items\" s-key=\"item\">
{item}
</li>
</ul>
</div>
</div>3.7 Understanding Reactivity: How Changes Flow Through Your App
Now that you've seen directives in action, let's understand the reactivity system that powers them.
The Proxy-Based Reactivity System
Under the hood, SimpliJS uses JavaScript Proxies to create reactive objects. When you define state with s-state or reactive(), SimpliJS wraps your data in a special object that can detect when properties are read or modified.
Here's a simplified mental model:
When you read a value (like {count} in HTML), SimpliJS notes that this part of the DOM depends on count.
When you change a value (like count++ in s-click), SimpliJS knows exactly what changed.
SimpliJS then updates only the parts of the DOM that depend on that specific value.
This is called fine-grained reactivity—only the exact pieces of UI that need updating are changed.
The Reactivity Flow
Let's trace what happens when you click a button:
<div s-app s-state=\"{ count: 0 }\">
<p>The count is: {count}</p>
<button s-click=\"count++\">Increment</button>
</div>When the page loads:
SimpliJS scans the DOM and finds the s-state with { count: 0 }
It creates a reactive count property
It notices the {count} in the paragraph and subscribes to changes
It attaches a click handler to the button
When you click the button:
The click handler runs count++
The reactive count detects it's being changed
It notifies all subscribers (the paragraph with {count})
SimpliJS updates just that paragraph's content
The rest of the DOM remains untouched
Multiple Dependencies
Reactivity becomes more powerful with multiple dependencies:
<div s-app s-state=\"{
width: 10,
height: 5,
unit: \'px\'
}\">
<p>Area: {width * height}{unit}</p>
<p>Perimeter: {2 * (width + height)}{unit}</p>
<label>
Width:
<input type=\"range\" min=\"1\" max=\"20\" s-bind=\"width\">
{width}{unit}
</label>
<label>
Height:
<input type=\"range\" min=\"1\" max=\"20\" s-bind=\"height\">
{height}{unit}
</label>
</div>Here, changing width updates both area and perimeter displays. Changing height does the same. Changing unit updates all displays. SimpliJS tracks all these dependencies automatically.
3.8 Common Patterns and Best Practices
As you start building with HTML-First, here are some patterns that will serve you well.
Pattern 1: Toggle Visibility
<div s-app s-state=\"{ showDetails: false }\">
<button s-click=\"showDetails = !showDetails\">
{showDetails ? \'Hide\' : \'Show\'} Details
</button>
<div s-show=\"showDetails\" class=\"details-panel\">
*<!-- Your details content -->*
</div>
</div>Pattern 2: Dynamic Classes
<div s-app s-state=\"{
status: \'success\',
isSelected: false
}\">
<div s-class=\"{
\'status-success\': status === \'success\',
\'status-error\': status === \'error\',
\'status-warning\': status === \'warning\',
selected: isSelected
}\">This div's classes update based on state
</div>
</div>
Pattern 3: Form Input Groups
<div s-app s-state=\"{
form: {
username: \'\',
password: \'\',
remember: false
}
}\">
<div class=\"form-group\">
<label>Username:</label>
<input s-bind=\"form.username\" placeholder=\"Enter username\">
</div>
<div class=\"form-group\">
<label>Password:</label>
<input type=\"password\" s-bind=\"form.password\" placeholder=\"Enter
password\">
</div>
<div class=\"form-group\">
<label>
<input type=\"checkbox\" s-model=\"form.remember\">
Remember me
</label>
</div>
<pre s-show=\"form.username \|\| form.password\">
{{ JSON.stringify(form, null, 2) }}
</pre>
</div>Pattern 4: List with Add/Remove
<div s-app s-state=\"{
items: \[\],
newItem: \'\'
}\">
<div class=\"input-group\">
<input s-bind=\"newItem\" placeholder=\"Add new item\"
s-key:enter=\"if(newItem) { items.push(newItem); newItem = \'\'; }\">
<button s-click=\"if(newItem) { items.push(newItem); newItem = \'\';
}\">
Add
</button>
</div>
<ul class=\"item-list\">
<li s-for=\"item, index in items\" s-key=\"index\">
<span>{item}</span>
<button class=\"remove\" s-click=\"items.splice(index,
1)\">✗</button>
</li>
</ul>
<p s-if=\"items.length === 0\" class=\"empty-message\">No items yet. Add one above!
</p>
</div>
Best Practices Summary
Keep state minimal - Store only what's necessary, derive everything else
Use s-key in loops - Always provide a unique key for better performance
Prefer s-show for frequently toggled UI - It's more performant
Use meaningful variable names - Your HTML will be more readable
Comment complex expressions - If an expression is complex, add a comment
Break large apps into components - We'll cover this in later chapters
Test in different browsers - Ensure your app works everywhere
3.9 Exercise: Build an Interactive Product Catalog
Now it's time to apply everything you've learned. Build an interactive product catalog with:
A grid of products (use s-for)
Filter by category (use s-if or s-show)
Search functionality (use s-bind for search input)
Add to cart functionality
Cart summary with total items and price
Responsive design
Here's starter code:
<!DOCTYPE html>
<html>
<head>
<title>Product Catalog</title>
<style>
/* Add your styles */
.product-grid { display: grid; grid-template-columns: repeat(3, 1fr);
gap: 20px; }
.product-card { border: 1px solid #ddd; padding: 15px; border-radius:
8px; }
.cart { position: fixed; top: 20px; right: 20px; background: white;
border: 1px solid #ccc; padding: 15px; border-radius: 8px; }
/* Add more styles */
</style>
</head>
<body>
<div s-app s-state=\"{
products: \[
{ id: 1, name: \'Laptop\', category: \'electronics\', price: 999, image:
\'💻\' },
{ id: 2, name: \'T-Shirt\', category: \'clothing\', price: 25, image:
\'👕\' },
{ id: 3, name: \'Book\', category: \'books\', price: 15, image: \'📚\'
},
// Add more products
\],
cart: \[\],
filterCategory: \'all\',
searchTerm: \'\',
// Add more state as needed
}\">
*<!-- Your implementation here -->*
</div>
<script type=\"module\">
import { createApp } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
createApp().mount(\'\[s-app\]\');
</script>
</body>
</html>Bonus Challenges:
Add quantity selector for each product
Implement "Save for later" feature
Add sorting by price (low to high, high to low)
Persist cart in localStorage
Add a checkout form with validation
Chapter 3 Summary
You've now mastered the HTML-First engine, the core of SimpliJS:
The difference between imperative and declarative programming
What directives are and how they work
The power of {} interpolation
Conditional rendering with s-if, s-else, and s-show
List rendering with s-for and the importance of s-key
How reactivity flows through your applications
Common patterns and best practices
Building complex, interactive UIs with pure HTML
You're thinking declaratively now. You're no longer telling the browser how to do things—you're describing what you want, and SimpliJS handles the rest. This mindset shift is the foundation for everything else you'll learn.
In the next chapter, we'll dive into reactive state management with s-state and build our first complete project: a sophisticated To-Do List application that brings together everything we've learned so far.
End of Part 1
This concludes Part 1 of "SimpliJS: Web Development for the Anti-Build Era." You've gone from absolute beginner to confidently building reactive web applications using pure HTML and the SimpliJS HTML-First engine. You understand the philosophy, the installation methods, and the core concepts that make SimpliJS unique.
In Part 2, we'll build on this foundation by introducing JavaScript-based components, deeper state management, and more complex applications. But for now, practice what you've learned. Build something fun, break things, and see what you can create with just HTML and a few s-* directives.
Remember: every expert was once a beginner. You're well on your way.
Part 2: Core Concepts - Building Reactivity
Chapter 4: Reactive State Management with s-state
Welcome to the fourth chapter of your SimpliJS journey. In Part 1, you learned how to create reactive applications using HTML directives. Now it's time to dive deep into the heart of reactivity: state management. Understanding state is crucial because it's the "single source of truth" for your entire application. Everything your app displays and does flows from its state.
4.1 What is State?
Before we dive into SimpliJS-specific concepts, let's understand what "state" means in web applications.
State is simply data that can change over time. Think of it as the memory of your application—it remembers things like:
Whether a user is logged in
The items in a shopping cart
The current theme (light or dark)
Text typed into a form
Which tab is currently selected
In traditional web development, state was scattered everywhere—in DOM elements, in JavaScript variables, in global objects. This made applications hard to debug and maintain.
The Problem with Scattered State
Imagine a simple todo app without proper state management:
*<!-- HTML -->*
<input id=\"newTodo\" placeholder=\"New todo\">
<button onclick=\"addTodo()\">Add</button>
<ul id=\"todoList\"></ul>
<script>
*// JavaScript scattered everywhere*
let todos = \[\]; *// One place for state*
function addTodo() {
const input = document.getElementById(\'newTodo\');
todos.push(input.value); *// Update state*
renderTodos(); *// Manually update UI*
input.value = \'\'; *// Update UI directly*
}
function renderTodos() {
const list = document.getElementById(\'todoList\');
list.innerHTML = \'\';
todos.forEach(todo => {
const li = document.createElement(\'li\');
li.textContent = todo;
list.appendChild(li);
});
}
</script>Problems with this approach:
State is duplicated (in todos array and in the DOM)
Manual synchronization (you must remember to call renderTodos())
No clear data flow (any function can modify state anywhere)
Bugs are hard to track (who changed what and when?)
SimpliJS's Approach: Single Source of Truth
SimpliJS solves this by making your state the single source of truth. The UI is just a reflection of your state. When state changes, the UI updates automatically. You never manually manipulate the DOM.
4.2 Deep Dive into s-state: Defining Data Directly in HTML
The simplest way to create state in SimpliJS is with the s-state attribute. Let's explore it in depth.
Basic Syntax
<div s-app s-state=\"{
count: 0,
message: \'Hello\',
isActive: true,
user: {
name: \'Alice\',
age: 30
},
tags: \[\'javascript\', \'simplijs\'\]
}\">
*<!-- Your reactive UI here -->*
</div>The value of s-state is a JavaScript object literal. You can include:
Strings: 'Hello' or "World"
Numbers: 42, 3.14, -10
Booleans: true or false
Objects: { name: 'Alice', age: 30 }
Arrays: [1, 2, 3] or ['a', 'b', 'c']
Any combination of the above
Accessing State Values
Once defined, you can access state values anywhere inside the s-app container using {} interpolation:
<div s-app s-state=\"{
user: {
firstName: \'John\',
lastName: \'Doe\'
},
score: 100
}\">
<h1>Welcome, {user.firstName} {user.lastName}!</h1>
<p>Your score is: {score}</p>
<p>Next level at: {score + 100}</p>
<p>Initials: {user.firstName\[0\]}{user.lastName\[0\]}</p>
</div>Updating State
You can update state directly in event handlers:
<div s-app s-state=\"{
count: 0,
name: \'World\'
}\">
<p>Count: {count}</p>
<button s-click=\"count = count + 1\">Increment</button>
<button s-click=\"count = 0\">Reset</button>
<p>Hello, {name}!</p>
<input s-bind=\"name\" placeholder=\"Enter your name\">
<button s-click=\"name = \'World\'\">Reset Name</button>
</div>4.3 Working with Different Data Types
Let's explore how to work with various data types in your state.
Strings
<div s-app s-state=\"{
message: \'Hello\',
name: \'Alice\',
greeting: \'\'
}\">
<h2>String Operations</h2>
*<!-- Display -->*
<p>Message: {message}</p>
<p>Uppercase: {message.toUpperCase()}</p>
<p>Length: {message.length}</p>
*<!-- Concatenation -->*
<p>{message}, {name}!</p>
<p>Greeting: {greeting \|\| \'Not set yet\'}</p>
*<!-- Input binding -->*
<input s-bind=\"message\" placeholder=\"Type a message\">
<input s-bind=\"name\" placeholder=\"Your name\">
<input s-bind=\"greeting\" placeholder=\"Custom greeting\">
*<!-- String methods in actions -->*
<button s-click=\"message = message + \'!\'\">Add
Excitement</button>
<button s-click=\"message = message.slice(0, -1)\">Remove Last
Char</button>
</div>Numbers
<div s-app s-state=\"{
price: 19.99,
quantity: 3,
taxRate: 0.08
}\">
<h2>Number Operations</h2>
*<!-- Basic math -->*
<p>Subtotal: \${(price * quantity).toFixed(2)}</p>
<p>Tax: \${(price * quantity * taxRate).toFixed(2)}</p>
<p>Total: \${(price * quantity * (1 + taxRate)).toFixed(2)}</p>
*<!-- Controls -->*
<label>
Price: \$
<input type=\"number\" s-bind=\"price\" step=\"0.01\" min=\"0\">
</label>
<label>
Quantity:
<input type=\"number\" s-bind=\"quantity\" min=\"1\" max=\"10\">
</label>
<label>
Tax Rate: {taxRate * 100}%
<input type=\"range\" s-bind=\"taxRate\" min=\"0\" max=\"0.15\"
step=\"0.01\">
</label>
*<!-- Quick actions -->*
<button s-click=\"quantity = quantity + 1\">+ Add One</button>
<button s-click=\"quantity = Math.max(1, quantity - 1)\">- Remove
One</button>
</div>Booleans
<div s-app s-state=\"{
isLoggedIn: false,
rememberMe: true,
darkMode: false,
hasPermission: true
}\">
<h2>Boolean Operations</h2>
*<!-- Conditional display -->*
<div s-if=\"isLoggedIn\">
<p>Welcome back! 🎉</p>
<button s-click=\"isLoggedIn = false\">Logout</button>
</div>
<div s-else>
<p>Please log in 🔒</p>
<button s-click=\"isLoggedIn = true\">Login</button>
</div>
*<!-- Toggle with checkbox -->*
<label>
<input type=\"checkbox\" s-model=\"rememberMe\">
Remember me (currently: {rememberMe})
</label>
*<!-- Toggle with button -->*
<button s-click=\"darkMode = !darkMode\">
Switch to {darkMode ? \'Light\' : \'Dark\'} Mode
</button>
*<!-- Complex conditions -->*
<div s-if=\"isLoggedIn && hasPermission\">
<p>You can access admin features</p>
</div>
*<!-- Boolean in class toggling -->*
<div s-class=\"{ \'dark-theme\': darkMode, \'light-theme\': !darkMode
}\">This div's theme changes
</div>
</div>
Objects
<div s-app s-state=\"{
user: {
id: 1,
profile: {
name: \'Alice\',
email: \'alice@example.com\',
preferences: {
theme: \'dark\',
notifications: true
}
},
stats: {
posts: 42,
followers: 128
}
},
address: {
street: \'123 Main St\',
city: \'Boston\',
zip: \'02101\'
}
}\">
<h2>Working with Objects</h2>
*<!-- Accessing nested properties -->*
<h3>User Profile</h3>
<p>Name: {user.profile.name}</p>
<p>Email: {user.profile.email}</p>
<p>Theme: {user.profile.preferences.theme}</p>
<p>Posts: {user.stats.posts}</p>
*<!-- Updating object properties -->*
<label>
Update Name:
<input s-bind=\"user.profile.name\">
</label>
<label>
<input type=\"checkbox\"
s-model=\"user.profile.preferences.notifications\">
Receive Notifications
</label>
*<!-- Toggle theme -->*
<button s-click=\"user.profile.preferences.theme =
user.profile.preferences.theme === \'dark\' ? \'light\' : \'dark\'\">
Toggle Theme (current: {user.profile.preferences.theme})
</button>
*<!-- Display entire object (for debugging) -->*
<pre>{JSON.stringify(user, null, 2)}</pre>
*<!-- Merge updates -->*
<button s-click=\"address = { \...address, city: \'New York\' }\">Move to NYC
</button>
</div>
Arrays
<div s-app s-state=\"{
todos: \[
{ id: 1, text: \'Learn SimpliJS\', done: false },
{ id: 2, text: \'Build an app\', done: false },
{ id: 3, text: \'Master reactivity\', done: false }
\],
numbers: \[1, 2, 3, 4, 5\],
newTodo: \'\',
filter: \'all\'
}\">
<h2>Working with Arrays</h2>
*<!-- Display array length -->*
<p>Total todos: {todos.length}</p>
<p>Completed: {todos.filter(t => t.done).length}</p>
<p>Pending: {todos.filter(t => !t.done).length}</p>
*<!-- Adding to array -->*
<div>
<input s-bind=\"newTodo\" placeholder=\"New todo\">
<button s-click=\"if(newTodo) {
todos.push({
id: todos.length + 1,
text: newTodo,
done: false
});
newTodo = \'\';
}\">Add Todo</button>
</div>
*<!-- Display array with s-for -->*
<ul>
<li s-for=\"todo, index in todos\" s-key=\"todo.id\">
<input type=\"checkbox\" s-model=\"todo.done\">
<span s-class=\"{ done: todo.done }\">{todo.text}</span>
<button s-click=\"todos.splice(index, 1)\">❌</button>
</li>
</ul>
*<!-- Array operations -->*
<div>
<button s-click=\"numbers.push(numbers.length + 1)\">Add
Number</button>
<button s-click=\"numbers.pop()\">Remove Last</button>
<button s-click=\"numbers = numbers.sort((a,b) => a - b)\">Sort
Asc</button>
<button s-click=\"numbers = numbers.sort((a,b) => b - a)\">Sort
Desc</button>
<button s-click=\"numbers = numbers.map(n => n * 2)\">Double
All</button>
<button s-click=\"numbers = numbers.filter(n => n % 2 === 0)\">Keep
Even</button>
</div>
*<!-- Display numbers -->*
<p>Numbers: {numbers.join(\', \')}</p>
</div>
<style>
.done { text-decoration: line-through; opacity: 0.7; }
</style>4.4 Nested State and Reactivity
One of SimpliJS's powerful features is that reactivity works deeply—changes to nested properties are automatically detected.
Deep Reactivity Example
<div s-app s-state=\"{
config: {
appearance: {
theme: \'light\',
colors: {
primary: \'#007bff\',
secondary: \'#6c757d\',
background: \'#ffffff\'
},
fonts: {
size: 16,
family: \'Arial\'
}
},
features: {
darkMode: false,
animations: true,
beta: false
}
}
}\">
<h2>Deep Reactivity Demo</h2>
*<!-- Theme selector affects deeply nested properties -->*
<div>
<h3>Theme: {config.appearance.theme}</h3>
<button s-click=\"
config.appearance.theme = \'light\';
config.appearance.colors.background = \'#ffffff\';
config.appearance.colors.primary = \'#007bff\';
\">Light Theme</button>
<button s-click=\"
config.appearance.theme = \'dark\';
config.appearance.colors.background = \'#333333\';
config.appearance.colors.primary = \'#bb86fc\';
\">Dark Theme</button>
</div>
*<!-- Color pickers for deep properties -->*
<div>
<label>
Primary Color:
<input type=\"color\" s-bind=\"config.appearance.colors.primary\">
</label>
<label>
Background Color:
<input type=\"color\" s-bind=\"config.appearance.colors.background\">
</label>
</div>
*<!-- Font size slider (deep property) -->*
<div>
<label>
Font Size: {config.appearance.fonts.size}px
<input type=\"range\" s-bind=\"config.appearance.fonts.size\"
min=\"12\" max=\"32\">
</label>
</div>
*<!-- Preview area using deeply nested values -->*
<div style=\"
background-color: {config.appearance.colors.background};
color: {config.appearance.colors.primary};
font-size: {config.appearance.fonts.size}px;
font-family: {config.appearance.fonts.family};
padding: 20px;
border-radius: 8px;
border: 2px solid {config.appearance.colors.primary};
margin-top: 20px;
\">
<p>This preview updates reactively as you change:</p>
<ul>
<li>Theme (affects multiple properties)</li>
<li>Individual colors</li>
<li>Font size</li>
</ul>
<p>All without writing any JavaScript!</p>
</div>
*<!-- Feature toggles (also deeply nested) -->*
<div style=\"margin-top: 20px;\">
<h3>Feature Flags</h3>
<label>
<input type=\"checkbox\" s-model=\"config.features.animations\">
Animations (deep: {config.features.animations})
</label>
<label>
<input type=\"checkbox\" s-model=\"config.features.beta\">
Beta Features (deep: {config.features.beta})
</label>
</div>
</div>How Deep Reactivity Works
When you create state with s-state, SimpliJS recursively wraps every property and nested object in JavaScript Proxies. This means:
Reading any property (even deeply nested) establishes a dependency
Writing any property triggers updates for everything that depends on it
Adding new properties to existing objects also becomes reactive
<div s-app s-state=\"{ user: { name: \'Alice\' } }\">
<p>Name: {user.name}</p>
*<!-- Adding a new property dynamically -->*
<button s-click=\"user.age = 30\">Add Age Property
</button>
<!-- The new property is reactive! -->
<p s-if="user.age">Age: {user.age}</p>
<!-- Even nested new objects are reactive -->
<button s-click="user.address = { city: 'Boston', zip: '02101' }">
Add Address
</button>
<div s-if="user.address">
<p>City: {user.address.city}</p>
<p>ZIP: {user.address.zip}</p>
</div>
</div>
4.5 Multiple s-state Declarations: Scoped Reactivity
One powerful feature of SimpliJS is that you can have multiple s-state declarations, each creating its own scope.
Understanding Scope
<div s-app>
*<!-- Global app state -->*
<div s-state=\"{ appName: \'My App\', version: \'1.0\' }\">
<h1>{appName} v{version}</h1>
*<!-- User section with its own state -->*
<div s-state=\"{ user: \'Alice\', loggedIn: true }\">
<h2>User: {user}</h2>
<p>Status: {loggedIn ? \'Online\' : \'Offline\'}</p>
*<!-- Can access parent state too -->*
<p>Using {appName} since today</p>
*<!-- Nested counter with its own state -->*
<div s-state=\"{ count: 0 }\">
<p>Count: {count}</p>
<button s-click=\"count++\">+</button>
*<!-- Can access all ancestor state -->*
<p>{user} has clicked {count} times</p>
</div>
</div>
*<!-- Separate counter, different scope -->*
<div s-state=\"{ count: 100 }\">
<p>Global counter: {count}</p>
<button s-click=\"count++\">+</button>
*<!-- This count is independent from the one above -->*
</div>
</div>
</div>Benefits of Scoped State
Isolation: Components don't accidentally interfere with each other
Reusability: You can copy-paste sections with their own state
Clarity: It's clear what state belongs to which part of the UI
Performance: Updates are scoped to smaller sections of the app
Real-World Example: Product Card with Independent State
<div s-app>
<h1>Product Catalog</h1>
<div style=\"display: grid; grid-template-columns: repeat(3, 1fr); gap:
20px;\">
*<!-- Product Card 1 -->*
<div class=\"product-card\" s-state=\"{
product: {
name: \'Laptop\',
price: 999,
image: \'💻\'
},
quantity: 1,
showDetails: false
}\">
<h3>{product.image} {product.name}</h3>
<p>Price: \${product.price}</p>
<label>
Quantity:
<input type=\"number\" s-bind=\"quantity\" min=\"1\" max=\"10\">
</label>
<p>Total: \${product.price * quantity}</p>
<button s-click=\"showDetails = !showDetails\">
{showDetails ? \'Hide\' : \'Show\'} Details
</button>
<div s-show=\"showDetails\" class=\"details\">
<h4>Product Details</h4>
<p>In stock: Yes</p>
<p>Free shipping available</p>
<p>1-year warranty</p>
</div>
<button class=\"add-to-cart\">Add to Cart</button>
</div>
*<!-- Product Card 2 - completely independent state -->*
<div class=\"product-card\" s-state=\"{
product: {
name: \'Headphones\',
price: 199,
image: \'🎧\'
},
quantity: 1,
showDetails: false
}\">
<h3>{product.image} {product.name}</h3>
<p>Price: \${product.price}</p>
<label>
Quantity:
<input type=\"number\" s-bind=\"quantity\" min=\"1\" max=\"10\">
</label>
<p>Total: \${product.price * quantity}</p>
<button s-click=\"showDetails = !showDetails\">
{showDetails ? \'Hide\' : \'Show\'} Details
</button>
<div s-show=\"showDetails\" class=\"details\">
<h4>Product Details</h4>
<p>Wireless, Noise-cancelling</p>
<p>30-hour battery life</p>
<p>Includes carrying case</p>
</div>
<button class=\"add-to-cart\">Add to Cart</button>
</div>
*<!-- Product Card 3 -->*
<div class=\"product-card\" s-state=\"{
product: {
name: \'Mouse\',
price: 49,
image: \'🖱️\'
},
quantity: 1,
showDetails: false
}\">
<h3>{product.image} {product.name}</h3>
<p>Price: \${product.price}</p>
<label>
Quantity:
<input type=\"number\" s-bind=\"quantity\" min=\"1\" max=\"10\">
</label>
<p>Total: \${product.price * quantity}</p>
<button s-click=\"showDetails = !showDetails\">
{showDetails ? \'Hide\' : \'Show\'} Details
</button>
<div s-show=\"showDetails\" class=\"details\">
<h4>Product Details</h4>
<p>Wireless, Ergonomic</p>
<p>Adjustable DPI</p>
<p>Programmable buttons</p>
</div>
<button class=\"add-to-cart\">Add to Cart</button>
</div>
</div>
</div>
<style>
.product-card {
border: 1px solid #ddd;
padding: 20px;
border-radius: 8px;
background: white;
}
.details {
margin-top: 10px;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
}
.add-to-cart {
margin-top: 10px;
width: 100%;
padding: 10px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>4.6 Computed Values in HTML
While s-state stores raw data, you often need values derived from that data. SimpliJS lets you compute values directly in your HTML expressions.
Basic Computed Values
<div s-app s-state=\"{
price: 100,
quantity: 2,
taxRate: 0.08
}\">
<h2>Shopping Cart</h2>
*<!-- Computed directly in expressions -->*
<p>Subtotal: \${price * quantity}</p>
<p>Tax: \${price * quantity * taxRate}</p>
<p>Total: \${price * quantity * (1 + taxRate)}</p>
*<!-- You can use ternary operators -->*
<p>Status: \${price * quantity > 200 ? \'Bulk discount available!\'
: \'\'}</p>
*<!-- String manipulation -->*
<p>Price per item: \${price.toFixed(2)}</p>
*<!-- Array operations -->*
<div s-state=\"{ numbers: \[1, 2, 3, 4, 5\] }\">
<p>Sum: {numbers.reduce((a, b) => a + b, 0)}</p>
<p>Average: {numbers.reduce((a, b) => a + b, 0) /
numbers.length}</p>
<p>Max: {Math.max(\...numbers)}</p>
</div>
</div>More Complex Computations
<div s-app s-state=\"{
cart: \[
{ name: \'Laptop\', price: 999, quantity: 1 },
{ name: \'Mouse\', price: 49, quantity: 2 },
{ name: \'Keyboard\', price: 129, quantity: 1 }
\],
discount: 0.1, // 10% discount
shipping: 15
}\">
<h2>Advanced Cart Calculations</h2>
*<!-- Cart items display -->*
<table border=\"1\" cellpadding=\"8\">
<tr>
<th>Item</th>
<th>Price</th>
<th>Qty</th>
<th>Subtotal</th>
</tr>
<tr s-for=\"item in cart\" s-key=\"item.name\">
<td>{item.name}</td>
<td>\${item.price}</td>
<td>{item.quantity}</td>
<td>\${item.price * item.quantity}</td>
</tr>
</table>
*<!-- Complex calculations -->*
<div style=\"margin-top: 20px;\">
<p>Subtotal: \${cart.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
).toFixed(2)}</p>
<p>Discount: \${(
cart.reduce((sum, item) => sum + (item.price * item.quantity), 0) *
discount
).toFixed(2)}</p>
<p>Subtotal after discount: \${(
cart.reduce((sum, item) => sum + (item.price * item.quantity), 0) *
(1 - discount)
).toFixed(2)}</p>
<p>Shipping: \${shipping}</p>
<p><strong>Total: \${(
cart.reduce((sum, item) => sum + (item.price * item.quantity), 0) *
(1 - discount) +
shipping
).toFixed(2)}</strong></p>
</div>
*<!-- Item count badges -->*
<div>
<span class=\"badge\">
Total items: {cart.reduce((sum, item) => sum + item.quantity, 0)}
</span>
<span class=\"badge\">
Unique products: {cart.length}
</span>
<span class=\"badge\" s-if=\"cart.length > 2\">
🎉 You qualify for bulk discount!
</span>
</div>
</div>
<style>
.badge {
display: inline-block;
padding: 5px 10px;
margin: 5px;
background: #007bff;
color: white;
border-radius: 20px;
font-size: 14px;
}
</style>4.7 Common Pitfalls and How to Avoid Them
As you work with state, you'll encounter some common issues. Here's how to recognize and fix them.
Pitfall 1: Forgetting s-app
Problem: Directives don't work.
*<!-- Wrong: missing s-app -->*
<div s-state=\"{ count: 0 }\">
<p>{count}</p> *<!-- Won\'t update! -->*
<button s-click=\"count++\">Click</button> *<!-- Won\'t work!
-->*
</div>
*<!-- Correct: with s-app -->*
<div s-app s-state=\"{ count: 0 }\">
<p>{count}</p>
<button s-click=\"count++\">Click</button>
</div>Pitfall 2: Using Reserved Words as State Names
Problem: Conflicts with JavaScript reserved words.
*<!-- Wrong: using reserved words -->*
<div s-app s-state=\"{
class: \'active\', // \'class\' is reserved
for: \'input\', // \'for\' is reserved
let: 5 // \'let\' is reserved
}\">
*<!-- This may cause errors -->*
</div>
*<!-- Correct: use different names -->*
<div s-app s-state=\"{
className: \'active\',
htmlFor: \'input\',
value: 5
}\">
*<!-- Works perfectly -->*
</div>Pitfall 3: Modifying State in Ways That Break Reactivity
Problem: Some operations don't trigger reactivity.
<div s-app s-state=\"{
user: { name: \'Alice\' },
items: \[1, 2, 3\]
}\">
*<!-- Wrong: reassigning entire objects might be inefficient -->*
<button s-click=\"user = { name: \'Bob\' }\"> *<!-- Works but
recreates object -->*
Change Name
</button>
*<!-- Better: modify properties directly -->*
<button s-click=\"user.name = \'Bob\'\"> *<!-- More efficient -->*
Change Name
</button>
*<!-- Wrong: array index assignment might not trigger -->*
<button s-click=\"items\[0\] = 99\"> *<!-- May not update properly
-->*Change First Item
</button>
<!-- Correct: use array methods -->
<button s-click="items.splice(0, 1, 99)"> <!-- Properly reactive -->
Change First Item
</button>
</div>
Pitfall 4: Too Much State
Problem: Storing everything in state, even derived values.
*<!-- Wrong: storing derived values -->*
<div s-app s-state=\"{
firstName: \'John\',
lastName: \'Doe\',
fullName: \'John Doe\', // Don\'t store this!
price: 100,
quantity: 2,
total: 200 // Don\'t store this!
}\">
<p>Full name: {fullName}</p> *<!-- Must manually keep in sync
-->*
<p>Total: \${total}</p> *<!-- Goes out of sync if price changes
-->*
</div>
*<!-- Correct: compute derived values -->*
<div s-app s-state=\"{
firstName: \'John\',
lastName: \'Doe\',
price: 100,
quantity: 2
}\">
<p>Full name: {firstName} {lastName}</p> *<!-- Always in sync
-->*
<p>Total: \${price * quantity}</p> *<!-- Automatically updates
-->*
</div>Pitfall 5: Deep Nesting Without Need
Problem: Overly nested objects make code hard to read.
*<!-- Hard to read and maintain -->*
<div s-app s-state=\"{
a: {
b: {
c: {
d: {
value: \'Hello\'
}
}
}
}
}\">
<p>{a.b.c.d.value}</p> *<!-- Long access path -->*
</div>
*<!-- Better: flatten when possible -->*
<div s-app s-state=\"{
value: \'Hello\'
}\">
<p>{value}</p>
</div>
*<!-- Use meaningful grouping, not deep nesting -->*
<div s-app s-state=\"{
user: {
profile: {
name: \'Alice\',
email: \'alice@example.com\'
},
settings: {
theme: \'dark\',
notifications: true
}
}
}\">
*<!-- 2-3 levels is usually fine for logical grouping -->*
<p>{user.profile.name}</p>
<p>{user.settings.theme}</p>
</div>4.8 Performance Considerations
Understanding how state affects performance helps you build faster applications.
What Makes Updates Fast or Slow
Fast updates:
Changing primitive values (strings, numbers, booleans)
Updating specific object properties
Using s-show for frequently toggled elements
Having shallow state structures
Slower updates:
Replacing entire large arrays
Adding/removing many DOM elements at once
Using s-if for frequently toggled elements
Having extremely deep state objects
Optimization Tips
*<!-- 1. Use s-show for frequent toggles -->*
<div s-app s-state=\"{ isVisible: true }\">
*<!-- Good for frequent toggles (like dropdowns) -->*
<div s-show=\"isVisible\">This just hides with CSS</div>
*<!-- Good for rare changes (like login/logout) -->*
<div s-if=\"isLoggedIn\">This removes/adds to DOM</div>
</div>
*<!-- 2. Use s-key in lists -->*
<div s-app s-state=\"{ items: \[{id:1, text:\'A\'}, {id:2,
text:\'B\'}\] }\">
*<!-- Always provide a unique key for better performance -->*
<div s-for=\"item in items\" s-key=\"item.id\">
{item.text}
</div>
</div>
*<!-- 3. Avoid huge expressions in loops -->*
<div s-app s-state=\"{ numbers: \[/* 1000 items */\] }\">
*<!-- Slow: complex calculation in each loop -->*
<div s-for=\"n in numbers\">
{fibonacci(n) * factorial(n) / complexCalculation(n)}
</div>
*<!-- Better: precompute if possible -->*
<div s-state=\"{ computed: numbers.map(n => expensiveOp(n)) }\">
<div s-for=\"val in computed\">
{val}
</div>
</div>
</div>
*<!-- 4. Use s-once for static content -->*
<div s-app s-state=\"{ staticData: \'This never changes\' }\">
*<!-- s-once tells SimpliJS not to track this for updates -->*
<div s-once>This content never updates, even if staticData changes
{staticData} <!-- Won't update after initial render -->
</div>
<!-- Regular content updates normally -->
<div>
This updates normally: {staticData}
</div>
</div>
4.9 Project: Build a Complete To-Do List Application
Now it's time to bring everything together. We'll build a fully-featured To-Do List application using only HTML-First features.
Project Requirements
Our To-Do List will have:
Add new todos with title and optional due date
Mark todos as complete/incomplete
Delete todos
Filter todos (All, Active, Completed)
Search todos
Sort by various criteria
Statistics (total, completed, pending)
Local storage persistence
Batch operations (complete all, delete completed)
Complete Implementation
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width,
initial-scale=1.0\">
<title>📝 SimpliJS Todo List</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto,
Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.todo-app {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.app-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.app-header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.app-header .stats {
display: flex;
justify-content: center;
gap: 20px;
font-size: 0.9em;
opacity: 0.9;
}
.add-todo {
padding: 30px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.add-todo input, .add-todo select {
width: 100%;
padding: 12px;
margin: 8px 0;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
.add-todo input:focus, .add-todo select:focus {
outline: none;
border-color: #667eea;
}
.add-todo button {
width: 100%;
padding: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.3s;
}
.add-todo button:hover {
background: #764ba2;
}
.todo-controls {
padding: 20px 30px;
background: white;
border-bottom: 1px solid #e9ecef;
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
}
.todo-controls input, .todo-controls select {
padding: 8px 12px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 14px;
}
.todo-controls button {
padding: 8px 16px;
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
}
.todo-controls button:hover {
background: #e9ecef;
}
.todo-list {
padding: 30px;
max-height: 500px;
overflow-y: auto;
}
.todo-item {
display: flex;
align-items: center;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 10px;
animation: slideIn 0.3s ease;
transition: all 0.3s;
}
\@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.todo-item:hover {
transform: translateX(5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.todo-item.completed {
opacity: 0.7;
background: #f1f3f5;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #868e96;
}
.todo-checkbox {
margin-right: 15px;
width: 20px;
height: 20px;
cursor: pointer;
}
.todo-content {
flex: 1;
}
.todo-text {
font-size: 16px;
font-weight: 500;
margin-bottom: 5px;
}
.todo-meta {
font-size: 12px;
color: #868e96;
display: flex;
gap: 15px;
}
.todo-category {
background: #e9ecef;
padding: 2px 8px;
border-radius: 12px;
}
.todo-actions {
display: flex;
gap: 8px;
}
.todo-actions button {
padding: 5px 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background 0.3s;
}
.todo-actions button.delete {
background: #ff6b6b;
color: white;
}
.todo-actions button.delete:hover {
background: #ff5252;
}
.empty-state {
text-align: center;
padding: 50px;
color: #868e96;
}
.empty-state h3 {
margin-bottom: 10px;
color: #495057;
}
.footer {
padding: 20px 30px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
color: #868e96;
}
.footer button {
padding: 5px 10px;
background: none;
border: 1px solid #e9ecef;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
}
.footer button:hover {
background: #e9ecef;
}
.badge {
display: inline-block;
padding: 2px 8px;
background: #e9ecef;
border-radius: 12px;
font-size: 12px;
margin-left: 5px;
}
</style>
</head>
<body>
<div s-app class=\"todo-app\">
*<!-- Main app state -->*
<div s-state=\"{
todos: \[\],
newTodo: {
text: \'\',
category: \'personal\',
dueDate: \'\'
},
filter: \'all\',
searchTerm: \'\',
sortBy: \'date-desc\',
categories: \[\'personal\', \'work\', \'shopping\', \'health\'\]
}\">
*<!-- Header with stats -->*
<div class=\"app-header\">
<h1>📝 SimpliTodo</h1>
<div class=\"stats\">
<span>Total: {todos.length}</span>
<span>✓ Completed: {todos.filter(t => t.completed).length}</span>
<span>⏳ Pending: {todos.filter(t => !t.completed).length}</span>
</div>
</div>
*<!-- Add new todo form -->*
<div class=\"add-todo\">
<h3>Add New Todo</h3>
<input
type=\"text\"
s-bind=\"newTodo.text\"
placeholder=\"What needs to be done?\"
s-key:enter=\"if(newTodo.text.trim()) {
todos.push({
id: Date.now(),
text: newTodo.text,
completed: false,
category: newTodo.category,
dueDate: newTodo.dueDate,
createdAt: new Date().toISOString()
});
newTodo = { text: \'\', category: \'personal\', dueDate: \'\' };
}\"
>
<div style=\"display: grid; grid-template-columns: 1fr 1fr; gap:
10px;\">
<select s-model=\"newTodo.category\">
<option s-for=\"cat in categories\" value=\"{cat}\">{cat}</option>
</select>
<input
type=\"date\"
s-bind=\"newTodo.dueDate\"
placeholder=\"Due date\"
>
</div>
<button s-click=\"if(newTodo.text.trim()) {
todos.push({
id: Date.now(),
text: newTodo.text,
completed: false,
category: newTodo.category,
dueDate: newTodo.dueDate,
createdAt: new Date().toISOString()
});
newTodo = { text: \'\', category: \'personal\', dueDate: \'\' };
}\">
Add Todo
</button>
</div>
*<!-- Controls bar -->*
<div class=\"todo-controls\">
<input
type=\"text\"
s-bind=\"searchTerm\"
placeholder=\"🔍 Search todos\...\"
style=\"flex: 1;\"
>
<select s-model=\"filter\">
<option value=\"all\">All</option>
<option value=\"active\">Active</option>
<option value=\"completed\">Completed</option>
</select>
<select s-model=\"sortBy\">
<option value=\"date-desc\">Newest First</option>
<option value=\"date-asc\">Oldest First</option>
<option value=\"alpha-asc\">A to Z</option>
<option value=\"alpha-desc\">Z to A</option>
<option value=\"due-date\">Due Date</option>
</select>
<button s-click=\"todos.forEach(t => t.completed = true)\">
✓ Complete All
</button>
<button s-click=\"todos = todos.filter(t => !t.completed)\">
🗑️ Clear Completed
</button>
</div>
*<!-- Todo list -->*
<div class=\"todo-list\">
*<!-- Computed and filtered todos -->*
<div s-state=\"{
filteredTodos: todos
.filter(t => {
// Filter by status
if(filter === \'active\') return !t.completed;
if(filter === \'completed\') return t.completed;
return true;
})
.filter(t => {
// Search filter
if(!searchTerm) return true;
return t.text.toLowerCase().includes(searchTerm.toLowerCase());
})
.sort((a, b) => {
// Sort logic
if(sortBy === \'date-desc\') return new Date(b.createdAt) - new
Date(a.createdAt);
if(sortBy === \'date-asc\') return new Date(a.createdAt) - new
Date(b.createdAt);
if(sortBy === \'alpha-asc\') return a.text.localeCompare(b.text);
if(sortBy === \'alpha-desc\') return b.text.localeCompare(a.text);
if(sortBy === \'due-date\') {
if(!a.dueDate) return 1;
if(!b.dueDate) return -1;
return new Date(a.dueDate) - new Date(b.dueDate);
}
return 0;
})
}\">
*<!-- Show empty state if no todos -->*
<div s-if=\"filteredTodos.length === 0\" class=\"empty-state\">
<h3>✨ No todos found</h3>
<p>Add a new todo to get started!</p>
</div>
*<!-- Render todos -->*
<div s-for=\"todo in filteredTodos\"
s-key=\"todo.id\"
class=\"todo-item\"
s-class=\"{ completed: todo.completed }\">
<input
type=\"checkbox\"
class=\"todo-checkbox\"
s-model=\"todo.completed\"
>
<div class=\"todo-content\">
<div class=\"todo-text\">{todo.text}</div>
<div class=\"todo-meta\">
<span class=\"todo-category\">{todo.category}</span>
<span s-if=\"todo.dueDate\">
📅 Due: {new Date(todo.dueDate).toLocaleDateString()}
</span>
<span>📌 Added: {new
Date(todo.createdAt).toLocaleDateString()}</span>
</div>
</div>
<div class=\"todo-actions\">
<button s-click=\"todo.completed = !todo.completed\">
{todo.completed ? \'↩️\' : \'✓\'}
</button>
<button
class=\"delete\"
s-click=\"todos = todos.filter(t => t.id !== todo.id)\"
>
🗑️
</button>
</div>
</div>
</div>
</div>
*<!-- Footer with persistence controls -->*
<div class=\"footer\">
<span>
Showing {filteredTodos.length} of {todos.length} todos
</span>
<div>
<button s-click=\"
// Save to localStorage
localStorage.setItem(\'simplijs-todos\', JSON.stringify(todos));
alert(\'Todos saved!\');
\">
💾 Save
</button>
<button s-click=\"
// Load from localStorage
const saved = localStorage.getItem(\'simplijs-todos\');
if(saved) {
todos = JSON.parse(saved);
}
\">
📂 Load
</button>
<button s-click=\"
// Clear all todos
if(confirm(\'Delete all todos?\')) {
todos = \[\];
}
\">
🗑️ Clear All
</button>
</div>
</div>
*<!-- Load todos from localStorage on page load -->*
<div s-state=\"{
init: (function() {
const saved = localStorage.getItem(\'simplijs-todos\');
if(saved) {
setTimeout(() => {
todos = JSON.parse(saved);
}, 0);
}
})()
}\"></div>
</div>
</div>
<script type=\"module\">
import { createApp } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
createApp().mount(\'\[s-app\]\');
</script>
</body>
</html>Understanding the Todo App
Let's break down the key parts of this application:
1. Main State Structure:
{
todos: \[\], *// Array of todo items*
newTodo: { *// Form data for new todo*
text: \'\',
category: \'personal\',
dueDate: \'\'
},
filter: \'all\', *// Current filter*
searchTerm: \'\', *// Search text*
sortBy: \'date-desc\', *// Sort criteria*
categories: \[\...\] *// Available categories*
}2. Adding a Todo:
The add functionality demonstrates:
Two-way binding with s-bind
Conditional execution with if()
Array manipulation with push()
Object creation and ID generation
3. Filtered Todos:
The computed filteredTodos shows:
Multiple filter conditions
Search functionality
Complex sorting logic
All done declaratively
4. Local Storage:
The save/load buttons demonstrate:
Using browser's localStorage API
JSON serialization/deserialization
Conditional confirmation dialogs
Chapter 4 Summary
You've now mastered reactive state management with SimpliJS:
What state is and why it's important
Defining state with s-state in HTML
Working with different data types (strings, numbers, booleans, objects, arrays)
Understanding deep reactivity and nested objects
Using multiple scoped states for isolation
Computing values directly in expressions
Common pitfalls and how to avoid them
Performance considerations for reactive state
Building a complete production-ready Todo application
The Todo app you built is not just a learning exercise—it's a fully functional application that you can use and extend. You've seen how complex features like filtering, searching, sorting, and persistence can be implemented with just HTML and SimpliJS directives.
In the next chapter, we'll explore control flow in more depth, focusing on advanced conditional rendering and list manipulation techniques.
End of Chapter 4
Chapter 5: Control Flow in HTML
Welcome to Chapter 5, where we dive deep into controlling the flow of your application directly in HTML. Control flow determines what users see and when they see it. In SimpliJS, you have powerful directives that let you conditionally render content, loop through data, and manage visibility—all without writing a single line of JavaScript.
5.1 Conditional Rendering with s-if, s-else, and s-else-if
Conditional rendering is the art of showing or hiding content based on certain conditions. SimpliJS provides a family of directives for this purpose.
The s-if Directive
The s-if directive conditionally renders an element based on a JavaScript expression. If the expression evaluates to true, the element is added to the DOM. If false, it's completely removed.
<div s-app s-state=\"{ isLoggedIn: false, username: \'\' }\">
<h2>Welcome to Our Site</h2>
*<!-- This element only exists when isLoggedIn is true -->*
<div s-if=\"isLoggedIn\" class=\"welcome-card\">
<h3>Welcome back, {username}!</h3>
<p>We\'re glad to see you again.</p>
</div>
*<!-- Login form (only shown when not logged in) -->*
<div s-if=\"!isLoggedIn\" class=\"login-card\">
<h3>Please Log In</h3>
<input s-bind=\"username\" placeholder=\"Enter your username\">
<button s-click=\"if(username) isLoggedIn = true\">
Login
</button>
</div>
</div>
<style>
.welcome-card, .login-card {
padding: 20px;
margin: 10px 0;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.welcome-card { background: #d4edda; }
.login-card { background: #f8d7da; }
</style>Important: When s-if is false, the element is completely removed from the DOM. It doesn't exist in the page structure at all.
Adding s-else for Alternative Content
The s-else directive works hand-in-hand with s-if to provide an alternative when the condition is false.
<div s-app s-state=\"{ temperature: 72, isRaining: false }\">
<h2>Weather Report</h2>
<div s-if=\"temperature > 75\" class=\"weather-hot\">
🔥 It\'s hot outside! Temperature: {temperature}°F
</div>
<div s-else class=\"weather-mild\">
🌤️ It\'s mild outside. Temperature: {temperature}°F
</div>
*<!-- You can also use s-else with different conditions -->*
<div>
<span s-if=\"isRaining\">☔ Bring an umbrella!</span>
<span s-else>😎 No rain today!</span>
</div>
*<!-- Controls -->*
<button s-click=\"temperature += 5\">Warmer +5°F</button>
<button s-click=\"temperature -= 5\">Cooler -5°F</button>
<button s-click=\"isRaining = !isRaining\">
Toggle Rain
</button>
</div>Multiple Conditions with s-else-if
When you have more than two possible states, use s-else-if:
<div s-app s-state=\"{
score: 85,
grade: \'\',
feedback: \'\'
}\">
<h2>Grade Calculator</h2>
*<!-- Grade determination with multiple conditions -->*
<div class=\"grade-display\">
<div s-if=\"score >= 90\" class=\"grade-a\">
🏆 Grade: A - Excellent!
</div>
<div s-else-if=\"score >= 80\" class=\"grade-b\">
👍 Grade: B - Good job!
</div>
<div s-else-if=\"score >= 70\" class=\"grade-c\">
📚 Grade: C - Keep studying!
</div>
<div s-else-if=\"score >= 60\" class=\"grade-d\">
⚠️ Grade: D - Need improvement
</div>
<div s-else class=\"grade-f\">
❌ Grade: F - Please see teacher
</div>
</div>
*<!-- Score input -->*
<label>Enter Score (0-100):
<input type="number" s-bind="score" min="0" max="100">
</label>
<!-- Additional feedback based on score -->
<div class="feedback" s-if="score >= 60">
<p s-if="score >= 90">🎉 Outstanding work!</p>
<p s-else-if="score >= 80">📈 You're doing great!</p>
<p s-else-if="score >= 70">📖 Keep up the good work!</p>
<p s-else>📝 You passed, but can do better!</p>
</div>
<div s-else class="feedback warning">
<p>Don't give up! Let's make a study plan.</p>
</div>
</div>
<style>
.grade-display { margin: 20px 0; }
.grade-a, .grade-b, .grade-c, .grade-d, .grade-f {
padding: 15px;
border-radius: 8px;
font-size: 1.2em;
}
.grade-a { background: #d4edda; color: #155724; }
.grade-b { background: #cce5ff; color: #004085; }
.grade-c { background: #fff3cd; color: #856404; }
.grade-d { background: #ffe5d0; color: #8a4b08; }
.grade-f { background: #f8d7da; color: #721c24; }
.feedback { margin-top: 20px; padding: 15px; background: #e2e3e5; border-radius: 8px; }
.warning { background: #fff3cd; color: #856404; }
</style>
Rules for Using s-if, s-else-if, and s-else
s-else must immediately follow an s-if or s-else-if element
No elements in between - they must be siblings
s-else-if can be chained multiple times
Only one s-else per if-block
*<!-- ✅ Correct usage -->*
<div s-if=\"condition1\">Content 1</div>
<div s-else-if=\"condition2\">Content 2</div>
<div s-else-if=\"condition3\">Content 3</div>
<div s-else>Fallback Content</div>
*<!-- ❌ Incorrect - can\'t have elements between -->*
<div s-if=\"condition1\">Content 1</div>
<p>This breaks the chain!</p>
<div s-else>Fallback</div> *<!-- This won\'t work -->*
*<!-- ❌ Incorrect - s-else must be last -->*
<div s-if=\"condition1\">Content 1</div>
<div s-else>Fallback</div>
<div s-else-if=\"condition2\">Content 2</div> *<!-- This won\'t
work -->*5.2 Visibility Toggling with s-show and s-hide
While s-if removes elements from the DOM, s-show and s-hide simply toggle their visibility using CSS. This makes them faster for frequently toggled content.
Understanding s-show
s-show shows an element when its condition is true and hides it when false by applying display: none.
<div s-app s-state=\"{
isVisible: true,
notification: \'You have 3 new messages\'
}\">
<h2>Notification Demo</h2>
*<!-- This element stays in DOM, just hidden with CSS -->*
<div class=\"notification\" s-show=\"isVisible\">
🔔 {notification}
<button s-click=\"isVisible = false\">Dismiss</button>
</div>
<button s-click=\"isVisible = !isVisible\">
{isVisible ? \'Hide\' : \'Show\'} Notification
</button>
*<!-- Check the DOM in browser tools - element is always there -->*
<p class=\"hint\">
<small>(Check browser inspector - notification is always in
DOM)</small>
</p>
</div>
<style>
.notification {
background: #007bff;
color: white;
padding: 15px;
border-radius: 8px;
margin: 10px 0;
transition: opacity 0.3s;
}
.hint { color: #666; margin-top: 20px; }
</style>The s-hide Directive
s-hide is the inverse of s-show - it hides the element when the condition is true and shows it when false.
<div s-app s-state=\"{
isLoading: false,
error: null,
data: null
}\">
<h2>Data Loading Demo</h2>
*<!-- Using s-hide to hide content while loading -->*
<div class=\"content\" s-hide=\"isLoading\">
<div s-if=\"data\">
<h3>Loaded Data:</h3>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
<div s-else-if=\"error\" class=\"error\">
❌ Error: {error}
</div>
<div s-else>Click load to fetch data
</div>
</div>
<!-- Loading indicator (shown when isLoading is true) -->
<div class="loading" s-show="isLoading">
⏳ Loading... Please wait
</div>
<!-- Control buttons -->
<button s-click="
isLoading = true;
error = null;
// Simulate API call
setTimeout(() => {
isLoading = false;
if(Math.random() > 0.3) {
data = { id: 1, name: 'Sample Data', timestamp: new Date().toISOString() };
} else {
error = 'Failed to load data';
}
}, 2000);
">
Load Data
</button>
<button s-click="isLoading = false; data = null; error = null">
Reset
</button>
</div>
<style>
.content { padding: 20px; background: #f8f9fa; border-radius: 8px; }
.loading { padding: 20px; background: #e2e3e5; text-align: center; border-radius: 8px; }
.error { color: #dc3545; }
</style>
s-if vs s-show: Choosing the Right Tool
Let's create a comprehensive comparison to understand when to use each:
<div s-app s-state=\"{
showModal: false,
expandedSections: {
details: false,
comments: false,
related: false
},
activeTab: \'home\',
userRole: \'guest\'
}\">
<h2>s-if vs s-show: When to Use Each</h2>
<div class=\"comparison\">
*<!-- Use s-if for: Rarely changing conditions -->*
<div class=\"card\">
<h3>✅ Use s-if WHEN:</h3>
<ul>
<li>Content changes rarely (login/logout)</li>
<li>Initial render cost is high</li>
<li>You want components to re-mount fresh</li>
<li>Condition is based on user role/permissions</li>
</ul>
*<!-- Example: User role-based content -->*
<div s-if=\"userRole === \'admin\'\" class=\"admin-panel\">
<h4>Admin Panel (s-if)</h4>
<p>This only renders for admins</p>
<button>Delete Database</button>
</div>
<div s-else-if=\"userRole === \'editor\'\" class=\"editor-panel\">
<h4>Editor Panel (s-if)</h4>
<p>This only renders for editors</p>
<button>Edit Content</button>
</div>
<div s-else class=\"guest-panel\">
<h4>Guest View (s-if)</h4>
<p>Please log in</p>
</div>
<button s-click=\"
userRole = userRole === \'admin\' ? \'guest\' :
userRole === \'guest\' ? \'editor\' : \'admin\'
\">
Cycle Role (current: {userRole})
</button>
</div>
*<!-- Use s-show for: Frequently toggling UI -->*
<div class=\"card\">
<h3>✅ Use s-show WHEN:</h3>
<ul>
<li>Content toggles frequently (dropdowns, accordions)</li>
<li>You need smooth CSS transitions</li>
<li>Content is already rendered</li>
<li>Toggle speed is critical</li>
</ul>
*<!-- Example: Accordion sections -->*
<div class=\"accordion\">
<div class=\"accordion-item\">
<button s-click=\"expandedSections.details =
!expandedSections.details\">
{expandedSections.details ? \'▼\' : \'▶\'} Details (s-show)
</button>
<div class=\"accordion-content\" s-show=\"expandedSections.details\">
<p>This content toggles frequently. s-show is perfect here!</p>
<p>It stays in DOM, just hidden with CSS.</p>
<p>You can also add CSS transitions:</p>
<p>Lorem ipsum dolor sit amet\...</p>
</div>
</div>
<div class=\"accordion-item\">
<button s-click=\"expandedSections.comments =
!expandedSections.comments\">
{expandedSections.comments ? \'▼\' : \'▶\'} Comments (s-show)
</button>
<div class=\"accordion-content\" s-show=\"expandedSections.comments\">
<p>Comment 1: Great article!</p>
<p>Comment 2: Very helpful</p>
<p>Comment 3: Thanks for this!</p>
</div>
</div>
</div>
</div>
</div>
*<!-- Modal example comparing both -->*
<div class=\"card\">
<h3>Modal Comparison</h3>
<div style=\"display: flex; gap: 20px;\">
*<!-- Modal with s-if -->*
<div>
<button s-click=\"showModal = \'if\'\">Open s-if Modal</button>
<div s-if=\"showModal === \'if\'\" class=\"modal\">
<div class=\"modal-content\">
<h4>s-if Modal</h4>
<p>This modal is completely removed from DOM when closed.</p>
<p>Good for: Login forms, signup, rare actions</p>
<button s-click=\"showModal = false\">Close</button>
</div>
</div>
</div>
*<!-- Modal with s-show -->*
<div>
<button s-click=\"showModal = \'show\'\">Open s-show Modal</button>
<div s-show=\"showModal === \'show\'\" class=\"modal\">
<div class=\"modal-content\">
<h4>s-show Modal</h4>
<p>This modal stays in DOM, just hidden with CSS.</p>
<p>Good for: Quick previews, tooltips, frequent toggles</p>
<button s-click=\"showModal = false\">Close</button>
</div>
</div>
</div>
</div>
<p class=\"hint\">
<small>Check browser inspector to see the DOM difference when closed.
s-if modal disappears completely, s-show modal has `display: none`.
</small>
</p>
</div>
</div>
<style>
.comparison { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.card {
border: 1px solid #ddd;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.admin-panel { background: #f8d7da; padding: 15px; border-radius: 4px; }
.editor-panel { background: #d4edda; padding: 15px; border-radius: 4px; }
.guest-panel { background: #e2e3e5; padding: 15px; border-radius: 4px; }
.accordion-item {
border: 1px solid #ddd;
margin: 5px 0;
border-radius: 4px;
}
.accordion-item button {
width: 100%;
text-align: left;
padding: 10px;
background: #f8f9fa;
border: none;
cursor: pointer;
}
.accordion-content {
padding: 15px;
background: white;
border-top: 1px solid #ddd;
transition: all 0.3s ease;
}
.modal {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 8px;
max-width: 400px;
}
.hint { color: #666; margin-top: 10px; }
</style>
Performance Comparison Table
| Aspect | s-if | s-show |
|---|---|---|
| DOM Presence | Removed when false | Always present |
| Initial Render | Only renders when true | Always renders once |
| Toggle Cost | High (DOM manipulation) | Low (CSS only) |
| CSS Transitions | Limited | Full support |
| Memory Usage | Lower when false | Higher (always in DOM) |
| Use Case | Rare changes, security | Frequent toggles, animations |
5.3 List Rendering Mastery with s-for
The s-for directive is one of SimpliJS's most powerful features. It lets you render lists of data with minimal code. Let's master it completely.
Basic s-for Syntax
<div s-app s-state=\"{
fruits: \[\'Apple\', \'Banana\', \'Orange\', \'Mango\'\],
users: \[
{ id: 1, name: \'Alice\', age: 28 },
{ id: 2, name: \'Bob\', age: 32 },
{ id: 3, name: \'Charlie\', age: 25 }
\]
}\">
<h2>Basic List Rendering</h2>
*<!-- Simple array of strings -->*
<h3>Fruits:</h3>
<ul>
<li s-for=\"fruit in fruits\">{fruit}</li>
</ul>
*<!-- Array of objects -->*
<h3>Users:</h3>
<div class=\"user-list\">
<div s-for=\"user in users\" class=\"user-card\">
<strong>{user.name}</strong> - Age: {user.age}
</div>
</div>
</div>
<style>
.user-card {
padding: 10px;
margin: 5px 0;
background: #f8f9fa;
border-radius: 4px;
}
</style>Accessing the Index
Often you need to know the current position in the loop:
<div s-app s-state=\"{
tasks: \[
\'Write documentation\',
\'Review pull requests\',
\'Update dependencies\',
\'Deploy to production\'
\]
}\">
<h2>Task List with Indices</h2>
<ol>
<li s-for=\"task, index in tasks\">
*<!-- index is 0-based -->*
Task {index + 1}: {task}
<button s-click=\"tasks.splice(index, 1)\">✓ Done</button>
</li>
</ol>
*<!-- You can also use index for conditional styling -->*
<h3>Zebra Striping with Index:</h3>
<div s-for=\"task, i in tasks\"
s-class=\"{ \'even-row\': i % 2 === 0, \'odd-row\': i % 2 === 1 }\">
{i}: {task}
</div>
</div>
<style>
.even-row { background: #f8f9fa; padding: 8px; }
.odd-row { background: #e9ecef; padding: 8px; }
</style>The Critical Importance of s-key
When rendering dynamic lists, s-key is essential for performance and correct behavior. It helps SimpliJS track which items have changed.
<div s-app s-state=\"{
items: \[
{ id: \'a1\', text: \'Item A\', color: \'red\' },
{ id: \'b2\', text: \'Item B\', color: \'blue\' },
{ id: \'c3\', text: \'Item C\', color: \'green\' }
\],
nextId: \'d4\'
}\">
<h2>Why s-key Matters</h2>
<div style=\"display: grid; grid-template-columns: 1fr 1fr; gap:
20px;\">
*<!-- Without s-key (problematic) -->*
<div class=\"card\">
<h3>❌ Without s-key</h3>
<p>Items may lose internal state when reordering</p>
<div s-for=\"item in items\" class=\"item\">
<div style=\"background: {item.color}; padding: 10px; margin: 5px;\">
<strong>{item.text}</strong>
<input type=\"text\" placeholder=\"Type something\...\">
</div>
</div>
<button s-click=\"items = \[items\[1\], items\[0\], items\[2\]\]\">Swap First Two (Watch inputs!)
</button>
<p class="hint">
<small>Type in inputs, then swap - the text moves with wrong items!</small>
</p>
</div>
<!-- With s-key (correct) -->
<div class="card">
<h3>✅ With s-key</h3>
<p>Items maintain correct identity</p>
<div s-for="item in items" s-key="item.id" class="item">
<div style="background: {item.color}; padding: 10px; margin: 5px;">
<strong>{item.text}</strong>
<input type="text" placeholder="Type something...">
</div>
</div>
<button s-click="items = [items[1], items[0], items[2]]">
Swap First Two (Watch inputs!)
</button>
<p class="hint">
<small>Type in inputs, then swap - text stays with correct items!</small>
</p>
</div>
</div>
<!-- Add new item demo -->
<button s-click="items.push({
id: nextId,
text: 'Item ' + String.fromCharCode(65 + items.length),
color: ['red','blue','green','purple','orange'][items.length % 5]
}); nextId = String.fromCharCode(97 + items.length) + (items.length + 1)">
Add Item
</button>
</div>
What Makes a Good Key?
A good key should be:
Stable - Doesn't change between renders
Unique - No two items share the same key
Predictable - Same item always gets same key
<div s-app s-state=\"{
users: \[
{ id: 1, username: \'alice\', email: \'alice@example.com\' },
{ id: 2, username: \'bob\', email: \'bob@example.com\' }
\]
}\">
<h2>Good vs Bad Keys</h2>
*<!-- ✅ Good: Using unique ID -->*
<div s-for=\"user in users\" s-key=\"user.id\">
Good: {user.username}
</div>
*<!-- ✅ Good: Using composite key if no ID -->*
<div s-for=\"user in users\" s-key=\"user.username + user.email\">
Also Good: {user.username}
</div>
*<!-- ❌ Bad: Using index as key (if list can change) -->*
<div s-for=\"user, index in users\" s-key=\"index\">
Bad: {user.username} (problems on reorder)
</div>
*<!-- ❌ Bad: Using non-unique values -->*
<div s-for=\"user in users\" s-key=\"user.username\">Bad if usernames aren't unique!
</div>
</div>
Advanced s-for Patterns
Nested Loops
<div s-app s-state=\"{
categories: \[
{
name: \'Electronics\',
products: \[
{ id: 1, name: \'Laptop\', price: 999 },
{ id: 2, name: \'Phone\', price: 699 }
\]
},
{
name: \'Clothing\',
products: \[
{ id: 3, name: \'Shirt\', price: 29 },
{ id: 4, name: \'Jeans\', price: 79 }
\]
}
\]
}\">
<h2>Nested Loops - Product Catalog</h2>
<div s-for=\"category in categories\" s-key=\"category.name\"
class=\"category\">
<h3>{category.name}</h3>
<div class=\"product-grid\">
<div s-for=\"product in category.products\"
s-key=\"product.id\"
class=\"product-card\">
<h4>{product.name}</h4>
<p>\${product.price}</p>
<button>Add to Cart</button>
</div>
</div>
</div>
</div>
<style>
.category { margin-bottom: 30px; }
.product-grid { display: grid; grid-template-columns: repeat(auto-fill,
minmax(150px, 1fr)); gap: 15px; }
.product-card { border: 1px solid #ddd; padding: 15px; border-radius:
8px; text-align: center; }
</style>Looping with Filters
<div s-app s-state=\"{
numbers: \[1, 2, 3, 4, 5, 6, 7, 8, 9, 10\],
minValue: 3,
showEven: false
}\">
<h2>Filtered Loops</h2>
*<!-- Filter directly in loop -->*
<h3>Numbers greater than {minValue}:</h3>
<ul>
<li s-for=\"n in numbers.filter(n => n > minValue)\" s-key=\"n\">
{n}
</li>
</ul>
*<!-- Conditional within loop -->*
<h3>Numbers with parity:</h3>
<ul>
<li s-for=\"n in numbers\" s-key=\"n\"
s-class=\"{ \'text-primary\': n % 2 === 0 }\">
{n} - {n % 2 === 0 ? \'Even\' : \'Odd\'}
</li>
</ul>
*<!-- Dynamic filter -->*
<h3>{showEven ? \'Even\' : \'Odd\'} numbers:</h3>
<ul>
<li s-for=\"n in numbers.filter(n => showEven ? n % 2 === 0 : n % 2
=== 1)\"
s-key=\"n\">
{n}
</li>
</ul>
<label>
<input type=\"checkbox\" s-model=\"showEven\">Show Even Numbers
</label>
<input type="range" s-bind="minValue" min="1" max="10">
</div>
<style>
.text-primary { color: #007bff; font-weight: bold; }
</style>
Looping with Sorting
<div s-app s-state=\"{
students: \[
{ name: \'Alice\', grade: 85 },
{ name: \'Bob\', grade: 92 },
{ name: \'Charlie\', grade: 78 },
{ name: \'Diana\', grade: 95 }
\],
sortBy: \'name\',
sortAsc: true
}\">
<h2>Sorted Lists</h2>
*<!-- Sort controls -->*
<div>
<label>
Sort by:
<select s-model=\"sortBy\">
<option value=\"name\">Name</option>
<option value=\"grade\">Grade</option>
</select>
</label>
<label>
<input type=\"checkbox\" s-model=\"sortAsc\">
Ascending
</label>
</div>
*<!-- Sorted list -->*
<table border=\"1\" cellpadding=\"8\">
<tr>
<th>Name</th>
<th>Grade</th>
</tr>
<tr s-for=\"student in students.sort((a, b) => {
if(sortBy === \'name\') {
return sortAsc
? a.name.localeCompare(b.name)
: b.name.localeCompare(a.name);
} else {
return sortAsc
? a.grade - b.grade
: b.grade - a.grade;
}
})\" s-key=\"student.name\">
<td>{student.name}</td>
<td>{student.grade}</td>
</tr>
</table>
</div>Performance Optimization in Lists
When working with large lists, follow these optimization tips:
<div s-app s-state=\"{
largeList: Array.from({ length: 1000 }, (_, i) => ({
id: i,
value: Math.random(),
text: \`Item \${i}\`
})),
pageSize: 50,
currentPage: 1
}\">
<h2>Optimizing Large Lists</h2>
*<!-- 1. Virtual scrolling concept (pagination) -->*
<div>
<h3>1. Pagination</h3>
<p>Total items: {largeList.length}</p>
*<!-- Calculate paginated items -->*
<div s-state=\"{
startIndex: (currentPage - 1) * pageSize,
endIndex: currentPage * pageSize,
paginatedItems: largeList.slice(startIndex, endIndex)
}\">
*<!-- Render only current page -->*
<div s-for=\"item in paginatedItems\" s-key=\"item.id\"
class=\"list-item\">
{item.id}: {item.text} - {item.value.toFixed(4)}
</div>
*<!-- Pagination controls -->*
<div class=\"pagination\">
<button s-click=\"currentPage = Math.max(1, currentPage - 1)\"
s-disabled=\"currentPage === 1\">
Previous
</button>
<span>Page {currentPage} of {Math.ceil(largeList.length /
pageSize)}</span>
<button s-click=\"currentPage = Math.min(Math.ceil(largeList.length /
pageSize), currentPage + 1)\"
s-disabled=\"currentPage === Math.ceil(largeList.length / pageSize)\">
Next
</button>
</div>
</div>
</div>
*<!-- 2. Use s-key with stable IDs -->*
<div class=\"tip\">
<h3>2. Always use s-key with unique IDs</h3>
<pre><div s-for=\"item in items\"
s-key=\"item.id\">\...</div></pre>
</div>
*<!-- 3. Avoid complex calculations in loops -->*
<div class=\"tip\">
<h3>3. Pre-compute expensive values</h3>
<div s-state=\"{
// Pre-compute expensive operations
processedItems: largeList.slice(0, 10).map(item => ({
\...item,
displayValue: expensiveCalculation(item.value)
}))
}\">
<div s-for=\"item in processedItems\" s-key=\"item.id\">
{item.displayValue}
</div>
</div>
</div>
</div>
<style>
.list-item {
padding: 5px;
border-bottom: 1px solid #eee;
font-size: 12px;
}
.pagination {
margin-top: 20px;
display: flex;
gap: 10px;
align-items: center;
}
.tip {
background: #d4edda;
padding: 15px;
border-radius: 8px;
margin: 20px 0;
}
</style>
<script>
*// Simulate expensive calculation*
function expensiveCalculation(value) {
*// In real app, this might be complex*
return Math.sin(value) * Math.cos(value);
}
</script>5.4 Combining Control Flow for Complex UIs
Real applications often combine multiple control flow directives. Let's build some complex examples.
Example: Data Table with Sorting, Filtering, and Pagination
<div s-app s-state=\"{
// Sample data
employees: \[
{ id: 1, name: \'Alice Johnson\', department: \'Engineering\', salary:
85000, active: true },
{ id: 2, name: \'Bob Smith\', department: \'Marketing\', salary: 65000,
active: true },
{ id: 3, name: \'Charlie Brown\', department: \'Sales\', salary: 75000,
active: false },
{ id: 4, name: \'Diana Prince\', department: \'Engineering\', salary:
95000, active: true },
{ id: 5, name: \'Edward Norton\', department: \'HR\', salary: 55000,
active: true },
{ id: 6, name: \'Fiona Apple\', department: \'Marketing\', salary:
70000, active: false },
{ id: 7, name: \'George Costanza\', department: \'Sales\', salary:
60000, active: true },
{ id: 8, name: \'Helen Keller\', department: \'HR\', salary: 58000,
active: true }
\],
// UI state
searchTerm: \'\',
departmentFilter: \'all\',
showActiveOnly: false,
sortField: \'name\',
sortAsc: true,
currentPage: 1,
pageSize: 3,
// Available departments for filter
departments: \[\'Engineering\', \'Marketing\', \'Sales\', \'HR\'\]
}\">
<h2>Employee Directory</h2>
*<!-- Filter controls -->*
<div class=\"filters\">
<input
type=\"text\"
s-bind=\"searchTerm\"
placeholder=\"Search by name\...\"
class=\"search-input\"
>
<select s-model=\"departmentFilter\">
<option value=\"all\">All Departments</option>
<option s-for=\"dept in departments\"
value=\"{dept}\">{dept}</option>
</select>
<label>
<input type=\"checkbox\" s-model=\"showActiveOnly\">Show Active Only
</label>
</div>
<!-- Sort controls -->
<div class="sort-controls">
<label>Sort by:</label>
<select s-model="sortField">
<option value="name">Name</option>
<option value="department">Department</option>
<option value="salary">Salary</option>
</select>
<button s-click="sortAsc = !sortAsc">
{sortAsc ? '↑ Ascending' : '↓ Descending'}
</button>
</div>
<!-- Filtered and sorted data -->
<div s-state="{
filteredData: employees
.filter(emp => {
// Search filter
if(searchTerm && !emp.name.toLowerCase().includes(searchTerm.toLowerCase())) {
return false;
}
// Department filter
if(departmentFilter !== 'all' && emp.department !== departmentFilter) {
return false;
}
// Active filter
if(showActiveOnly && !emp.active) {
return false;
}
return true;
})
.sort((a, b) => {
let valA = a[sortField];
let valB = b[sortField];
// Handle string comparison
if(typeof valA === 'string') {
valA = valA.toLowerCase();
valB = valB.toLowerCase();
}
if(valA < valB) return sortAsc ? -1 : 1;
if(valA > valB) return sortAsc ? 1 : -1;
return 0;
})
}">
<!-- Pagination info -->
<div class="pagination-info">
Showing page {currentPage} of {Math.ceil(filteredData.length / pageSize) || 1}
(Total: {filteredData.length} records)
</div>
<!-- Data table -->
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th s-click="sortField = 'name'; sortAsc = sortField === 'name' ? !sortAsc : true">
Name {sortField === 'name' ? (sortAsc ? '↑' : '↓') : ''}
</th>
<th s-click="sortField = 'department'; sortAsc = sortField === 'department' ? !sortAsc : true">
Department {sortField === 'department' ? (sortAsc ? '↑' : '↓') : ''}
</th>
<th s-click="sortField = 'salary'; sortAsc = sortField === 'salary' ? !sortAsc : true">
Salary {sortField === 'salary' ? (sortAsc ? '↑' : '↓') : ''}
</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<!-- Show if no data -->
<tr s-if="filteredData.length === 0">
<td colspan="5" class="no-data">
No employees match your filters
</td>
</tr>
<!-- Show paginated data -->
<tr s-for="emp in filteredData.slice(
(currentPage - 1) * pageSize,
currentPage * pageSize
)"
s-key="emp.id"
s-class="{ 'inactive-row': !emp.active }">
<td>{emp.id}</td>
<td>{emp.name}</td>
<td>{emp.department}</td>
<td>${emp.salary.toLocaleString()}</td>
<td>
<span class="status-badge"
s-class="{ 'active': emp.active, 'inactive': !emp.active }">
{emp.active ? 'Active' : 'Inactive'}
</span>
</td>
</tr>
</tbody>
</table>
<!-- Pagination controls -->
<div class="pagination" s-if="filteredData.length > pageSize">
<button s-click="currentPage = 1"
s-disabled="currentPage === 1">
⏮️ First
</button>
<button s-click="currentPage = Math.max(1, currentPage - 1)"
s-disabled="currentPage === 1">
◀ Previous
</button>
<span class="page-numbers">
<button s-for="page in Math.ceil(filteredData.length / pageSize)"
s-if="Math.abs(page - currentPage) <= 2 || page === 1 || page === Math.ceil(filteredData.length / pageSize)"
s-key="page"
s-click="currentPage = page"
s-class="{ 'current-page': page === currentPage }">
{page}
<span s-if="page === 1 && currentPage > 3">...</span>
</button>
</span>
<button s-click="currentPage = Math.min(Math.ceil(filteredData.length / pageSize), currentPage + 1)"
s-disabled="currentPage === Math.ceil(filteredData.length / pageSize)">
Next ▶
</button>
<button s-click="currentPage = Math.ceil(filteredData.length / pageSize)"
s-disabled="currentPage === Math.ceil(filteredData.length / pageSize)">
Last ⏭️
</button>
</div>
<!-- Summary statistics -->
<div class="summary" s-if="filteredData.length > 0">
<h4>Summary Statistics</h4>
<p>
Average Salary: ${(
filteredData.reduce((sum, emp) => sum + emp.salary, 0) /
filteredData.length
).toFixed(2)}
</p>
<p>
Total Payroll: ${filteredData.reduce((sum, emp) => sum + emp.salary, 0).toLocaleString()}
</p>
<p>
Active Employees: {filteredData.filter(e => e.active).length} |
Inactive: {filteredData.filter(e => !e.active).length}
</p>
</div>
</div>
</div>
<style>
.filters, .sort-controls {
margin: 15px 0;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.search-input {
flex: 1;
padding: 8px;
border: 2px solid #ddd;
border-radius: 4px;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.data-table th {
background: #f8f9fa;
padding: 12px;
text-align: left;
cursor: pointer;
user-select: none;
}
.data-table th:hover {
background: #e9ecef;
}
.data-table td {
padding: 12px;
border-bottom: 1px solid #ddd;
}
.inactive-row {
background: #f8d7da;
opacity: 0.8;
}
.status-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.status-badge.active {
background: #d4edda;
color: #155724;
}
.status-badge.inactive {
background: #f8d7da;
color: #721c24;
}
.no-data {
text-align: center;
padding: 40px;
color: #666;
font-style: italic;
}
.pagination-info {
margin: 10px 0;
color: #666;
}
.pagination {
display: flex;
gap: 5px;
justify-content: center;
margin: 20px 0;
}
.pagination button {
padding: 8px 12px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 4px;
}
.pagination button:hover:not(:disabled) {
background: #007bff;
color: white;
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-numbers {
display: flex;
gap: 5px;
}
.current-page {
background: #007bff !important;
color: white;
border-color: #007bff;
}
.summary {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
}
</style>
Example: Dynamic Form Builder
This example shows how to build forms dynamically based on configuration:
<div s-app s-state=\"{
formConfig: {
title: \'User Registration\',
fields: \[
{ type: \'text\', name: \'firstName\', label: \'First Name\', required:
true },
{ type: \'text\', name: \'lastName\', label: \'Last Name\', required:
true },
{ type: \'email\', name: \'email\', label: \'Email Address\', required:
true },
{ type: \'password\', name: \'password\', label: \'Password\', required:
true, minLength: 8 },
{ type: \'number\', name: \'age\', label: \'Age\', min: 18, max: 120 },
{ type: \'select\', name: \'country\', label: \'Country\',
options: \[\'USA\', \'Canada\', \'UK\', \'Australia\', \'Other\'\] },
{ type: \'checkbox\', name: \'newsletter\', label: \'Subscribe to
newsletter\' },
{ type: \'radio\', name: \'gender\', label: \'Gender\',
options: \[\'Male\', \'Female\', \'Other\', \'Prefer not to say\'\] },
{ type: \'textarea\', name: \'bio\', label: \'Biography\', rows: 4 }
\],
buttons: \[
{ type: \'submit\', text: \'Register\', class: \'btn-primary\' },
{ type: \'reset\', text: \'Clear\', class: \'btn-secondary\' }
\]
},
// Form data will be populated dynamically
formData: {},
// Validation errors
errors: {},
// Form submission status
submitted: false
}\">
<h2>{formConfig.title}</h2>
*<!-- Initialize formData based on config -->*
<div s-state=\"{
init: (function() {
// Create empty form data based on field defaults
formConfig.fields.forEach(field => {
if(!formData.hasOwnProperty(field.name)) {
if(field.type === \'checkbox\') {
formData\[field.name\] = false;
} else if(field.type === \'number\') {
formData\[field.name\] = field.min \|\| 0;
} else {
formData\[field.name\] = \'\';
}
}
});
})()
}\"></div>
*<!-- Dynamic form -->*
<form class=\"dynamic-form\" s-submit=\"
// Validate form
let isValid = true;
errors = {};
formConfig.fields.forEach(field => {
if(field.required && !formData\[field.name\]) {
errors\[field.name\] = \`\${field.label} is required\`;
isValid = false;
}
if(field.type === \'email\' && formData\[field.name\] &&
!formData\[field.name\].includes(\'@\')) {
errors\[field.name\] = \'Invalid email address\';
isValid = false;
}
if(field.minLength && formData\[field.name\] &&
formData\[field.name\].length < field.minLength) {
errors\[field.name\] = \`\${field.label} must be at least
\${field.minLength} characters\`;
isValid = false;
}
if(field.type === \'number\' && formData\[field.name\]) {
if(field.min && formData\[field.name\] < field.min) {
errors\[field.name\] = \`\${field.label} must be at least
\${field.min}\`;
isValid = false;
}
if(field.max && formData\[field.name\] > field.max) {
errors\[field.name\] = \`\${field.label} must be at most
\${field.max}\`;
isValid = false;
}
}
});
if(isValid) {
submitted = true;
console.log(\'Form submitted:\', formData);
}
\">
*<!-- Loop through fields to render them -->*
<div s-for=\"field in formConfig.fields\" s-key=\"field.name\"
class=\"form-group\">
<label for=\"{field.name}\">
{field.label}
<span s-if=\"field.required\" class=\"required\">*</span>
</label>
*<!-- Render different input types based on field.type -->*
<div>
*<!-- Text, email, password, number inputs -->*
<input s-if=\"\[\'text\', \'email\', \'password\',
\'number\'\].includes(field.type)\"
type=\"{field.type}\"
id=\"{field.name}\"
name=\"{field.name}\"
s-bind=\"formData\[field.name\]\"
placeholder=\"Enter {field.label.toLowerCase()}\"
min=\"{field.min}\"
max=\"{field.max}\"
class=\"{errors\[field.name\] ? \'error\' : \'\'}\">
*<!-- Select dropdown -->*
<select s-else-if=\"field.type === \'select\'\"
id=\"{field.name}\"
name=\"{field.name}\"
s-model=\"formData\[field.name\]\"
class=\"{errors\[field.name\] ? \'error\' : \'\'}\">
<option value=\"\">Select {field.label}</option>
<option s-for=\"option in field.options\" value=\"{option}\">
{option}
</option>
</select>
*<!-- Checkbox -->*
<label s-else-if=\"field.type === \'checkbox\'\"
class=\"checkbox-label\">
<input type=\"checkbox\"
id=\"{field.name}\"
name=\"{field.name}\"
s-model=\"formData\[field.name\]\">
{field.label}
</label>
*<!-- Radio buttons -->*
<div s-else-if=\"field.type === \'radio\'\" class=\"radio-group\">
<label s-for=\"option in field.options\" class=\"radio-label\">
<input type=\"radio\"
name=\"{field.name}\"
value=\"{option}\"
s-model=\"formData\[field.name\]\"
s-checked=\"formData\[field.name\] === option\">
{option}
</label>
</div>
*<!-- Textarea -->*
<textarea s-else-if=\"field.type === \'textarea\'\"
id=\"{field.name}\"
name=\"{field.name}\"
s-bind=\"formData\[field.name\]\"
rows=\"{field.rows \|\| 3}\"
placeholder=\"Enter {field.label.toLowerCase()}\"
class=\"{errors\[field.name\] ? \'error\' : \'\'}\">
</textarea>
</div>
*<!-- Error message -->*
<span s-if=\"errors\[field.name\]\" class=\"error-message\">
{errors\[field.name\]}
</span>
*<!-- Hint text -->*
<small s-if=\"field.minLength\" class=\"hint\">
Minimum {field.minLength} characters
</small>
</div>
*<!-- Form buttons -->*
<div class=\"form-actions\">
<button s-for=\"btn in formConfig.buttons\"
s-key=\"btn.text\"
type=\"{btn.type}\"
class=\"{btn.class}\">
{btn.text}
</button>
</div>
</form>
*<!-- Success message -->*
<div s-if=\"submitted\" class=\"success-message\">
<h3>✅ Form Submitted Successfully!</h3>
<pre>{JSON.stringify(formData, null, 2)}</pre>
<button s-click=\"submitted = false\">OK</button>
</div>
*<!-- Live preview -->*
<div class=\"live-preview\" s-if=\"Object.keys(formData).length >
0\">
<h4>Live Form Data:</h4>
<pre>{JSON.stringify(formData, null, 2)}</pre>
</div>
</div>
<style>
.dynamic-form {
max-width: 600px;
margin: 0 auto;
padding: 30px;
background: white;
border-radius: 12px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #333;
}
.required {
color: #dc3545;
margin-left: 5px;
}
input\[type=\"text\"\],
input\[type=\"email\"\],
input\[type=\"password\"\],
input\[type=\"number\"\],
select,
textarea {
width: 100%;
padding: 10px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: #007bff;
}
input.error,
select.error,
textarea.error {
border-color: #dc3545;
}
.error-message {
color: #dc3545;
font-size: 14px;
margin-top: 5px;
display: block;
}
.hint {
color: #666;
font-size: 12px;
margin-top: 5px;
display: block;
}
.checkbox-label,
.radio-label {
display: flex;
align-items: center;
gap: 8px;
font-weight: normal;
cursor: pointer;
}
.radio-group {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.form-actions {
margin-top: 30px;
display: flex;
gap: 15px;
}
.form-actions button {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-primary:hover {
background: #0056b3;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #545b62;
}
.success-message {
margin-top: 20px;
padding: 20px;
background: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 8px;
color: #155724;
}
.live-preview {
margin-top: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
font-size: 14px;
}
.live-preview pre {
background: white;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
}
</style>Chapter 5 Summary
You've now mastered control flow in SimpliJS:
Conditional rendering with s-if, s-else-if, and s-else
Visibility toggling with s-show and s-hide
Understanding when to use s-if vs s-show
List rendering with s-for including index access
The critical importance of s-key for performance and correctness
Advanced patterns like nested loops, filtering, and sorting
Performance optimization techniques for large lists
Complex real-world examples combining multiple control flow directives
You've seen how these directives work together to create sophisticated, interactive applications—all without writing a single line of traditional JavaScript. The form builder example alone demonstrates the power of declarative programming: a complete, dynamic form system built with just HTML and SimpliJS directives.
In the next chapter, we'll explore user interaction and events, learning how to handle clicks, inputs, form submissions, and more.
End of Chapter 5
Chapter 6: User Interaction and Events
Welcome to Chapter 6, where we bring your applications to life through user interaction. So far, you've learned how to manage state and control the flow of your application. Now it's time to make your apps respond to user actions like clicks, typing, form submissions, and more. In SimpliJS, handling events is intuitive and powerful—all through simple HTML attributes.
6.1 Understanding Event Directives
Event directives are HTML attributes that start with s- followed by the event name. They allow you to respond to user actions directly in your HTML.
Basic Event Syntax
<div s-app s-state=\"{ count: 0 }\">
<h2>Basic Event Handling</h2>
*<!-- Click event -->*
<button s-click=\"count = count + 1\">
Clicked {count} times
</button>
*<!-- Different events -->*
<button s-mouseenter=\"console.log(\'Mouse entered!\')\">Hover over me
</button>
<input s-focus="console.log('Input focused')"
s-blur="console.log('Input lost focus')"
placeholder="Click to focus">
</div>
Available Event Directives
SimpliJS supports all standard DOM events. Here are the most commonly used ones:
| Directive | DOM Event | Triggered When |
|---|---|---|
| s-click | click | Element is clicked |
| s-dblclick | dblclick | Element is double-clicked |
| s-mousedown | mousedown | Mouse button pressed |
| s-mouseup | mouseup | Mouse button released |
| s-mouseenter | mouseenter | Mouse enters element |
| s-mouseleave | mouseleave | Mouse leaves element |
| s-mousemove | mousemove | Mouse moves over element |
| s-keydown | keydown | Key is pressed down |
| s-keyup | keyup | Key is released |
| s-keypress | keypress | Key is pressed (character) |
| s-input | input | Input value changes |
| s-change | change | Input loses focus with changes |
| s-submit | submit | Form is submitted |
| s-focus | focus | Element gains focus |
| s-blur | blur | Element loses focus |
| s-scroll | scroll | Element is scrolled |
| s-resize | resize | Window is resized |
6.2 The s-click Directive: Handling Clicks
The s-click directive is your primary tool for handling mouse clicks. Let's explore its full capabilities.
Basic Click Handlers
<div s-app s-state=\"{
clicks: 0,
lastClick: \'\',
buttonStyle: \'primary\'
}\">
<h2>s-click Examples</h2>
*<!-- Simple increment -->*
<button s-click=\"clicks++\">
Increment: {clicks}
</button>
*<!-- Multiple statements -->*
<button s-click=\"
clicks = clicks + 5;
lastClick = \'Added 5 at \' + new Date().toLocaleTimeString()
\">
Add 5
</button>
*<!-- Conditional logic -->*
<button s-click=\"
if(clicks >= 10) {
alert(\'You reached 10 clicks!\');
} else {
clicks++;
}
\">
Conditional Click
</button>
*<!-- Using functions (preview of later chapters) -->*
<button s-click=\"handleClick(\'custom\')\">
Custom Handler
</button>
*<!-- Display results -->*
<p>Total clicks: {clicks}</p>
<p s-if=\"lastClick\">Last action: {lastClick}</p>
</div>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// Define a component with methods*
component(\'click-demo\', () => {
return {
handleClick: (type) => {
console.log(\'Click handled:\', type);
alert(\'Method called!\');
},
render: () => \`
<div s-app s-state=\"{ clicks: 0 }\">
<button s-click=\"clicks++\">Count: {clicks}</button>
<button s-click=\"handleClick(\'test\')\">Test Method</button>
</div>
\`
};
});
createApp().mount(\'\[s-app\]\');
</script>Click Modifiers
SimpliJS supports event modifiers that change how events are handled:
<div s-app s-state=\"{ count: 0 }\">
<h2>Click Modifiers</h2>
*<!-- Prevent default behavior -->*
<a href=\"https://example.com\" s-click.prevent=\"console.log(\'Link
clicked but navigation prevented\')\">Click me (navigation prevented)
</a>
<!-- Stop propagation -->
<div s-click="console.log('Div clicked')" style="padding: 20px; background: #f0f0f0;">
<button s-click.stop="console.log('Button clicked - parent not notified')">
Click me (stop propagation)
</button>
</div>
<!-- Once - only triggers once -->
<button s-click.once="count++">
Can only click once: {count}
</button>
<!-- Self - only triggers if event target is the element itself -->
<div s-click.self="console.log('Div clicked directly')"
style="padding: 20px; background: #e0e0e0;">
<button>Clicking button doesn't trigger div's handler</button>
</div>
<!-- Capture - use capture phase -->
<div s-click.capture="console.log('Capture phase')">
<button s-click="console.log('Bubble phase')">Check console order</button>
</div>
</div>
6.3 Working with Form Events: s-input, s-change, s-submit
Form events are crucial for collecting user input. Let's master them.
s-input: Real-time Input Tracking
<div s-app s-state=\"{
text: \'\',
searchResults: \[\],
charCount: 0,
wordCount: 0
}\">
<h2>Real-time Input with s-input</h2>
<input
type=\"text\"
s-input=\"
text = event.target.value;
charCount = text.length;
wordCount = text.split(/\\s+/).filter(w => w).length;
// Simulate search
if(text.length > 2) {
searchResults = \[\'Result 1\', \'Result 2\', \'Result 3\'\]
.map(r => \`\${r} for \"\${text}\"\`);
} else {
searchResults = \[\];
}
\"
placeholder=\"Type something\...\"
style=\"width: 100%; padding: 10px; font-size: 16px;\"
>
<div class=\"stats\">
<p>Current text: \"{text}\"</p>
<p>Characters: {charCount}</p>
<p>Words: {wordCount}</p>
</div>
<div s-if=\"searchResults.length > 0\" class=\"results\">
<h4>Search Results:</h4>
<ul>
<li s-for=\"result in searchResults\">{result}</li>
</ul>
</div>
</div>
<style>
.stats {
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.results {
margin-top: 20px;
padding: 15px;
background: #e3f2fd;
border-radius: 8px;
}
</style>s-change: When Input is Finalized
<div s-app s-state=\"{
selectedOption: \'\',
confirmedValue: \'\',
lastChange: \'\'
}\">
<h2>s-change - Triggered on Blur</h2>
<select s-change=\"
selectedOption = event.target.value;
lastChange = \'Changed at \' + new Date().toLocaleTimeString();
\">
<option value=\"\">Select an option\...</option>
<option value=\"option1\">Option 1</option>
<option value=\"option2\">Option 2</option>
<option value=\"option3\">Option 3</option>
</select>
<input
type=\"text\"
s-change=\"confirmedValue = event.target.value\"
placeholder=\"Type and press Enter or tab out\"
style=\"margin-left: 10px; padding: 5px;\"
>
<div class=\"change-info\">
<p>Selected: {selectedOption \|\| \'None\'}</p>
<p>Confirmed text: {confirmedValue \|\| \'Not set\'}</p>
<p>Last change: {lastChange}</p>
</div>
<p class=\"hint\">
<small>s-change triggers when you leave the field, not on every
keystroke</small>
</p>
</div>s-submit: Handling Form Submissions
<div s-app s-state=\"{
formData: {
username: \'\',
email: \'\',
password: \'\',
confirmPassword: \'\'
},
errors: {},
submitted: false,
attempts: 0
}\">
<h2>Form Submission with s-submit</h2>
<form class=\"registration-form\" s-submit=\"
attempts++;
errors = {};
// Validation
if(!formData.username) {
errors.username = \'Username is required\';
} else if(formData.username.length < 3) {
errors.username = \'Username must be at least 3 characters\';
}
if(!formData.email) {
errors.email = \'Email is required\';
} else if(!formData.email.includes(\'@\')) {
errors.email = \'Invalid email format\';
}
if(!formData.password) {
errors.password = \'Password is required\';
} else if(formData.password.length < 6) {
errors.password = \'Password must be at least 6 characters\';
}
if(formData.password !== formData.confirmPassword) {
errors.confirmPassword = \'Passwords do not match\';
}
// If no errors, submit
if(Object.keys(errors).length === 0) {
submitted = true;
console.log(\'Form submitted:\', formData);
} else {
submitted = false;
}
\">
<div class=\"form-group\">
<label>Username:</label>
<input
type=\"text\"
s-bind=\"formData.username\"
s-input=\"delete errors.username\"
placeholder=\"Enter username\"
class=\"{errors.username ? \'error\' : \'\'}\"
>
<span s-if=\"errors.username\" class=\"error-message\">
{errors.username}
</span>
</div>
<div class=\"form-group\">
<label>Email:</label>
<input
type=\"email\"
s-bind=\"formData.email\"
s-input=\"delete errors.email\"
placeholder=\"Enter email\"
class=\"{errors.email ? \'error\' : \'\'}\"
>
<span s-if=\"errors.email\" class=\"error-message\">
{errors.email}
</span>
</div>
<div class=\"form-group\">
<label>Password:</label>
<input
type=\"password\"
s-bind=\"formData.password\"
s-input=\"delete errors.password\"
placeholder=\"Enter password\"
class=\"{errors.password ? \'error\' : \'\'}\"
>
<span s-if=\"errors.password\" class=\"error-message\">
{errors.password}
</span>
</div>
<div class=\"form-group\">
<label>Confirm Password:</label>
<input
type=\"password\"
s-bind=\"formData.confirmPassword\"
s-input=\"delete errors.confirmPassword\"
placeholder=\"Confirm password\"
class=\"{errors.confirmPassword ? \'error\' : \'\'}\"
>
<span s-if=\"errors.confirmPassword\" class=\"error-message\">
{errors.confirmPassword}
</span>
</div>
<div class=\"form-actions\">
<button type=\"submit\">Register</button>
<button type=\"button\" s-click=\"
formData = { username: \'\', email: \'\', password: \'\',
confirmPassword: \'\' };
errors = {};
submitted = false;
\">Reset</button>
</div>
<div class=\"stats\">
<p>Attempts: {attempts}</p>
</div>
</form>
<div s-if=\"submitted\" class=\"success-message\">
<h3>✅ Registration Successful!</h3>
<p>Welcome, {formData.username}!</p>
<p>Confirmation email sent to {formData.email}</p>
</div>
</div>
<style>
.registration-form {
max-width: 400px;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.form-group input {
width: 100%;
padding: 8px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.form-group input.error {
border-color: #dc3545;
}
.error-message {
color: #dc3545;
font-size: 14px;
margin-top: 5px;
display: block;
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
.form-actions button {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
.form-actions button\[type=\"submit\"\] {
background: #007bff;
color: white;
}
.form-actions button\[type=\"submit\"\]:hover {
background: #0056b3;
}
.form-actions button\[type=\"button\"\] {
background: #6c757d;
color: white;
}
.form-actions button\[type=\"button\"\]:hover {
background: #545b62;
}
.success-message {
margin-top: 20px;
padding: 20px;
background: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 8px;
color: #155724;
}
.stats {
margin-top: 15px;
padding: 10px;
background: #f8f9fa;
border-radius: 4px;
font-size: 14px;
}
</style>6.4 Keyboard Events: s-keydown, s-keyup, and Key Modifiers
Keyboard events let you respond to key presses, essential for shortcuts, navigation, and forms.
Basic Keyboard Events
<div s-app s-state=\"{
key: \'\',
keyCode: \'\',
modifierKeys: \'\',
typed: \'\'
}\">
<h2>Keyboard Events</h2>
<input
type=\"text\"
s-keydown=\"
key = event.key;
keyCode = event.code;
modifierKeys = \[
event.ctrlKey ? \'Ctrl\' : \'\',
event.shiftKey ? \'Shift\' : \'\',
event.altKey ? \'Alt\' : \'\',
event.metaKey ? \'Meta\' : \'\'
\].filter(Boolean).join(\'+\');
\"
s-keyup=\"console.log(\'Key up:\', event.key)\"
placeholder=\"Type here and watch the info below\"
style=\"width: 100%; padding: 10px; font-size: 16px;\"
>
<div class=\"key-info\">
<p><strong>Key pressed:</strong> {key \|\| \'None\'}</p>
<p><strong>Key code:</strong> {keyCode \|\| \'None\'}</p>
<p><strong>Modifiers:</strong> {modifierKeys \|\| \'None\'}</p>
</div>
<div class=\"keyboard-shortcuts\">
<h3>Try these shortcuts:</h3>
<ul>
<li>Ctrl+S - Save</li>
<li>Ctrl+Z - Undo</li>
<li>Ctrl+Shift+F - Search</li>
</ul>
</div>
</div>
<style>
.key-info {
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
font-family: monospace;
font-size: 16px;
}
.keyboard-shortcuts {
margin-top: 20px;
padding: 15px;
background: #e3f2fd;
border-radius: 8px;
}
</style>Key-Specific Event Modifiers
SimpliJS provides special syntax for handling specific keys:
<div s-app s-state=\"{
messages: \[\],
text: \'\',
isTyping: false
}\">
<h2>Key-Specific Handlers</h2>
<div class=\"chat-demo\">
*<!-- s-key:enter - triggers on Enter key -->*
<input
type=\"text\"
s-bind=\"text\"
s-key:enter=\"
if(text.trim()) {
messages.push({
text: text,
time: new Date().toLocaleTimeString()
});
text = \'\';
}
\"
s-key:escape=\"
text = \'\';
console.log(\'Cleared with Escape\');
\"
s-focus=\"isTyping = true\"
s-blur=\"isTyping = false\"
placeholder=\"Type and press Enter to send, Escape to clear\"
style=\"width: 100%; padding: 10px; margin-bottom: 10px;\"
>
*<!-- Multiple key shortcuts -->*
<div class=\"shortcuts\">
<button s-click:ctrl.s=\"save()\">Ctrl+S (Save)</button>
<button s-click:ctrl.z=\"undo()\">Ctrl+Z (Undo)</button>
<button s-click:ctrl.shift.f=\"search()\">Ctrl+Shift+F
(Search)</button>
</div>
*<!-- Typing indicator -->*
<div s-show=\"isTyping\" class=\"typing-indicator\">
Typing\... {text}
</div>
*<!-- Messages -->*
<div class=\"messages\">
<div s-for=\"msg, i in messages\" s-key=\"i\" class=\"message\">
<span class=\"time\">\[{msg.time}\]</span>
<span class=\"text\">{msg.text}</span>
</div>
<div s-if=\"messages.length === 0\" class=\"empty\">No messages yet. Type and press Enter!
</div>
</div>
</div>
</div>
<style>
.chat-demo {
max-width: 500px;
margin: 20px 0;
}
.shortcuts {
display: flex;
gap: 10px;
margin: 10px 0;
}
.shortcuts button {
padding: 5px 10px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.typing-indicator {
padding: 5px;
color: #28a745;
font-style: italic;
}
.messages {
margin-top: 20px;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
min-height: 200px;
max-height: 300px;
overflow-y: auto;
}
.message {
padding: 8px;
margin: 5px 0;
background: #f8f9fa;
border-radius: 4px;
}
.message .time {
color: #666;
font-size: 12px;
margin-right: 10px;
}
.empty {
text-align: center;
color: #999;
padding: 40px;
}
</style>
Available Key Modifiers
| Modifier | Description |
|---|---|
| s-key:enter | Enter key |
| s-key:tab | Tab key |
| s-key:delete | Delete key |
| s-key:backspace | Backspace key |
| s-key:escape | Escape key |
| s-key:space | Spacebar |
| s-key:up | Up arrow |
| s-key:down | Down arrow |
| s-key:left | Left arrow |
| s-key:right | Right arrow |
| s-key:ctrl.s | Ctrl+S combination |
| s-key:shift.a | Shift+A combination |
| s-key:alt.f4 | Alt+F4 combination |
6.5 Mouse Events: Hover, Move, and More
Mouse events let you create rich interactive experiences based on mouse movement and position.
Mouse Enter/Leave for Hover Effects
<div s-app s-state=\"{
hovered: false,
hoverPosition: { x: 0, y: 0 },
tooltip: \'\',
cards: \[
{ id: 1, title: \'Product 1\', description: \'Amazing product with great
features\' },
{ id: 2, title: \'Product 2\', description: \'Incredible value for
money\' },
{ id: 3, title: \'Product 3\', description: \'Limited edition, get it
now\' }
\]
}\">
<h2>Mouse Events Demo</h2>
*<!-- Basic hover -->*
<div class=\"hover-box\"
s-mouseenter=\"hovered = true\"
s-mouseleave=\"hovered = false\">Hover over me!
<div s-show="hovered" class="hover-content">
✨ Surprise! You hovered!
</div>
</div>
<!-- Mouse move tracking -->
<div class="tracking-area"
s-mousemove="hoverPosition = { x: event.offsetX, y: event.offsetY }">
<p>Move mouse inside this box</p>
<p class="coordinates">
X: {hoverPosition.x}, Y: {hoverPosition.y}
</p>
<div class="dot"
s-style="{
left: hoverPosition.x + 'px',
top: hoverPosition.y + 'px',
position: 'absolute',
width: '10px',
height: '10px',
background: 'red',
borderRadius: '50%',
pointerEvents: 'none'
}"
s-show="hoverPosition.x > 0">
</div>
</div>
<!-- Product cards with tooltips -->
<div class="product-grid">
<div s-for="card in cards"
s-key="card.id"
class="product-card"
s-mouseenter="tooltip = card.description"
s-mouseleave="tooltip = ''">
<h3>{card.title}</h3>
<p>Hover for details</p>
<div s-show="tooltip === card.description" class="tooltip">
{card.description}
</div>
</div>
</div>
</div>
<style>
.hover-box {
width: 200px;
height: 100px;
background: #007bff;
color: white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
margin: 20px 0;
position: relative;
transition: transform 0.3s;
}
.hover-box:hover {
transform: scale(1.05);
}
.hover-content {
position: absolute;
top: -30px;
background: #28a745;
padding: 5px 10px;
border-radius: 20px;
font-size: 12px;
white-space: nowrap;
}
.tracking-area {
width: 400px;
height: 200px;
background: #f8f9fa;
border: 2px dashed #007bff;
border-radius: 8px;
margin: 20px 0;
position: relative;
cursor: crosshair;
}
.coordinates {
font-family: monospace;
font-size: 14px;
color: #666;
}
.product-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin: 20px 0;
}
.product-card {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
position: relative;
cursor: help;
transition: box-shadow 0.3s;
}
.product-card:hover {
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
margin-bottom: 5px;
z-index: 10;
}
.tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #333 transparent transparent transparent;
}
</style>
6.6 Working with the Event Object
In every event handler, you have access to the native JavaScript event object through the event variable. This gives you detailed information about the event.
Accessing Event Properties
<div s-app s-state=\"{
eventInfo: {
type: \'\',
target: \'\',
currentTarget: \'\',
timestamp: \'\',
clientX: 0,
clientY: 0,
key: \'\',
ctrlKey: false,
shiftKey: false
}
}\">
<h2>Event Object Properties</h2>
<div class=\"event-demo\">
<button s-click=\"
eventInfo.type = event.type;
eventInfo.target = event.target.tagName;
eventInfo.currentTarget = event.currentTarget.tagName;
eventInfo.timestamp = event.timeStamp;
eventInfo.clientX = event.clientX;
eventInfo.clientY = event.clientY;
\">Click to see event details
</button>
<input s-keydown="
eventInfo.type = event.type;
eventInfo.key = event.key;
eventInfo.ctrlKey = event.ctrlKey;
eventInfo.shiftKey = event.shiftKey;
" placeholder="Type something">
<div class="event-info">
<h3>Event Information:</h3>
<table>
<tr>
<td><strong>Type:</strong></td>
<td>{eventInfo.type || 'None'}</td>
</tr>
<tr>
<td><strong>Target:</strong></td>
<td>{eventInfo.target || 'None'}</td>
</tr>
<tr>
<td><strong>Current Target:</strong></td>
<td>{eventInfo.currentTarget || 'None'}</td>
</tr>
<tr>
<td><strong>Timestamp:</strong></td>
<td>{eventInfo.timestamp || 'None'}</td>
</tr>
<tr>
<td><strong>Mouse X/Y:</strong></td>
<td>{eventInfo.clientX}, {eventInfo.clientY}</td>
</tr>
<tr>
<td><strong>Key:</strong></td>
<td>{eventInfo.key || 'None'}</td>
</tr>
<tr>
<td><strong>Ctrl Key:</strong></td>
<td>{eventInfo.ctrlKey ? 'Yes' : 'No'}</td>
</tr>
<tr>
<td><strong>Shift Key:</strong></td>
<td>{eventInfo.shiftKey ? 'Yes' : 'No'}</td>
</tr>
</table>
</div>
</div>
</div>
<style>
.event-demo {
max-width: 500px;
margin: 20px 0;
}
.event-demo button,
.event-demo input {
margin: 10px 0;
padding: 10px;
width: 100%;
}
.event-info {
margin-top: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.event-info table {
width: 100%;
border-collapse: collapse;
}
.event-info td {
padding: 8px;
border-bottom: 1px solid #ddd;
}
.event-info td:first-child {
width: 120px;
font-weight: 600;
}
</style>
Common Event Methods
<div s-app s-state=\"{ logs: \[\] }\">
<h2>Event Methods</h2>
<div class=\"methods-demo\">
*<!-- preventDefault() -->*
<a href=\"https://example.com\"
s-click.prevent=\"
logs.push(\'Default prevented at \' + new Date().toLocaleTimeString());
\">Click me (navigation prevented)
</a>
<!-- stopPropagation() -->
<div class="parent"
s-click="logs.push('Parent clicked')"
style="padding: 20px; background: #f0f0f0; margin: 10px 0;">
<button s-click.stop="logs.push('Button clicked - propagation stopped')">
Click me (stops propagation)
</button>
</div>
<!-- Multiple methods -->
<a href="https://example.com"
s-click.prevent.stop="
logs.push('Both preventDefault and stopPropagation');
">
Click me (both methods)
</a>
<!-- Clear logs -->
<button s-click="logs = []">Clear Logs</button>
<!-- Display logs -->
<div class="logs">
<h3>Event Log:</h3>
<ul>
<li s-for="log, i in logs" s-key="i">{log}</li>
</ul>
<p s-if="logs.length === 0">No events logged yet</p>
</div>
</div>
</div>
<style>
.methods-demo {
max-width: 400px;
}
.parent {
border: 2px solid #007bff;
border-radius: 8px;
}
.logs {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
max-height: 200px;
overflow-y: auto;
}
.logs ul {
margin: 0;
padding-left: 20px;
}
.logs li {
margin: 5px 0;
color: #666;
}
</style>
6.7 Event Modifiers and Chaining
SimpliJS provides a rich set of event modifiers that can be chained together for precise control.
Complete Modifier Reference
<div s-app s-state=\"{
clicks: {
once: 0,
prevent: 0,
stop: 0,
self: 0,
capture: 0
}
}\">
<h2>Event Modifiers Reference</h2>
<div class=\"modifiers-grid\">
*<!-- .once - triggers only once -->*
<div class=\"modifier-card\">
<h3>.once</h3>
<button s-click.once=\"clicks.once++\">
Click once only: {clicks.once}
</button>
<p>Only the first click works</p>
</div>
*<!-- .prevent - prevents default -->*
<div class=\"modifier-card\">
<h3>.prevent</h3>
<a href=\"#\" s-click.prevent=\"clicks.prevent++\">
Prevented link: {clicks.prevent}
</a>
<p>No page jump/refresh</p>
</div>
*<!-- .stop - stops propagation -->*
<div class=\"modifier-card\">
<h3>.stop</h3>
<div s-click=\"alert(\'Parent clicked!\')\">
<button s-click.stop=\"clicks.stop++\">
Stop propagation: {clicks.stop}
</button>
</div>
<p>Parent alert won\'t show</p>
</div>
*<!-- .self - only triggers on self -->*
<div class=\"modifier-card\">
<h3>.self</h3>
<div s-click.self=\"clicks.self++\" class=\"self-box\">Click the box (not the button)
<button>Button inside</button>
</div>
<p>Count: {clicks.self}</p>
</div>
<!-- .capture - use capture phase -->
<div class="modifier-card">
<h3>.capture</h3>
<div s-click.capture="console.log('Capture phase')">
<button s-click="clicks.capture++">
Check console: {clicks.capture}
</button>
</div>
<p>See console for capture order</p>
</div>
</div>
<!-- Chaining modifiers -->
<div class="chaining-demo">
<h3>Chaining Modifiers</h3>
<a href="#"
s-click.prevent.stop.once="
console.log('This combines multiple modifiers');
clicks.once++;
">
.prevent.stop.once combined
</a>
</div>
</div>
<style>
.modifiers-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin: 20px 0;
}
.modifier-card {
padding: 15px;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.modifier-card h3 {
margin-top: 0;
color: #007bff;
}
.modifier-card button,
.modifier-card a {
display: inline-block;
margin: 10px 0;
padding: 8px 12px;
background: #007bff;
color: white;
text-decoration: none;
border: none;
border-radius: 4px;
cursor: pointer;
}
.self-box {
padding: 20px;
background: #e9ecef;
border: 2px dashed #007bff;
border-radius: 4px;
text-align: center;
}
.self-box button {
display: block;
margin: 10px auto 0;
}
.chaining-demo {
margin-top: 30px;
padding: 20px;
background: #d4edda;
border-radius: 8px;
}
</style>
6.8 Building Interactive Components
Now let's combine everything we've learned to build complex, interactive components.
Example: Drag and Drop
<div s-app s-state=\"{
items: \[
{ id: 1, text: \'Item 1\', x: 50, y: 50 },
{ id: 2, text: \'Item 2\', x: 200, y: 100 },
{ id: 3, text: \'Item 3\', x: 350, y: 150 }
\],
dragging: null,
offset: { x: 0, y: 0 }
}\">
<h2>Drag and Drop Demo</h2>
<div class=\"canvas\"
s-mousemove=\"if(dragging) {
dragging.x = event.clientX - offset.x;
dragging.y = event.clientY - offset.y;
}\"
s-mouseup=\"dragging = null\"
s-mouseleave=\"dragging = null\">
<div s-for=\"item in items\"
s-key=\"item.id\"
class=\"draggable\"
s-style=\"{
left: item.x + \'px\',
top: item.y + \'px\',
position: \'absolute\',
cursor: dragging === item ? \'grabbing\' : \'grab\',
zIndex: dragging === item ? 100 : 1
}\"
s-mousedown=\"
dragging = item;
offset.x = event.clientX - item.x;
offset.y = event.clientY - item.y;
\"
s-dblclick=\"items = items.filter(i => i.id !== item.id)\">
<span class=\"drag-handle\">⋮⋮</span>
{item.text}
<span class=\"delete-hint\">(double-click to delete)</span>
</div>
</div>
<div class=\"controls\">
<button s-click=\"items.push({
id: Date.now(),
text: \'Item \' + (items.length + 1),
x: Math.random() * 400,
y: Math.random() * 200
})\">
Add Item
</button>
<button s-click=\"items = \[\]\">Clear All</button>
</div>
<div class=\"instructions\">
<h3>Instructions:</h3>
<ul>
<li>Click and drag items to move them</li>
<li>Double-click an item to delete it</li>
<li>Add new items with the button</li>
</ul>
</div>
</div>
<style>
.canvas {
position: relative;
width: 600px;
height: 400px;
border: 2px solid #007bff;
border-radius: 8px;
background: #f8f9fa;
margin: 20px 0;
overflow: hidden;
user-select: none;
}
.draggable {
padding: 10px 15px;
background: white;
border: 2px solid #28a745;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
cursor: grab;
transition: box-shadow 0.2s;
display: flex;
align-items: center;
gap: 10px;
white-space: nowrap;
}
.draggable:active {
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
.drag-handle {
color: #666;
font-size: 20px;
line-height: 1;
}
.delete-hint {
font-size: 10px;
color: #999;
margin-left: 5px;
}
.controls {
margin: 20px 0;
display: flex;
gap: 10px;
}
.controls button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.controls button:hover {
background: #0056b3;
}
.instructions {
padding: 20px;
background: #e3f2fd;
border-radius: 8px;
}
.instructions ul {
margin: 10px 0 0;
padding-left: 20px;
}
</style>Example: Interactive Quiz
<div s-app s-state=\"{
quiz: {
title: \'JavaScript Fundamentals Quiz\',
questions: \[
{
id: 1,
text: \'What is the correct way to declare a variable in JavaScript?\',
options: \[
\'var myVar;\',
\'variable myVar;\',
\'v myVar;\',
\'declare myVar;\'
\],
correct: 0
},
{
id: 2,
text: \'Which of the following is NOT a JavaScript data type?\',
options: \[
\'String\',
\'Boolean\',
\'Integer\',
\'Object\'
\],
correct: 2
},
{
id: 3,
text: \'What does the \`===\` operator do?\',
options: \[
\'Assigns a value\',
\'Compares values with type coercion\',
\'Compares values without type coercion\',
\'Checks if values are equal in value and type\'
\],
correct: 3
}
\]
},
currentQuestion: 0,
answers: {},
showResults: false,
timeLeft: 30
}\">
<h2>{quiz.title}</h2>
*<!-- Timer -->*
<div s-if=\"!showResults\" class=\"timer\"
s-state=\"{
timer: (function() {
if(!showResults) {
setTimeout(() => {
if(timeLeft > 0 && !showResults) {
timeLeft--;
} else if(timeLeft === 0) {
showResults = true;
}
}, 1000);
}
})()
}\">
Time Left: {timeLeft} seconds
</div>
*<!-- Quiz content -->*
<div s-if=\"!showResults\" class=\"quiz-container\">
*<!-- Progress -->*
<div class=\"progress\">
Question {currentQuestion + 1} of {quiz.questions.length}
</div>
*<!-- Question -->*
<div class=\"question\">
<h3>{quiz.questions\[currentQuestion\].text}</h3>
<div class=\"options\">
<div s-for=\"option, index in
quiz.questions\[currentQuestion\].options\"
s-key=\"index\"
class=\"option\"
s-class=\"{ selected: answers\[currentQuestion\] === index }\"
s-click=\"answers\[currentQuestion\] = index\">
<span class=\"option-letter\">
{String.fromCharCode(65 + index)}.
</span>
{option}
<span s-if=\"answers\[currentQuestion\] === index\"
class=\"checkmark\">
✓
</span>
</div>
</div>
</div>
*<!-- Navigation -->*
<div class=\"navigation\">
<button s-click=\"currentQuestion = Math.max(0, currentQuestion - 1)\"
s-disabled=\"currentQuestion === 0\">
← Previous
</button>
<button s-if=\"currentQuestion < quiz.questions.length - 1\"
s-click=\"currentQuestion++\">
Next →
</button>
<button s-if=\"currentQuestion === quiz.questions.length - 1\"
s-click=\"showResults = true\">
Submit Quiz
</button>
</div>
*<!-- Question palette -->*
<div class=\"palette\">
<div s-for=\"q, index in quiz.questions\"
s-key=\"index\"
class=\"palette-item\"
s-class=\"{
answered: answers.hasOwnProperty(index),
current: currentQuestion === index
}\"
s-click=\"currentQuestion = index\">
{index + 1}
</div>
</div>
</div>
*<!-- Results -->*
<div s-if=\"showResults\" class=\"results\">
<h3>Quiz Results</h3>
<div s-state=\"{
score: quiz.questions.reduce((total, q, index) => {
return total + (answers\[index\] === q.correct ? 1 : 0);
}, 0)
}\">
<div class=\"score\">
<span class=\"score-number\">{score}</span>
<span class=\"score-total\">/{quiz.questions.length}</span>
</div>
<div class=\"percentage\">
{Math.round(score / quiz.questions.length * 100)}%
</div>
</div>
<div class=\"review\">
<h4>Review Answers:</h4>
<div s-for=\"q, index in quiz.questions\" s-key=\"index\"
class=\"review-item\">
<div class=\"review-question\">
<strong>Q{index + 1}:</strong> {q.text}
</div>
<div class=\"review-answer\"
s-class=\"{
correct: answers\[index\] === q.correct,
incorrect: answers\[index\] !== q.correct
}\">
Your answer: {answers\[index\] !== undefined ?
q.options\[answers\[index\]\] :
\'Not answered\'}
<span s-if=\"answers\[index\] !== q.correct\"
class=\"correct-answer\">
(Correct: {q.options\[q.correct\]})
</span>
</div>
</div>
</div>
<button s-click=\"
currentQuestion = 0;
answers = {};
showResults = false;
timeLeft = 30;
\">Retake Quiz</button>
</div>
</div>
<style>
.timer {
padding: 15px;
background: #fff3cd;
border: 1px solid #ffeeba;
border-radius: 8px;
margin: 20px 0;
font-weight: bold;
color: #856404;
}
.quiz-container {
max-width: 600px;
margin: 20px 0;
}
.progress {
padding: 10px;
background: #007bff;
color: white;
border-radius: 8px 8px 0 0;
text-align: center;
}
.question {
padding: 20px;
background: white;
border: 1px solid #ddd;
border-top: none;
}
.options {
margin-top: 20px;
}
.option {
padding: 12px;
margin: 8px 0;
background: #f8f9fa;
border: 2px solid transparent;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
}
.option:hover {
background: #e9ecef;
transform: translateX(5px);
}
.option.selected {
background: #cce5ff;
border-color: #007bff;
}
.option-letter {
display: inline-block;
width: 30px;
height: 30px;
background: #007bff;
color: white;
border-radius: 50%;
text-align: center;
line-height: 30px;
margin-right: 10px;
font-weight: bold;
}
.checkmark {
margin-left: auto;
color: #28a745;
font-weight: bold;
font-size: 20px;
}
.navigation {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.navigation button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.navigation button:disabled {
background: #ccc;
cursor: not-allowed;
}
.palette {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.palette-item {
width: 35px;
height: 35px;
display: flex;
align-items: center;
justify-content: center;
background: white;
border: 2px solid #dee2e6;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
.palette-item.answered {
background: #d4edda;
border-color: #28a745;
}
.palette-item.current {
border-color: #007bff;
background: #cce5ff;
}
.results {
max-width: 600px;
margin: 20px 0;
padding: 20px;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
}
.score {
text-align: center;
font-size: 48px;
margin: 20px 0;
}
.score-number {
color: #28a745;
font-weight: bold;
}
.score-total {
color: #666;
}
.percentage {
text-align: center;
font-size: 24px;
color: #007bff;
margin-bottom: 30px;
}
.review-item {
margin: 15px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.review-question {
margin-bottom: 10px;
}
.review-answer {
padding: 10px;
border-radius: 4px;
}
.review-answer.correct {
background: #d4edda;
border-left: 4px solid #28a745;
}
.review-answer.incorrect {
background: #f8d7da;
border-left: 4px solid #dc3545;
}
.correct-answer {
display: block;
margin-top: 5px;
font-size: 14px;
color: #155724;
}
</style>Chapter 6 Summary
You've now mastered user interaction and events in SimpliJS:
Event directives for all common DOM events
Click handling with s-click and modifiers
Form events including s-input, s-change, and s-submit
Keyboard events with key-specific modifiers
Mouse events for hover, move, and complex interactions
The event object and its properties/methods
Event modifiers for precise control
Building complex interactive components like drag-and-drop and quizzes
You've seen how SimpliJS makes event handling intuitive and declarative. Every interaction is expressed directly in your HTML, making your code more readable and maintainable.
In the next chapter, we'll explore two-way data binding and form handling in even greater depth, building on the foundation we've established here.
End of Chapter 6
Chapter 7: Two-Way Data Binding and Forms
Welcome to Chapter 7, where we dive deep into one of SimpliJS's most powerful features: two-way data binding and comprehensive form handling. Forms are the primary way users interact with your applications, and SimpliJS makes working with them intuitive and efficient.
7.1 Understanding Two-Way Data Binding
Two-way data binding creates a automatic synchronization between your state and the UI. When the state changes, the UI updates. When the user interacts with the UI, the state updates. It's a seamless circle of reactivity.
The Concept Explained
<div s-app s-state=\"{ message: \'Hello, World!\' }\">
<h2>Two-Way Data Binding Concept</h2>
*<!-- One-way binding: State → UI -->*
<p>Message from state: {message}</p>
*<!-- Two-way binding: State ↔ UI -->*
<input s-bind=\"message\" placeholder=\"Type something\">
*<!-- The UI updates automatically when state changes -->*
<button s-click=\"message = message.toUpperCase()\">
Make Uppercase
</button>
<button s-click=\"message = message.toLowerCase()\">
Make Lowercase
</button>
<p class=\"note\">Notice: Typing in the input updates the paragraph above,
and clicking buttons updates both the input and paragraph!
</p>
</div>
<style>
.note {
margin-top: 20px;
padding: 10px;
background: #e3f2fd;
border-radius: 4px;
font-style: italic;
}
</style>
How Two-Way Binding Works
Under the hood, SimpliJS sets up a reactive relationship:
When the user types: The input event updates the state
When state changes: The input's value attribute updates
All dependencies update: Anywhere {message} appears updates automatically
This creates a clean, predictable flow of data.
7.2 The s-bind Directive: Simple Two-Way Binding
The s-bind directive is your go-to for most two-way binding scenarios. It works with text inputs, textareas, and other form elements.
Basic s-bind Usage
<div s-app s-state=\"{
username: \'\',
bio: \'\',
search: \'\',
count: 0
}\">
<h2>s-bind Examples</h2>
<div class=\"example\">
<h3>Text Input</h3>
<label>
Username:
<input type=\"text\" s-bind=\"username\" placeholder=\"Enter
username\">
</label>
<p>Preview: <strong>{username \|\| \'Not entered\'}</strong></p>
</div>
<div class=\"example\">
<h3>Textarea</h3>
<label>
Bio:
<textarea s-bind=\"bio\" placeholder=\"Tell us about yourself\"
rows=\"4\"></textarea>
</label>
<div class=\"preview\">
<p>Bio preview:</p>
<p class=\"bio-preview\">{bio \|\| \'No bio yet\'}</p>
</div>
</div>
<div class=\"example\">
<h3>Number Input</h3>
<label>
Count:
<input type=\"number\" s-bind=\"count\" min=\"0\" max=\"10\">
</label>
<p>Count × 2 = {count * 2}</p>
<p>Count squared = {count * count}</p>
</div>
<div class=\"example\">
<h3>Real-time Search</h3>
<label>
Search:
<input type=\"text\" s-bind=\"search\" placeholder=\"Type to
search\...\">
</label>
<div s-if=\"search\" class=\"search-results\">
<p>Searching for: \"{search}\"</p>
<p>Results found: {Math.floor(Math.random() * 10) + 1}</p>
</div>
</div>
</div>
<style>
.example {
margin: 20px 0;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background: white;
}
.example h3 {
margin-top: 0;
color: #007bff;
}
input\[type=\"text\"\],
input\[type=\"number\"\],
textarea {
width: 100%;
padding: 8px;
margin: 5px 0;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
input:focus,
textarea:focus {
outline: none;
border-color: #007bff;
}
.preview {
margin-top: 10px;
padding: 10px;
background: #f8f9fa;
border-radius: 4px;
}
.bio-preview {
white-space: pre-wrap;
font-family: inherit;
}
.search-results {
margin-top: 10px;
padding: 10px;
background: #e3f2fd;
border-radius: 4px;
}
</style>s-bind with Different Input Types
<div s-app s-state=\"{
textValue: \'\',
numberValue: 0,
rangeValue: 50,
colorValue: \'#007bff\',
dateValue: \'\',
timeValue: \'\',
urlValue: \'\',
emailValue: \'\'
}\">
<h2>s-bind with Different Input Types</h2>
<div class=\"input-grid\">
<div class=\"input-group\">
<label>Text:</label>
<input type=\"text\" s-bind=\"textValue\">
<span class=\"value\">{textValue}</span>
</div>
<div class=\"input-group\">
<label>Number:</label>
<input type=\"number\" s-bind=\"numberValue\" min=\"-10\" max=\"10\">
<span class=\"value\">{numberValue}</span>
</div>
<div class=\"input-group\">
<label>Range (slider):</label>
<input type=\"range\" s-bind=\"rangeValue\" min=\"0\" max=\"100\">
<span class=\"value\">{rangeValue}</span>
</div>
<div class=\"input-group\">
<label>Color:</label>
<input type=\"color\" s-bind=\"colorValue\">
<span class=\"value\" style=\"background: {colorValue}; padding: 2px
8px;\">
{colorValue}
</span>
</div>
<div class=\"input-group\">
<label>Date:</label>
<input type=\"date\" s-bind=\"dateValue\">
<span class=\"value\">{dateValue \|\| \'Not set\'}</span>
</div>
<div class=\"input-group\">
<label>Time:</label>
<input type=\"time\" s-bind=\"timeValue\">
<span class=\"value\">{timeValue \|\| \'Not set\'}</span>
</div>
<div class=\"input-group\">
<label>URL:</label>
<input type=\"url\" s-bind=\"urlValue\"
placeholder=\"https://example.com\">
<span class=\"value\">{urlValue}</span>
</div>
<div class=\"input-group\">
<label>Email:</label>
<input type=\"email\" s-bind=\"emailValue\"
placeholder=\"user@example.com\">
<span class=\"value\">{emailValue}</span>
</div>
</div>
</div>
<style>
.input-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
}
.input-group {
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.input-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #333;
}
.input-group input {
width: 100%;
padding: 8px;
border: 2px solid #ddd;
border-radius: 4px;
margin-bottom: 5px;
}
.value {
display: inline-block;
padding: 4px 8px;
background: #e9ecef;
border-radius: 4px;
font-family: monospace;
font-size: 14px;
}
</style>7.3 The s-model Directive: Advanced Form Binding
While s-bind works great for simple inputs, s-model provides additional features for complex form elements like checkboxes, radio buttons, and selects.
Checkboxes with s-model
<div s-app s-state=\"{
newsletter: false,
terms: false,
preferences: {
email: true,
sms: false,
push: true
},
interests: \[\]
}\">
<h2>Checkboxes with s-model</h2>
<div class=\"checkbox-demo\">
*<!-- Single checkbox -->*
<div class=\"checkbox-group\">
<label class=\"checkbox-label\">
<input type=\"checkbox\" s-model=\"newsletter\">Subscribe to newsletter
</label>
<p>Newsletter value: {newsletter ? '✅ Subscribed' : '❌ Not subscribed'}</p>
</div>
<!-- Required checkbox -->
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" s-model="terms">
I accept the terms and conditions
</label>
<p s-if="!terms" class="warning">You must accept terms to continue</p>
</div>
<!-- Multiple checkboxes bound to object -->
<div class="checkbox-group">
<h3>Notification Preferences:</h3>
<label class="checkbox-label">
<input type="checkbox" s-model="preferences.email">
Email notifications
</label>
<label class="checkbox-label">
<input type="checkbox" s-model="preferences.sms">
SMS notifications
</label>
<label class="checkbox-label">
<input type="checkbox" s-model="preferences.push">
Push notifications
</label>
<pre>Preferences: {JSON.stringify(preferences, null, 2)}</pre>
</div>
<!-- Multiple checkboxes bound to array -->
<div class="checkbox-group">
<h3>Interests (select all that apply):</h3>
<label class="checkbox-label">
<input type="checkbox"
s-model="interests"
value="technology"
s-checked="interests.includes('technology')">
Technology
</label>
<label class="checkbox-label">
<input type="checkbox"
s-model="interests"
value="sports"
s-checked="interests.includes('sports')">
Sports
</label>
<label class="checkbox-label">
<input type="checkbox"
s-model="interests"
value="music"
s-checked="interests.includes('music')">
Music
</label>
<label class="checkbox-label">
<input type="checkbox"
s-model="interests"
value="art"
s-checked="interests.includes('art')">
Art
</label>
<p>Selected interests: {interests.join(', ') || 'None'}</p>
</div>
</div>
</div>
<style>
.checkbox-demo {
max-width: 500px;
margin: 20px 0;
}
.checkbox-group {
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 10px;
margin: 10px 0;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.warning {
color: #dc3545;
font-size: 14px;
margin-top: 5px;
}
pre {
background: white;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
}
</style>
Radio Buttons with s-model
<div s-app s-state=\"{
gender: \'\',
title: \'\',
payment: \'credit\',
experience: \'beginner\',
rating: 3
}\">
<h2>Radio Buttons with s-model</h2>
<div class=\"radio-demo\">
*<!-- Basic radio group -->*
<div class=\"radio-group\">
<h3>Gender:</h3>
<label class=\"radio-label\">
<input type=\"radio\" name=\"gender\" value=\"male\"
s-model=\"gender\">
Male
</label>
<label class=\"radio-label\">
<input type=\"radio\" name=\"gender\" value=\"female\"
s-model=\"gender\">
Female
</label>
<label class=\"radio-label\">
<input type=\"radio\" name=\"gender\" value=\"other\"
s-model=\"gender\">
Other
</label>
<p>Selected: <strong>{gender \|\| \'None\'}</strong></p>
</div>
*<!-- Radio group with default selection -->*
<div class=\"radio-group\">
<h3>Payment Method:</h3>
<label class=\"radio-label\">
<input type=\"radio\" name=\"payment\" value=\"credit\"
s-model=\"payment\">
Credit Card
</label>
<label class=\"radio-label\">
<input type=\"radio\" name=\"payment\" value=\"debit\"
s-model=\"payment\">
Debit Card
</label>
<label class=\"radio-label\">
<input type=\"radio\" name=\"payment\" value=\"paypal\"
s-model=\"payment\">
PayPal
</label>
<p>You\'ll pay with: <strong>{payment}</strong></p>
</div>
*<!-- Radio group with dynamic options -->*
<div class=\"radio-group\">
<h3>Experience Level:</h3>
<div s-for=\"level in \[\'beginner\', \'intermediate\', \'advanced\',
\'expert\'\]\"
s-key=\"level\">
<label class=\"radio-label\">
<input type=\"radio\"
name=\"experience\"
value=\"{level}\"
s-model=\"experience\">
{level.charAt(0).toUpperCase() + level.slice(1)}
</label>
</div>
</div>
*<!-- Star rating with radio buttons -->*
<div class=\"radio-group\">
<h3>Rate your experience:</h3>
<div class=\"star-rating\">
<label s-for=\"star in \[1,2,3,4,5\]\" s-key=\"star\"
class=\"star-label\">
<input type=\"radio\"
name=\"rating\"
value=\"{star}\"
s-model=\"rating\">
<span class=\"star\" s-class=\"{ active: star <= rating
}\">★</span>
</label>
</div>
<p>Rating: {rating} out of 5</p>
</div>
</div>
</div>
<style>
.radio-demo {
max-width: 500px;
margin: 20px 0;
}
.radio-group {
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.radio-label {
display: flex;
align-items: center;
gap: 10px;
margin: 8px 0;
cursor: pointer;
}
.radio-label input\[type=\"radio\"\] {
width: 18px;
height: 18px;
cursor: pointer;
}
.star-rating {
display: flex;
gap: 5px;
margin: 10px 0;
}
.star-label {
cursor: pointer;
}
.star-label input\[type=\"radio\"\] {
display: none;
}
.star {
font-size: 30px;
color: #ddd;
transition: color 0.2s;
}
.star.active {
color: #ffc107;
}
.star-label:hover .star {
color: #ffdb7e;
}
</style>Select Dropdowns with s-model
<div s-app s-state=\"{
country: \'\',
city: \'\',
skills: \[\],
experience: \'\',
multiSelect: \[\]
}\">
<h2>Select Dropdowns with s-model</h2>
<div class=\"select-demo\">
*<!-- Basic select -->*
<div class=\"select-group\">
<label>Country:</label>
<select s-model=\"country\">
<option value=\"\">Select a country</option>
<option value=\"us\">United States</option>
<option value=\"uk\">United Kingdom</option>
<option value=\"ca\">Canada</option>
<option value=\"au\">Australia</option>
</select>
<p>Selected country: <strong>{country \|\|
\'None\'}</strong></p>
</div>
*<!-- Dynamic options from array -->*
<div class=\"select-group\">
<label>City:</label>
<select s-model=\"city\">
<option value=\"\">Select a city</option>
<option s-for=\"c in \[\'New York\', \'London\', \'Toronto\',
\'Sydney\'\]\"
value=\"{c.toLowerCase().replace(\' \', \'\')}\">
{c}
</option>
</select>
<p>Selected city: <strong>{city \|\| \'None\'}</strong></p>
</div>
*<!-- Select with optgroups -->*
<div class=\"select-group\">
<label>Skills:</label>
<select s-model=\"skills\" multiple size=\"5\">
<optgroup label=\"Frontend\">
<option value=\"html\">HTML</option>
<option value=\"css\">CSS</option>
<option value=\"js\">JavaScript</option>
</optgroup>
<optgroup label=\"Backend\">
<option value=\"node\">Node.js</option>
<option value=\"python\">Python</option>
<option value=\"java\">Java</option>
</optgroup>
<optgroup label=\"Database\">
<option value=\"sql\">SQL</option>
<option value=\"mongodb\">MongoDB</option>
</optgroup>
</select>
<p>Selected skills: {skills.join(\', \') \|\| \'None\'}</p>
</div>
*<!-- Dependent selects -->*
<div class=\"select-group\">
<h3>Dependent Selects Example:</h3>
<div s-state=\"{
countries: {
us: \[\'New York\', \'Los Angeles\', \'Chicago\'\],
uk: \[\'London\', \'Manchester\', \'Birmingham\'\],
ca: \[\'Toronto\', \'Vancouver\', \'Montreal\'\]
}
}\">
<label>Country:</label>
<select s-model=\"country\">
<option value=\"\">Select country first</option>
<option value=\"us\">USA</option>
<option value=\"uk\">UK</option>
<option value=\"ca\">Canada</option>
</select>
<label s-if=\"country\">City:</label>
<select s-if=\"country\" s-model=\"city\">
<option value=\"\">Select a city</option>
<option s-for=\"c in countries\[country\]\"
value=\"{c.toLowerCase().replace(\' \', \'\')}\">
{c}
</option>
</select>
<p s-if=\"country && city\">
You selected: {city} in {country}
</p>
</div>
</div>
</div>
</div>
<style>
.select-demo {
max-width: 500px;
margin: 20px 0;
}
.select-group {
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.select-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.select-group select {
width: 100%;
padding: 8px;
border: 2px solid #ddd;
border-radius: 4px;
margin-bottom: 10px;
font-size: 16px;
}
.select-group select\[multiple\] {
min-height: 120px;
}
.select-group select:focus {
outline: none;
border-color: #007bff;
}
</style>7.4 Form Validation with s-validate and s-error
SimpliJS includes built-in validation directives that make form validation declarative and easy.
Basic Validation
<div s-app s-state=\"{
form: {
username: \'\',
email: \'\',
age: \'\',
password: \'\',
confirmPassword: \'\'
},
submitted: false
}\">
<h2>Form Validation with s-validate</h2>
<form class=\"validation-form\" s-submit=\"submitted = true\">
*<!-- Required field -->*
<div class=\"form-group\">
<label>
Username (required):
<input type=\"text\"
s-bind=\"form.username\"
s-validate=\"required\"
placeholder=\"Enter username\">
</label>
<span class=\"error\" s-error=\"form.username\"></span>
</div>
*<!-- Email validation -->*
<div class=\"form-group\">
<label>Email (required, valid format):
<input type="email"
s-bind="form.email"
s-validate="required|email"
placeholder="Enter email">
</label>
<span class="error" s-error="form.email"></span>
</div>
<!-- Min/max validation -->
<div class="form-group">
<label>
Age (18-120):
<input type="number"
s-bind="form.age"
s-validate="min:18|max:120"
placeholder="Enter age">
</label>
<span class="error" s-error="form.age"></span>
</div>
<!-- Password with min length -->
<div class="form-group">
<label>
Password (min 8 characters):
<input type="password"
s-bind="form.password"
s-validate="required|min:8"
placeholder="Enter password">
</label>
<span class="error" s-error="form.password"></span>
</div>
<!-- Custom validation can be added with expressions -->
<div class="form-group">
<label>
Confirm Password:
<input type="password"
s-bind="form.confirmPassword"
s-validate="required"
s-blur="if(form.confirmPassword && form.confirmPassword !== form.password) {
alert('Passwords must match');
}">
</label>
</div>
<button type="submit"
s-disabled="!form.username || !form.email || !form.age ||
form.age < 18 || form.age > 120 ||
!form.password || form.password.length < 8">
Register
</button>
</form>
<div s-if="submitted" class="success">
✅ Form submitted successfully!
<pre>{JSON.stringify(form, null, 2)}</pre>
</div>
</div>
<style>
.validation-form {
max-width: 400px;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.form-group input {
width: 100%;
padding: 8px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.form-group input:focus {
outline: none;
border-color: #007bff;
}
.error {
color: #dc3545;
font-size: 14px;
margin-top: 5px;
display: block;
}
button[type="submit"] {
width: 100%;
padding: 10px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
button[type="submit"]:disabled {
background: #ccc;
cursor: not-allowed;
}
.success {
margin-top: 20px;
padding: 20px;
background: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 8px;
color: #155724;
}
</style>
Advanced Validation Rules
<div s-app s-state=\"{
registration: {
username: \'\',
email: \'\',
phone: \'\',
website: \'\',
age: \'\',
zipcode: \'\',
password: \'\',
confirmPassword: \'\'
},
errors: {}
}\">
<h2>Advanced Validation Patterns</h2>
<form class=\"advanced-form\">
*<!-- Username with pattern validation -->*
<div class=\"form-row\">
<label>Username (letters, numbers, underscore only):
<input type="text"
s-bind="registration.username"
s-input="
errors.username = /^[a-zA-Z0-9_]+$/.test(registration.username)
'Username can only contain letters, numbers, and underscores';
"
placeholder="john_doe123">
</label>
<span class="error" s-if="errors.username">{errors.username}</span>
</div>
<!-- Email with custom validation -->
<div class="form-row">
<label>
Email:
<input type="email"
s-bind="registration.email"
s-blur="
if(registration.email) {
if(!registration.email.includes('@')) {
errors.email = 'Email must contain @';
} else if(!registration.email.includes('.')) {
errors.email = 'Email must contain a domain';
} else {
errors.email = '';
}
} else {
errors.email = 'Email is required';
}
"
placeholder="user@example.com">
</label>
<span class="error" s-if="errors.email">{errors.email}</span>
</div>
<!-- Phone number formatting -->
<div class="form-row">
<label>
Phone (format: XXX-XXX-XXXX):
<input type="tel"
s-bind="registration.phone"
s-input="
// Auto-format phone number
let cleaned = registration.phone.replace(/\D/g, '');
if(cleaned.length > 3 && cleaned.length <= 6) {
registration.phone = cleaned.slice(0,3) + '-' + cleaned.slice(3);
} else if(cleaned.length > 6) {
registration.phone = cleaned.slice(0,3) + '-' +
cleaned.slice(3,6) + '-' +
cleaned.slice(6,10);
}
// Validate
if(cleaned.length > 0 && cleaned.length !== 10) {
errors.phone = 'Phone must have 10 digits';
} else {
errors.phone = '';
}
"
placeholder="123-456-7890">
</label>
<span class="error" s-if="errors.phone">{errors.phone}</span>
</div>
<!-- URL validation -->
<div class="form-row">
<label>
Website:
<input type="url"
s-bind="registration.website"
s-blur="
if(registration.website) {
try {
new URL(registration.website);
errors.website = '';
} catch {
errors.website = 'Please enter a valid URL';
}
}
"
placeholder="https://example.com">
</label>
<span class="error" s-if="errors.website">{errors.website}</span>
</div>
<!-- Age with range validation -->
<div class="form-row">
<label>
Age (13-120):
<input type="number"
s-bind="registration.age"
s-input="
let age = parseInt(registration.age);
if(isNaN(age)) {
errors.age = 'Please enter a valid age';
} else if(age < 13) {
errors.age = 'You must be at least 13 years old';
} else if(age > 120) {
errors.age = 'Please enter a valid age';
} else {
errors.age = '';
}
"
min="13" max="120">
</label>
<span class="error" s-if="errors.age">{errors.age}</span>
</div>
<!-- ZIP code validation -->
<div class="form-row">
<label>
ZIP Code (5 digits):
<input type="text"
s-bind="registration.zipcode"
s-input="
let zip = registration.zipcode.replace(/\D/g, '');
if(zip.length > 0) {
if(zip.length !== 5) {
errors.zipcode = 'ZIP code must be 5 digits';
registration.zipcode = zip;
} else {
errors.zipcode = '';
registration.zipcode = zip;
}
}
"
maxlength="5"
placeholder="12345">
</label>
<span class="error" s-if="errors.zipcode">{errors.zipcode}</span>
</div>
<!-- Password strength meter -->
<div class="form-row">
<label>
Password:
<input type="password"
s-bind="registration.password"
s-input="
errors.password = '';
if(registration.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
} else if(!/[A-Z]/.test(registration.password)) {
errors.password = 'Password must contain at least one uppercase letter';
} else if(!/[a-z]/.test(registration.password)) {
errors.password = 'Password must contain at least one lowercase letter';
} else if(!/[0-9]/.test(registration.password)) {
errors.password = 'Password must contain at least one number';
}
"
placeholder="Enter password">
</label>
<!-- Password strength indicator -->
<div class="strength-meter" s-if="registration.password">
<div class="strength-bar"
s-class="{
weak: registration.password.length < 6,
medium: registration.password.length >= 6 && registration.password.length < 10,
strong: registration.password.length >= 10
}">
</div>
<span class="strength-text">
{registration.password.length < 6 ? 'Weak' :
registration.password.length < 10 ? 'Medium' : 'Strong'}
</span>
</div>
<span class="error" s-if="errors.password">{errors.password}</span>
</div>
<!-- Confirm password -->
<div class="form-row">
<label>
Confirm Password:
<input type="password"
s-bind="registration.confirmPassword"
s-input="
if(registration.confirmPassword !== registration.password) {
errors.confirmPassword = 'Passwords do not match';
} else {
errors.confirmPassword = '';
}
"
placeholder="Confirm password">
</label>
<span class="error" s-if="errors.confirmPassword">{errors.confirmPassword}</span>
</div>
<!-- Real-time validation summary -->
<div class="validation-summary">
<h4>Validation Summary:</h4>
<ul>
<li s-for="field, error in errors"
s-if="error"
s-key="field"
class="error-item">
❌ {field}: {error}
</li>
<li s-if="Object.values(errors).every(e => !e) &&
Object.values(registration).every(v => v)">
✅ All fields are valid!
</li>
</ul>
</div>
</form>
</div>
<style>
.advanced-form {
max-width: 500px;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.form-row {
margin-bottom: 20px;
}
.form-row label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.form-row input {
width: 100%;
padding: 8px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.form-row input:focus {
outline: none;
border-color: #007bff;
}
.error {
color: #dc3545;
font-size: 14px;
margin-top: 5px;
display: block;
}
.strength-meter {
margin-top: 5px;
height: 20px;
background: #f0f0f0;
border-radius: 10px;
overflow: hidden;
position: relative;
}
.strength-bar {
height: 100%;
width: 0;
transition: width 0.3s, background-color 0.3s;
}
.strength-bar.weak {
width: 33.33%;
background-color: #dc3545;
}
.strength-bar.medium {
width: 66.66%;
background-color: #ffc107;
}
.strength-bar.strong {
width: 100%;
background-color: #28a745;
}
.strength-text {
position: absolute;
top: 0;
left: 10px;
line-height: 20px;
font-size: 12px;
color: white;
font-weight: bold;
text-shadow: 0 0 2px rgba(0,0,0,0.5);
}
.validation-summary {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.validation-summary ul {
margin: 10px 0 0;
padding-left: 20px;
}
.error-item {
color: #dc3545;
margin: 5px 0;
}
</style>
7.5 Building Complex Forms
Now let's combine everything to build complex, real-world forms.
Multi-Step Registration Form
<div s-app s-state=\"{
currentStep: 1,
registration: {
// Step 1: Account Info
username: \'\',
email: \'\',
password: \'\',
confirmPassword: \'\',
// Step 2: Personal Info
firstName: \'\',
lastName: \'\',
dateOfBirth: \'\',
gender: \'\',
// Step 3: Address
street: \'\',
city: \'\',
state: \'\',
zipCode: \'\',
country: \'us\',
// Step 4: Preferences
newsletter: false,
interests: \[\],
theme: \'light\',
language: \'en\'
},
errors: {},
submitted: false
}\">
<h2>Multi-Step Registration Form</h2>
*<!-- Progress bar -->*
<div class=\"progress-container\">
<div class=\"progress-steps\">
<div class=\"step\"
s-class=\"{ active: currentStep >= 1, completed: currentStep > 1 }\">
<span class=\"step-number\">1</span>
<span class=\"step-label\">Account</span>
</div>
<div class=\"step\"
s-class=\"{ active: currentStep >= 2, completed: currentStep > 2 }\">
<span class=\"step-number\">2</span>
<span class=\"step-label\">Personal</span>
</div>
<div class=\"step\"
s-class=\"{ active: currentStep >= 3, completed: currentStep > 3 }\">
<span class=\"step-number\">3</span>
<span class=\"step-label\">Address</span>
</div>
<div class=\"step\"
s-class=\"{ active: currentStep >= 4, completed: currentStep > 4 }\">
<span class=\"step-number\">4</span>
<span class=\"step-label\">Preferences</span>
</div>
</div>
<div class=\"progress-bar\">
<div class=\"progress-fill\"
s-style=\"{ width: ((currentStep - 1) / 3 * 100) + \'%\' }\">
</div>
</div>
</div>
*<!-- Step 1: Account Information -->*
<div s-if=\"currentStep === 1\" class=\"step-form\">
<h3>Step 1: Account Information</h3>
<div class=\"form-group\">
<label>Username *</label>
<input type=\"text\"
s-bind=\"registration.username\"
s-validate=\"required\"
placeholder=\"Choose a username\">
<span class=\"error\" s-error=\"registration.username\"></span>
</div>
<div class=\"form-group\">
<label>Email *</label>
<input type=\"email\"
s-bind=\"registration.email\"
s-validate=\"required\|email\"
placeholder=\"your@email.com\">
<span class=\"error\" s-error=\"registration.email\"></span>
</div>
<div class=\"form-group\">
<label>Password *</label>
<input type=\"password\"
s-bind=\"registration.password\"
s-validate=\"required\|min:8\"
placeholder=\"Create a password\">
<span class=\"error\" s-error=\"registration.password\"></span>
</div>
<div class=\"form-group\">
<label>Confirm Password *</label>
<input type=\"password\"
s-bind=\"registration.confirmPassword\"
s-blur=\"if(registration.confirmPassword &&
registration.confirmPassword !== registration.password) {
errors.confirmPassword = \'Passwords must match\';
} else {
delete errors.confirmPassword;
}\"
placeholder=\"Confirm your password\">
<span class=\"error\" s-if=\"errors.confirmPassword\">
{errors.confirmPassword}
</span>
</div>
</div>
*<!-- Step 2: Personal Information -->*
<div s-if=\"currentStep === 2\" class=\"step-form\">
<h3>Step 2: Personal Information</h3>
<div class=\"form-row\">
<div class=\"form-group half\">
<label>First Name *</label>
<input type=\"text\"
s-bind=\"registration.firstName\"
s-validate=\"required\"
placeholder=\"First name\">
<span class=\"error\" s-error=\"registration.firstName\"></span>
</div>
<div class=\"form-group half\">
<label>Last Name *</label>
<input type=\"text\"
s-bind=\"registration.lastName\"
s-validate=\"required\"
placeholder=\"Last name\">
<span class=\"error\" s-error=\"registration.lastName\"></span>
</div>
</div>
<div class=\"form-group\">
<label>Date of Birth *</label>
<input type=\"date\"
s-bind=\"registration.dateOfBirth\"
s-validate=\"required\"
max=\"{new Date().toISOString().split(\'T\')\[0\]}\">
<span class=\"error\" s-error=\"registration.dateOfBirth\"></span>
</div>
<div class=\"form-group\">
<label>Gender</label>
<div class=\"radio-group\">
<label>
<input type=\"radio\" name=\"gender\" value=\"male\"
s-model=\"registration.gender\">
Male
</label>
<label>
<input type=\"radio\" name=\"gender\" value=\"female\"
s-model=\"registration.gender\">
Female
</label>
<label>
<input type=\"radio\" name=\"gender\" value=\"other\"
s-model=\"registration.gender\">
Other
</label>
</div>
</div>
</div>
*<!-- Step 3: Address -->*
<div s-if=\"currentStep === 3\" class=\"step-form\">
<h3>Step 3: Address Information</h3>
<div class=\"form-group\">
<label>Street Address *</label>
<input type=\"text\"
s-bind=\"registration.street\"
s-validate=\"required\"
placeholder=\"Street address\">
<span class=\"error\" s-error=\"registration.street\"></span>
</div>
<div class=\"form-row\">
<div class=\"form-group half\">
<label>City *</label>
<input type=\"text\"
s-bind=\"registration.city\"
s-validate=\"required\"
placeholder=\"City\">
<span class=\"error\" s-error=\"registration.city\"></span>
</div>
<div class=\"form-group half\">
<label>State *</label>
<select s-model=\"registration.state\" s-validate=\"required\">
<option value=\"\">Select state</option>
<option value=\"AL\">Alabama</option>
<option value=\"AK\">Alaska</option>
<option value=\"AZ\">Arizona</option>
*<!-- Add more states -->*
</select>
<span class=\"error\" s-error=\"registration.state\"></span>
</div>
</div>
<div class=\"form-row\">
<div class=\"form-group half\">
<label>ZIP Code *</label>
<input type=\"text\"
s-bind=\"registration.zipCode\"
s-validate=\"required\"
s-input=\"registration.zipCode = registration.zipCode.replace(/\\D/g,
\'\')\"
maxlength=\"5\"
placeholder=\"12345\">
<span class=\"error\" s-error=\"registration.zipCode\"></span>
</div>
<div class=\"form-group half\">
<label>Country *</label>
<select s-model=\"registration.country\" s-validate=\"required\">
<option value=\"us\">United States</option>
<option value=\"ca\">Canada</option>
<option value=\"uk\">United Kingdom</option>
</select>
<span class=\"error\" s-error=\"registration.country\"></span>
</div>
</div>
</div>
*<!-- Step 4: Preferences -->*
<div s-if=\"currentStep === 4\" class=\"step-form\">
<h3>Step 4: Preferences</h3>
<div class=\"form-group\">
<label class=\"checkbox-label\">
<input type=\"checkbox\" s-model=\"registration.newsletter\">Subscribe to newsletter
</label>
</div>
<div class="form-group">
<label>Interests (select all that apply):</label>
<div class="checkbox-group">
<label>
<input type="checkbox"
s-model="registration.interests"
value="technology">
Technology
</label>
<label>
<input type="checkbox"
s-model="registration.interests"
value="sports">
Sports
</label>
<label>
<input type="checkbox"
s-model="registration.interests"
value="music">
Music
</label>
<label>
<input type="checkbox"
s-model="registration.interests"
value="art">
Art
</label>
<label>
<input type="checkbox"
s-model="registration.interests"
value="science">
Science
</label>
</div>
</div>
<div class="form-group">
<label>Theme preference:</label>
<select s-model="registration.theme">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="auto">Auto (system default)</option>
</select>
</div>
<div class="form-group">
<label>Language:</label>
<select s-model="registration.language">
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="de">German</option>
</select>
</div>
</div>
<!-- Navigation buttons -->
<div class="step-navigation">
<button s-if="currentStep > 1"
s-click="currentStep--"
class="btn-secondary">
← Previous
</button>
<button s-if="currentStep < 4"
s-click="currentStep++"
class="btn-primary"
s-disabled="!validateStep(currentStep)">
Next →
</button>
<button s-if="currentStep === 4"
s-click="submitted = true"
class="btn-success"
s-disabled="!validateAll()">
Complete Registration
</button>
</div>
<!-- Registration summary -->
<div s-if="submitted" class="summary">
<h3>✅ Registration Complete!</h3>
<p>Thank you for registering. Here's your information:</p>
<pre>{JSON.stringify(registration, null, 2)}</pre>
<button s-click="
currentStep = 1;
registration = {
username: '', email: '', password: '', confirmPassword: '',
firstName: '', lastName: '', dateOfBirth: '', gender: '',
street: '', city: '', state: '', zipCode: '', country: 'us',
newsletter: false, interests: [], theme: 'light', language: 'en'
};
submitted = false;
">Start Over</button>
</div>
</div>
<script>
// Helper functions for validation
function validateStep(step) {
// This would contain step validation logic
// In a real app, you'd check required fields for each step
return true;
}
function validateAll() {
// This would validate all fields
return true;
}
</script>
<style>
.progress-container {
margin: 30px 0;
}
.progress-steps {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.step-number {
width: 30px;
height: 30px;
background: #ddd;
color: #666;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-bottom: 5px;
}
.step.active .step-number {
background: #007bff;
color: white;
}
.step.completed .step-number {
background: #28a745;
color: white;
}
.step.completed .step-number::after {
content: '✓';
}
.step-label {
font-size: 12px;
color: #666;
}
.step.active .step-label {
color: #007bff;
font-weight: bold;
}
.progress-bar {
height: 4px;
background: #ddd;
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #007bff, #28a745);
transition: width 0.3s;
}
.step-form {
max-width: 600px;
margin: 30px auto;
padding: 30px;
background: white;
border-radius: 12px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}
.step-form h3 {
margin-top: 0;
color: #333;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
margin-bottom: 20px;
}
.form-row {
display: flex;
gap: 20px;
margin-bottom: 15px;
}
.form-group {
flex: 1;
margin-bottom: 15px;
}
.form-group.half {
flex: 1;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #333;
}
.form-group input,
.form-group select {
width: 100%;
padding: 10px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 16px;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #007bff;
}
.radio-group {
display: flex;
gap: 20px;
margin-top: 5px;
}
.radio-group label {
display: flex;
align-items: center;
gap: 5px;
font-weight: normal;
}
.checkbox-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
margin-top: 5px;
}
.checkbox-group label {
display: flex;
align-items: center;
gap: 5px;
font-weight: normal;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 10px;
font-weight: normal !important;
}
.step-navigation {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 30px;
}
.btn-primary,
.btn-secondary,
.btn-success {
padding: 12px 30px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #0056b3;
transform: translateY(-2px);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #545b62;
transform: translateY(-2px);
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover:not(:disabled) {
background: #218838;
transform: translateY(-2px);
}
.btn-primary:disabled,
.btn-success:disabled {
background: #ccc;
cursor: not-allowed;
}
.summary {
margin-top: 30px;
padding: 30px;
background: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 12px;
color: #155724;
}
.summary pre {
background: white;
padding: 15px;
border-radius: 6px;
overflow-x: auto;
}
</style>
Chapter 7 Summary
You've now mastered forms and two-way data binding in SimpliJS:
Two-way data binding with s-bind for automatic state-UI synchronization
Different input types and how they work with binding
Advanced form binding with s-model for checkboxes, radio buttons, and selects
Built-in validation with s-validate and s-error
Custom validation patterns for complex requirements
Real-time validation feedback and error messages
Complex multi-step forms with progress tracking
Form state management across multiple steps
You've seen how SimpliJS makes form handling intuitive and powerful. Every form element can be bound to state, validated automatically, and provide immediate feedback to users.
In the next chapter, we'll explore JavaScript-based components, moving beyond HTML-First to create reusable, encapsulated components with full programmatic control.
End of Chapter 7
Chapter 8: Styling and Attributes
Welcome to Chapter 8, where we explore how to make your SimpliJS applications visually dynamic and responsive. While you've already learned to manage data and user interactions, this chapter focuses on the presentation layer—how to dynamically style elements and manipulate attributes based on your application's state.
8.1 Dynamic Attribute Binding with s-attr
The s-attr directive allows you to dynamically set any HTML attribute based on your state. This is incredibly powerful for creating dynamic images, links, form elements, and more.
Basic Attribute Binding
<div s-app s-state=\"{
imageUrl: \'https://picsum.photos/300/200\',
altText: \'Random image\',
linkUrl: \'https://example.com\',
linkText: \'Visit Example\',
buttonDisabled: false,
inputPlaceholder: \'Type something\...\',
inputValue: \'\',
progressValue: 50,
meterValue: 75
}\">
<h2>Dynamic Attribute Binding</h2>
<div class=\"attribute-demo\">
*<!-- Image attributes -->*
<div class=\"demo-section\">
<h3>Image Attributes</h3>
<img s-attr:src=\"imageUrl\"
s-attr:alt=\"altText\"
s-attr:width=\"300\"
s-attr:height=\"200\"
style=\"border-radius: 8px; margin: 10px 0;\">
<div class=\"controls\">
<label>
Image URL:
<input type=\"text\" s-bind=\"imageUrl\" size=\"40\">
</label>
<label>
Alt Text:
<input type=\"text\" s-bind=\"altText\">
</label>
</div>
</div>
*<!-- Link attributes -->*
<div class=\"demo-section\">
<h3>Link Attributes</h3>
<a s-attr:href=\"linkUrl\"
s-attr:target=\"\'_blank\'\"
s-attr:title=\"\'Click to visit \' + linkUrl\">
{linkText}
</a>
<div class=\"controls\">
<label>
Link URL:
<input type=\"url\" s-bind=\"linkUrl\">
</label>
<label>
Link Text:
<input type=\"text\" s-bind=\"linkText\">
</label>
</div>
</div>
*<!-- Button attributes -->*
<div class=\"demo-section\">
<h3>Button Attributes</h3>
<button s-attr:disabled=\"buttonDisabled\"
s-attr:data-count=\"clicks \|\| 0\"
s-click=\"clicks = (clicks \|\| 0) + 1\">
Click me {clicks \|\| 0} times
</button>
<label>
<input type=\"checkbox\" s-model=\"buttonDisabled\">
Disable button
</label>
</div>
*<!-- Input attributes -->*
<div class=\"demo-section\">
<h3>Input Attributes</h3>
<input type=\"text\"
s-attr:placeholder=\"inputPlaceholder\"
s-attr:value=\"inputValue\"
s-attr:readonly=\"isReadonly\"
s-attr:maxlength=\"20\"
s-input=\"inputValue = event.target.value\">
<div class=\"controls\">
<label>
Placeholder:
<input type=\"text\" s-bind=\"inputPlaceholder\">
</label>
<label>
<input type=\"checkbox\" s-model=\"isReadonly\">
Readonly
</label>
</div>
</div>
*<!-- Progress bar -->*
<div class=\"demo-section\">
<h3>Progress Bar</h3>
<progress s-attr:value=\"progressValue\"
s-attr:max=\"100\"
style=\"width: 100%; height: 30px;\">
{progressValue}%
</progress>
<input type=\"range\" s-bind=\"progressValue\" min=\"0\" max=\"100\">
</div>
*<!-- Meter -->*
<div class=\"demo-section\">
<h3>Meter</h3>
<meter s-attr:value=\"meterValue\"
s-attr:min=\"0\"
s-attr:max=\"100\"
s-attr:low=\"33\"
s-attr:high=\"66\"
s-attr:optimum=\"80\"
style=\"width: 100%; height: 30px;\">
{meterValue}%
</meter>
<input type=\"range\" s-bind=\"meterValue\" min=\"0\" max=\"100\">
</div>
</div>
</div>
<style>
.attribute-demo {
max-width: 800px;
margin: 20px auto;
}
.demo-section {
margin: 30px 0;
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
border-left: 4px solid #007bff;
}
.demo-section h3 {
margin-top: 0;
color: #007bff;
}
.controls {
margin-top: 15px;
display: flex;
flex-direction: column;
gap: 10px;
}
.controls label {
display: flex;
align-items: center;
gap: 10px;
}
.controls input\[type=\"text\"\],
.controls input\[type=\"url\"\] {
flex: 1;
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
progress, meter {
margin: 10px 0;
}
</style>Dynamic Data Attributes
Data attributes are perfect for storing additional information in elements:
<div s-app s-state=\"{
products: \[
{ id: 1, name: \'Laptop\', price: 999, category: \'electronics\',
inStock: true },
{ id: 2, name: \'Mouse\', price: 49, category: \'electronics\', inStock:
false },
{ id: 3, name: \'Desk\', price: 299, category: \'furniture\', inStock:
true }
\],
selectedProduct: null
}\">
<h2>Dynamic Data Attributes</h2>
<div class=\"product-grid\">
<div s-for=\"product in products\"
s-key=\"product.id\"
class=\"product-card\"
s-attr:data-id=\"product.id\"
s-attr:data-price=\"product.price\"
s-attr:data-category=\"product.category\"
s-attr:data-instock=\"product.inStock\"
s-attr:aria-label=\"\'Product: \' + product.name\"
s-attr:role=\"\'button\'\"
s-click=\"selectedProduct = product\"
s-class=\"{ \'out-of-stock\': !product.inStock }\">
<h4>{product.name}</h4>
<p>\${product.price}</p>
<span class=\"stock-badge\"
s-class=\"{ \'in-stock\': product.inStock, \'out-of-stock\':
!product.inStock }\">
{product.inStock ? \'✓ In Stock\' : \'✗ Out of Stock\'}
</span>
</div>
</div>
*<!-- Display selected product info -->*
<div s-if=\"selectedProduct\" class=\"selected-info\">
<h3>Selected Product Info:</h3>
<p>ID: {selectedProduct.id}</p>
<p>Name: {selectedProduct.name}</p>
<p>Price: \${selectedProduct.price}</p>
<p>Category: {selectedProduct.category}</p>
<p>In Stock: {selectedProduct.inStock ? \'Yes\' : \'No\'}</p>
*<!-- Show all data attributes -->*
<h4>Data Attributes from Element:</h4>
<pre>{
data-id: \"{selectedProduct.id}\",
data-price: \"{selectedProduct.price}\",
data-category: \"{selectedProduct.category}\",
data-instock: \"{selectedProduct.inStock}\"
}</pre>
</div>
</div>
<style>
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
margin: 20px 0;
}
.product-card {
padding: 15px;
background: white;
border: 2px solid #ddd;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.product-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
border-color: #007bff;
}
.product-card.out-of-stock {
opacity: 0.6;
background: #f8f9fa;
}
.stock-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 12px;
}
.stock-badge.in-stock {
background: #d4edda;
color: #155724;
}
.stock-badge.out-of-stock {
background: #f8d7da;
color: #721c24;
}
.selected-info {
margin-top: 30px;
padding: 20px;
background: #e3f2fd;
border-radius: 8px;
}
.selected-info pre {
background: white;
padding: 10px;
border-radius: 4px;
}
</style>Boolean Attributes
Some attributes like disabled, readonly, checked, and selected are boolean—they're either present or not. SimpliJS handles these specially:
<div s-app s-state=\"{
isDisabled: false,
isReadonly: false,
isChecked: true,
isSelected: \'option2\',
isRequired: true,
isMuted: false,
controls: {
autoplay: false,
loop: true,
controls: true
}
}\">
<h2>Boolean Attributes</h2>
<div class=\"boolean-demo\">
*<!-- Form elements -->*
<div class=\"demo-section\">
<h3>Form Element States</h3>
<div class=\"control-row\">
<label>
<input type=\"checkbox\"
s-attr:checked=\"isChecked\"
s-attr:disabled=\"isDisabled\">
Checkbox (checked: {isChecked})
</label>
</div>
<div class=\"control-row\">
<label>
<input type=\"text\"
s-attr:disabled=\"isDisabled\"
s-attr:readonly=\"isReadonly\"
s-attr:required=\"isRequired\"
value=\"Sample text\"
placeholder=\"Type here\">
Text input
</label>
</div>
<div class=\"control-row\">
<select s-attr:disabled=\"isDisabled\" s-attr:required=\"isRequired\">
<option value=\"option1\">Option 1</option>
<option value=\"option2\" s-attr:selected=\"isSelected ===
\'option2\'\">Option 2 (selected)
</option>
<option value="option3">Option 3</option>
</select>
Select dropdown
</div>
<div class="control-row">
<textarea s-attr:disabled="isDisabled"
s-attr:readonly="isReadonly"
s-attr:required="isRequired"
placeholder="Textarea">Some text</textarea>
Textarea
</div>
</div>
<!-- Media elements -->
<div class="demo-section">
<h3>Media Element Attributes</h3>
<audio controls s-attr:autoplay="controls.autoplay"
s-attr:loop="controls.loop"
s-attr:muted="isMuted"
style="width: 100%;">
<source src="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3" type="audio/mpeg">
</audio>
<video width="320" height="240" controls
s-attr:autoplay="controls.autoplay"
s-attr:loop="controls.loop"
s-attr:muted="isMuted"
style="margin-top: 10px;">
<source src="https://www.w3schools.com/html/mov_bbb.mp4" type="video/mp4">
</video>
</div>
<!-- Control panel -->
<div class="control-panel">
<h3>Toggle Attributes</h3>
<label>
<input type="checkbox" s-model="isDisabled">
Disabled
</label>
<label>
<input type="checkbox" s-model="isReadonly">
Readonly
</label>
<label>
<input type="checkbox" s-model="isChecked">
Checked
</label>
<label>
<input type="checkbox" s-model="isRequired">
Required
</label>
<label>
<input type="checkbox" s-model="isMuted">
Muted
</label>
<h4>Media Controls:</h4>
<label>
<input type="checkbox" s-model="controls.autoplay">
Autoplay
</label>
<label>
<input type="checkbox" s-model="controls.loop">
Loop
</label>
</div>
<!-- Attribute inspector -->
<div class="attribute-inspector">
<h3>Current Boolean Attributes</h3>
<pre>{
disabled: {isDisabled},
readonly: {isReadonly},
checked: {isChecked},
required: {isRequired},
muted: {isMuted},
autoplay: {controls.autoplay},
loop: {controls.loop}
}</pre>
</div>
</div>
</div>
<style>
.boolean-demo {
max-width: 800px;
margin: 20px auto;
}
.demo-section {
margin: 30px 0;
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
}
.control-row {
margin: 15px 0;
display: flex;
align-items: center;
gap: 20px;
}
.control-row label {
display: flex;
align-items: center;
gap: 10px;
}
.control-panel {
margin: 20px 0;
padding: 20px;
background: #e3f2fd;
border-radius: 8px;
}
.control-panel label {
display: block;
margin: 10px 0;
}
.attribute-inspector {
margin-top: 20px;
padding: 15px;
background: #333;
color: #fff;
border-radius: 8px;
font-family: monospace;
}
.attribute-inspector pre {
margin: 10px 0 0;
color: #0f0;
}
audio, video {
width: 100%;
margin: 10px 0;
}
</style>
8.2 Dynamic Classes with s-class
The s-class directive provides a powerful way to dynamically add or remove CSS classes based on your state.
Object Syntax for Classes
<div s-app s-state=\"{
isActive: true,
isHighlighted: false,
isError: false,
isSuccess: false,
size: \'medium\',
theme: \'light\',
priority: \'normal\'
}\">
<h2>Dynamic Classes with Object Syntax</h2>
*<!-- Basic object syntax -->*
<div class=\"demo-box\"
s-class=\"{
active: isActive,
highlighted: isHighlighted,
error: isError,
success: isSuccess
}\">This box's classes change based on state
</div>
<!-- Multiple classes from expressions -->
<div class="demo-box"
s-class="{
'size-small': size === 'small',
'size-medium': size === 'medium',
'size-large': size === 'large',
'theme-light': theme === 'light',
'theme-dark': theme === 'dark',
'priority-high': priority === 'high',
'priority-normal': priority === 'normal',
'priority-low': priority === 'low'
}">
Size: {size}, Theme: {theme}, Priority: {priority}
</div>
<!-- Controls -->
<div class="controls">
<h3>Toggle Classes:</h3>
<label>
<input type="checkbox" s-model="isActive">
Active
</label>
<label>
<input type="checkbox" s-model="isHighlighted">
Highlighted
</label>
<label>
<input type="checkbox" s-model="isError">
Error
</label>
<label>
<input type="checkbox" s-model="isSuccess">
Success
</label>
<h3>Size:</h3>
<select s-model="size">
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
</select>
<h3>Theme:</h3>
<select s-model="theme">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<h3>Priority:</h3>
<select s-model="priority">
<option value="high">High</option>
<option value="normal">Normal</option>
<option value="low">Low</option>
</select>
</div>
</div>
<style>
.demo-box {
padding: 20px;
margin: 20px 0;
border: 2px solid #ddd;
border-radius: 8px;
transition: all 0.3s;
}
/* Base states */
.demo-box.active {
border-color: #007bff;
background: #e3f2fd;
}
.demo-box.highlighted {
box-shadow: 0 0 20px #ffc107;
}
.demo-box.error {
border-color: #dc3545;
background: #f8d7da;
}
.demo-box.success {
border-color: #28a745;
background: #d4edda;
}
/* Size variations */
.demo-box.size-small {
font-size: 12px;
padding: 10px;
}
.demo-box.size-medium {
font-size: 16px;
padding: 20px;
}
.demo-box.size-large {
font-size: 20px;
padding: 30px;
}
/* Theme variations */
.demo-box.theme-light {
background: white;
color: #333;
}
.demo-box.theme-dark {
background: #333;
color: white;
}
/* Priority variations */
.demo-box.priority-high {
border-width: 4px;
}
.demo-box.priority-low {
opacity: 0.6;
}
.controls {
margin: 20px 0;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.controls label {
display: block;
margin: 10px 0;
}
</style>
Array Syntax for Classes
You can also use arrays to combine multiple classes:
<div s-app s-state=\"{
baseClass: \'card\',
statusClass: \'active\',
sizeClass: \'large\',
customClasses: \[\'shadow\', \'rounded\', \'bordered\'\],
userRole: \'admin\',
isPremium: true
}\">
<h2>Array Syntax for Classes</h2>
*<!-- Basic array syntax -->*
<div class=\"demo-card\"
s-class=\"\[baseClass, statusClass, sizeClass\]\">
Classes from array: {baseClass}, {statusClass}, {sizeClass}
</div>
*<!-- Mixed array with conditional classes -->*
<div class=\"demo-card\"
s-class=\"\[
\'card\',
statusClass,
isPremium ? \'premium\' : \'standard\',
userRole === \'admin\' ? \'admin-border\' : \'\',
\...customClasses
\]\">
<h3>Premium User Card</h3>
<p>Role: {userRole}</p>
<p>Status: {isPremium ? \'Premium\' : \'Standard\'}</p>
</div>
*<!-- Control panel -->*
<div class=\"control-panel\">
<h3>Class Controls:</h3>
<label>
Status Class:
<select s-model=\"statusClass\">
<option value=\"active\">Active</option>
<option value=\"inactive\">Inactive</option>
<option value=\"pending\">Pending</option>
</select>
</label>
<label>
Size Class:
<select s-model=\"sizeClass\">
<option value=\"small\">Small</option>
<option value=\"medium\">Medium</option>
<option value=\"large\">Large</option>
</select>
</label>
<label>
User Role:
<select s-model=\"userRole\">
<option value=\"user\">User</option>
<option value=\"admin\">Admin</option>
<option value=\"moderator\">Moderator</option>
</select>
</label>
<label>
<input type=\"checkbox\" s-model=\"isPremium\">
Premium User
</label>
<h4>Custom Classes:</h4>
<label>
<input type=\"checkbox\" s-model=\"customClasses\" value=\"shadow\"
s-checked=\"customClasses.includes(\'shadow\')\">
Shadow
</label>
<label>
<input type=\"checkbox\" s-model=\"customClasses\" value=\"rounded\"
s-checked=\"customClasses.includes(\'rounded\')\">
Rounded
</label>
<label>
<input type=\"checkbox\" s-model=\"customClasses\" value=\"bordered\"
s-checked=\"customClasses.includes(\'bordered\')\">
Bordered
</label>
<label>
<input type=\"checkbox\" s-model=\"customClasses\" value=\"animated\"
s-checked=\"customClasses.includes(\'animated\')\">
Animated
</label>
</div>
</div>
<style>
.demo-card {
padding: 20px;
margin: 20px 0;
transition: all 0.3s;
}
/* Base card styles */
.card {
background: white;
border: 2px solid #ddd;
}
/* Status classes */
.active {
border-color: #28a745;
background: #d4edda;
}
.inactive {
border-color: #6c757d;
background: #e2e3e5;
opacity: 0.7;
}
.pending {
border-color: #ffc107;
background: #fff3cd;
}
/* Size classes */
.small {
font-size: 12px;
padding: 10px;
}
.medium {
font-size: 16px;
padding: 20px;
}
.large {
font-size: 20px;
padding: 30px;
}
/* Custom classes */
.shadow {
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
.rounded {
border-radius: 12px;
}
.bordered {
border-width: 3px;
}
.animated {
transition: all 0.3s;
}
.animated:hover {
transform: scale(1.02);
}
/* Role-based */
.admin-border {
border-color: #dc3545;
border-width: 3px;
}
.premium {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.control-panel {
margin: 20px 0;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.control-panel label {
display: block;
margin: 10px 0;
}
</style>Real-World Example: Interactive Todo with Dynamic Styling
<div s-app s-state=\"{
todos: \[
{ id: 1, text: \'Learn SimpliJS\', completed: true, priority: \'high\',
category: \'work\' },
{ id: 2, text: \'Build a project\', completed: false, priority:
\'high\', category: \'personal\' },
{ id: 3, text: \'Write documentation\', completed: false, priority:
\'medium\', category: \'work\' },
{ id: 4, text: \'Review code\', completed: false, priority: \'low\',
category: \'work\' },
{ id: 5, text: \'Update resume\', completed: true, priority: \'medium\',
category: \'personal\' }
\],
filter: \'all\',
sortBy: \'priority\'
}\">
<h2>Interactive Todo with Dynamic Styling</h2>
*<!-- Filter controls -->*
<div class=\"filter-bar\">
<button s-click=\"filter = \'all\'\"
s-class=\"{ active: filter === \'all\' }\">
All
</button>
<button s-click=\"filter = \'active\'\"
s-class=\"{ active: filter === \'active\' }\">
Active
</button>
<button s-click=\"filter = \'completed\'\"
s-class=\"{ active: filter === \'completed\' }\">
Completed
</button>
<select s-model=\"sortBy\" class=\"sort-select\">
<option value=\"priority\">Sort by Priority</option>
<option value=\"category\">Sort by Category</option>
<option value=\"text\">Sort by Name</option>
</select>
</div>
*<!-- Todo list -->*
<div class=\"todo-container\">
<div s-for=\"todo in todos
.filter(t => {
if(filter === \'active\') return !t.completed;
if(filter === \'completed\') return t.completed;
return true;
})
.sort((a, b) => {
if(sortBy === \'priority\') {
const priorityWeight = { high: 3, medium: 2, low: 1 };
return priorityWeight\[b.priority\] - priorityWeight\[a.priority\];
}
if(sortBy === \'category\') {
return a.category.localeCompare(b.category);
}
return a.text.localeCompare(b.text);
})\"
s-key=\"todo.id\"
class=\"todo-item\"
s-class=\"{
completed: todo.completed,
\'priority-high\': todo.priority === \'high\',
\'priority-medium\': todo.priority === \'medium\',
\'priority-low\': todo.priority === \'low\',
\'category-work\': todo.category === \'work\',
\'category-personal\': todo.category === \'personal\',
\'hover-effect\': true
}\">
<div class=\"todo-content\">
<input type=\"checkbox\"
s-model=\"todo.completed\"
class=\"todo-checkbox\">
<div class=\"todo-details\">
<span class=\"todo-text\">{todo.text}</span>
<div class=\"todo-meta\">
<span class=\"badge priority-badge\"
s-class=\"{
\'badge-high\': todo.priority === \'high\',
\'badge-medium\': todo.priority === \'medium\',
\'badge-low\': todo.priority === \'low\'
}\">
{todo.priority}
</span>
<span class=\"badge category-badge\"
s-class=\"{
\'badge-work\': todo.category === \'work\',
\'badge-personal\': todo.category === \'personal\'
}\">
{todo.category}
</span>
</div>
</div>
</div>
<div class=\"todo-actions\">
<button class=\"btn-delete\"
s-click=\"todos = todos.filter(t => t.id !== todo.id)\"
s-attr:aria-label=\"\'Delete \' + todo.text\">
×
</button>
</div>
</div>
<div s-if=\"todos.filter(t => {
if(filter === \'active\') return !t.completed;
if(filter === \'completed\') return t.completed;
return true;
}).length === 0\" class=\"empty-state\">
<p>No todos match your filter</p>
</div>
</div>
*<!-- Add new todo -->*
<div class=\"add-todo\">
<input type=\"text\"
s-bind=\"newTodoText\"
placeholder=\"Add a new todo\...\"
s-key:enter=\"if(newTodoText) {
todos.push({
id: Date.now(),
text: newTodoText,
completed: false,
priority: newPriority \|\| \'medium\',
category: newCategory \|\| \'personal\'
});
newTodoText = \'\';
}\">
<select s-model=\"newPriority\">
<option value=\"high\">High Priority</option>
<option value=\"medium\" selected>Medium Priority</option>
<option value=\"low\">Low Priority</option>
</select>
<select s-model=\"newCategory\">
<option value=\"work\">Work</option>
<option value=\"personal\" selected>Personal</option>
</select>
<button s-click=\"if(newTodoText) {
todos.push({
id: Date.now(),
text: newTodoText,
completed: false,
priority: newPriority \|\| \'medium\',
category: newCategory \|\| \'personal\'
});
newTodoText = \'\';
}\">Add Todo</button>
</div>
*<!-- Statistics -->*
<div class=\"stats\">
<div class=\"stat-card\">
<span class=\"stat-label\">Total</span>
<span class=\"stat-value\">{todos.length}</span>
</div>
<div class=\"stat-card\">
<span class=\"stat-label\">Completed</span>
<span class=\"stat-value\">{todos.filter(t =>
t.completed).length}</span>
</div>
<div class=\"stat-card\">
<span class=\"stat-label\">Active</span>
<span class=\"stat-value\">{todos.filter(t =>
!t.completed).length}</span>
</div>
<div class=\"stat-card\">
<span class=\"stat-label\">High Priority</span>
<span class=\"stat-value\">{todos.filter(t => t.priority ===
\'high\').length}</span>
</div>
</div>
</div>
<style>
.filter-bar {
display: flex;
gap: 10px;
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.filter-bar button {
padding: 8px 16px;
border: 2px solid transparent;
border-radius: 20px;
background: white;
cursor: pointer;
transition: all 0.3s;
}
.filter-bar button.active {
background: #007bff;
color: white;
border-color: #0056b3;
}
.sort-select {
margin-left: auto;
padding: 8px;
border: 2px solid #ddd;
border-radius: 4px;
}
.todo-container {
margin: 20px 0;
}
.todo-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
margin: 10px 0;
background: white;
border: 2px solid #ddd;
border-radius: 8px;
transition: all 0.3s;
}
/* Priority-based styling */
.todo-item.priority-high {
border-left-width: 8px;
border-left-color: #dc3545;
}
.todo-item.priority-medium {
border-left-width: 8px;
border-left-color: #ffc107;
}
.todo-item.priority-low {
border-left-width: 8px;
border-left-color: #28a745;
}
/* Category-based styling */
.todo-item.category-work {
background: #e3f2fd;
}
.todo-item.category-personal {
background: #f3e5f5;
}
/* Completed state */
.todo-item.completed {
opacity: 0.7;
background: #f8f9fa;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #6c757d;
}
/* Hover effect */
.todo-item.hover-effect:hover {
transform: translateX(5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.todo-content {
display: flex;
align-items: center;
gap: 15px;
flex: 1;
}
.todo-checkbox {
width: 20px;
height: 20px;
cursor: pointer;
}
.todo-details {
flex: 1;
}
.todo-text {
font-size: 16px;
display: block;
margin-bottom: 5px;
}
.todo-meta {
display: flex;
gap: 8px;
}
.badge {
padding: 3px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}
.priority-badge.badge-high {
background: #dc3545;
color: white;
}
.priority-badge.badge-medium {
background: #ffc107;
color: #333;
}
.priority-badge.badge-low {
background: #28a745;
color: white;
}
.category-badge.badge-work {
background: #007bff;
color: white;
}
.category-badge.badge-personal {
background: #6f42c1;
color: white;
}
.todo-actions {
display: flex;
gap: 5px;
}
.btn-delete {
width: 30px;
height: 30px;
border: none;
border-radius: 50%;
background: #dc3545;
color: white;
font-size: 20px;
line-height: 1;
cursor: pointer;
transition: all 0.3s;
}
.btn-delete:hover {
background: #c82333;
transform: scale(1.1);
}
.empty-state {
text-align: center;
padding: 40px;
color: #6c757d;
font-style: italic;
}
.add-todo {
display: flex;
gap: 10px;
margin: 20px 0;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.add-todo input\[type=\"text\"\] {
flex: 1;
padding: 10px;
border: 2px solid #ddd;
border-radius: 4px;
}
.add-todo select {
padding: 10px;
border: 2px solid #ddd;
border-radius: 4px;
}
.add-todo button {
padding: 10px 20px;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-top: 30px;
}
.stat-card {
padding: 20px;
background: white;
border: 2px solid #ddd;
border-radius: 8px;
text-align: center;
}
.stat-label {
display: block;
font-size: 14px;
color: #6c757d;
margin-bottom: 5px;
}
.stat-value {
display: block;
font-size: 24px;
font-weight: bold;
color: #007bff;
}
</style>8.3 Inline Styles with s-style
The s-style directive gives you fine-grained control over inline CSS styles based on your state.
Basic Style Binding
<div s-app s-state=\"{
color: \'#007bff\',
bgColor: \'#f8f9fa\',
fontSize: 16,
padding: 20,
borderRadius: 8,
opacity: 1,
rotation: 0,
scale: 1
}\">
<h2>Dynamic Inline Styles</h2>
*<!-- Basic style binding -->*
<div class=\"style-demo-box\"
s-style=\"{
color: color,
backgroundColor: bgColor,
fontSize: fontSize + \'px\',
padding: padding + \'px\',
borderRadius: borderRadius + \'px\',
opacity: opacity,
transform: \'rotate(\' + rotation + \'deg) scale(\' + scale + \')\'
}\">This box's styles update dynamically
</div>
<!-- Controls -->
<div class="style-controls">
<h3>Style Controls:</h3>
<div class="control-row">
<label>Text Color:</label>
<input type="color" s-bind="color">
</div>
<div class="control-row">
<label>Background Color:</label>
<input type="color" s-bind="bgColor">
</div>
<div class="control-row">
<label>Font Size: {fontSize}px</label>
<input type="range" s-bind="fontSize" min="12" max="32">
</div>
<div class="control-row">
<label>Padding: {padding}px</label>
<input type="range" s-bind="padding" min="0" max="50">
</div>
<div class="control-row">
<label>Border Radius: {borderRadius}px</label>
<input type="range" s-bind="borderRadius" min="0" max="50">
</div>
<div class="control-row">
<label>Opacity: {opacity}</label>
<input type="range" s-bind="opacity" min="0" max="1" step="0.1">
</div>
<div class="control-row">
<label>Rotation: {rotation}°</label>
<input type="range" s-bind="rotation" min="0" max="360">
</div>
<div class="control-row">
<label>Scale: {scale}</label>
<input type="range" s-bind="scale" min="0.5" max="2" step="0.1">
</div>
</div>
</div>
<style>
.style-demo-box {
width: 300px;
height: 200px;
margin: 20px auto;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
border: 2px solid #333;
transition: all 0.3s;
}
.style-controls {
max-width: 400px;
margin: 20px auto;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.control-row {
margin: 15px 0;
}
.control-row label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.control-row input[type="range"] {
width: 100%;
}
.control-row input[type="color"] {
width: 100%;
height: 40px;
}
</style>
Conditional and Computed Styles
<div s-app s-state=\"{
theme: \'light\',
status: \'success\',
size: \'medium\',
customColor: \'#ff6b6b\',
isHighlighted: false,
isAnimated: true
}\">
<h2>Conditional and Computed Styles</h2>
*<!-- Theme-based styles -->*
<div class=\"conditional-styles\">
<div class=\"style-card\"
s-style=\"{
backgroundColor: theme === \'light\' ? \'#ffffff\' : \'#333333\',
color: theme === \'light\' ? \'#333333\' : \'#ffffff\',
borderColor: theme === \'light\' ? \'#ddd\' : \'#666\',
boxShadow: isHighlighted ? \'0 0 20px #007bff\' : \'none\',
transition: isAnimated ? \'all 0.3s\' : \'none\'
}\">
<h3>Theme: {theme}</h3>
<p>Status: {status}</p>
</div>
*<!-- Status-based indicator -->*
<div class=\"status-indicator\"
s-style=\"{
backgroundColor:
status === \'success\' ? \'#28a745\' :
status === \'warning\' ? \'#ffc107\' :
status === \'error\' ? \'#dc3545\' : \'#6c757d\',
width:
status === \'success\' ? \'100%\' :
status === \'warning\' ? \'70%\' :
status === \'error\' ? \'30%\' : \'50%\',
height: \'30px\',
borderRadius: \'4px\',
transition: \'all 0.5s\'
}\">
</div>
*<!-- Size-based styles -->*
<div class=\"size-demo\"
s-style=\"{
fontSize:
size === \'small\' ? \'12px\' :
size === \'medium\' ? \'16px\' :
size === \'large\' ? \'24px\' : \'16px\',
padding:
size === \'small\' ? \'10px\' :
size === \'medium\' ? \'20px\' :
size === \'large\' ? \'30px\' : \'20px\'
}\">
Current size: {size}
</div>
*<!-- Custom color picker -->*
<div class=\"custom-color-demo\"
s-style=\"{
backgroundColor: customColor,
color: getContrastColor(customColor)
}\">
<p>Custom Color: {customColor}</p>
<p>Auto-contrast text</p>
</div>
*<!-- Controls -->*
<div class=\"style-controls\">
<h3>Theme:</h3>
<button s-click=\"theme = \'light\'\"
s-class=\"{ active: theme === \'light\' }\">
Light
</button>
<button s-click=\"theme = \'dark\'\"
s-class=\"{ active: theme === \'dark\' }\">
Dark
</button>
<h3>Status:</h3>
<button s-click=\"status = \'success\'\"
s-class=\"{ active: status === \'success\' }\">
Success
</button>
<button s-click=\"status = \'warning\'\"
s-class=\"{ active: status === \'warning\' }\">
Warning
</button>
<button s-click=\"status = \'error\'\"
s-class=\"{ active: status === \'error\' }\">
Error
</button>
<h3>Size:</h3>
<button s-click=\"size = \'small\'\"
s-class=\"{ active: size === \'small\' }\">
Small
</button>
<button s-click=\"size = \'medium\'\"
s-class=\"{ active: size === \'medium\' }\">
Medium
</button>
<button s-click=\"size = \'large\'\"
s-class=\"{ active: size === \'large\' }\">
Large
</button>
<h3>Custom Color:</h3>
<input type=\"color\" s-bind=\"customColor\">
<label>
<input type=\"checkbox\" s-model=\"isHighlighted\">
Highlight
</label>
<label>
<input type=\"checkbox\" s-model=\"isAnimated\">
Animated
</label>
</div>
</div>
</div>
<script>
*// Helper function for contrast color*
function getContrastColor(hexcolor) {
*// Convert hex to RGB*
let r = parseInt(hexcolor.substr(1,2), 16);
let g = parseInt(hexcolor.substr(3,2), 16);
let b = parseInt(hexcolor.substr(5,2), 16);
*// Calculate luminance*
let luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
*// Return black or white based on luminance*
return luminance > 0.5 ? \'#000000\' : \'#ffffff\';
}
</script>
<style>
.conditional-styles {
max-width: 500px;
margin: 20px auto;
}
.style-card {
padding: 30px;
border: 2px solid;
border-radius: 12px;
margin: 20px 0;
text-align: center;
}
.status-indicator {
margin: 20px 0;
}
.size-demo {
border: 2px solid #007bff;
border-radius: 8px;
margin: 20px 0;
text-align: center;
}
.custom-color-demo {
padding: 20px;
border-radius: 8px;
margin: 20px 0;
text-align: center;
transition: all 0.3s;
}
.style-controls {
margin-top: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.style-controls button {
margin: 5px;
padding: 8px 16px;
border: 2px solid transparent;
border-radius: 20px;
background: white;
cursor: pointer;
}
.style-controls button.active {
background: #007bff;
color: white;
border-color: #0056b3;
}
.style-controls label {
display: block;
margin: 10px 0;
}
</style>8.4 Real-World Project: Themeable Dashboard
Let's build a complete dashboard that demonstrates all the styling and attribute concepts we've learned:
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width,
initial-scale=1.0\">
<title>Themeable Dashboard</title>
<link rel=\"stylesheet\"
href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css\">
</head>
<body>
<div s-app s-state=\"{
// Theme configuration
theme: {
mode: \'light\',
primary: \'#4361ee\',
secondary: \'#3f37c9\',
success: \'#4cc9f0\',
danger: \'#f72585\',
warning: \'#f8961e\',
info: \'#4895ef\'
},
// User preferences
preferences: {
sidebarCollapsed: false,
fontSize: \'medium\',
animations: true,
compactMode: false,
highContrast: false
},
// Dashboard data
stats: \[
{ label: \'Revenue\', value: \'\$54,239\', change: \'+12.5%\', trend:
\'up\', icon: \'fa-dollar-sign\' },
{ label: \'Users\', value: \'8,549\', change: \'+23.1%\', trend: \'up\',
icon: \'fa-users\' },
{ label: \'Orders\', value: \'1,423\', change: \'-2.3%\', trend:
\'down\', icon: \'fa-shopping-cart\' },
{ label: \'Conversion\', value: \'3.24%\', change: \'+5.7%\', trend:
\'up\', icon: \'fa-percent\' }
\],
// Recent activities
activities: \[
{ user: \'John Doe\', action: \'created a new project\', time: \'2 min
ago\', avatar: \'JD\' },
{ user: \'Jane Smith\', action: \'updated settings\', time: \'15 min
ago\', avatar: \'JS\' },
{ user: \'Bob Johnson\', action: \'completed task\', time: \'1 hour
ago\', avatar: \'BJ\' },
{ user: \'Alice Brown\', action: \'added comment\', time: \'3 hours
ago\', avatar: \'AB\' }
\],
// Chart data
chartData: \[65, 59, 80, 81, 56, 55, 40\],
chartLabels: \[\'Mon\', \'Tue\', \'Wed\', \'Thu\', \'Fri\', \'Sat\',
\'Sun\'\],
// UI state
currentPage: \'dashboard\',
notifications: 3
}\">
*<!-- Main container with dynamic theme classes -->*
<div class=\"app-container\"
s-class=\"{
\'theme-light\': theme.mode === \'light\',
\'theme-dark\': theme.mode === \'dark\',
\'high-contrast\': preferences.highContrast,
\'compact-mode\': preferences.compactMode
}\"
s-style=\"{
\'--primary-color\': theme.primary,
\'--secondary-color\': theme.secondary,
\'--success-color\': theme.success,
\'--danger-color\': theme.danger,
\'--warning-color\': theme.warning,
\'--info-color\': theme.info,
\'--bg-color\': theme.mode === \'light\' ? \'#f8f9fa\' : \'#1a1a1a\',
\'--text-color\': theme.mode === \'light\' ? \'#333\' : \'#f8f9fa\',
\'--card-bg\': theme.mode === \'light\' ? \'#ffffff\' : \'#2d2d2d\',
\'--border-color\': theme.mode === \'light\' ? \'#dee2e6\' :
\'#404040\',
\'font-size\': preferences.fontSize === \'small\' ? \'14px\' :
preferences.fontSize === \'medium\' ? \'16px\' : \'18px\'
}\">
*<!-- Sidebar -->*
<div class=\"sidebar\"
s-class=\"{ collapsed: preferences.sidebarCollapsed }\"
s-style=\"{
width: preferences.sidebarCollapsed ? \'80px\' : \'250px\',
transition: preferences.animations ? \'all 0.3s\' : \'none\'
}\">
<div class=\"sidebar-header\">
<div class=\"logo\">
<i class=\"fas fa-cube\"></i>
<span s-show=\"!preferences.sidebarCollapsed\">DashBoard</span>
</div>
<button class=\"collapse-btn\"
s-click=\"preferences.sidebarCollapsed = !preferences.sidebarCollapsed\"
s-attr:title=\"preferences.sidebarCollapsed ? \'Expand\' :
\'Collapse\'\">
<i class=\"fas\"
s-class=\"{
\'fa-chevron-right\': preferences.sidebarCollapsed,
\'fa-chevron-left\': !preferences.sidebarCollapsed
}\"></i>
</button>
</div>
<nav class=\"sidebar-nav\">
<a href=\"#\" class=\"nav-item\"
s-class=\"{ active: currentPage === \'dashboard\' }\"
s-click=\"currentPage = \'dashboard\'\">
<i class=\"fas fa-home\"></i>
<span s-show=\"!preferences.sidebarCollapsed\">Dashboard</span>
</a>
<a href=\"#\" class=\"nav-item\"
s-class=\"{ active: currentPage === \'analytics\' }\"
s-click=\"currentPage = \'analytics\'\">
<i class=\"fas fa-chart-line\"></i>
<span s-show=\"!preferences.sidebarCollapsed\">Analytics</span>
</a>
<a href=\"#\" class=\"nav-item\"
s-class=\"{ active: currentPage === \'users\' }\"
s-click=\"currentPage = \'users\'\">
<i class=\"fas fa-users\"></i>
<span s-show=\"!preferences.sidebarCollapsed\">Users</span>
<span class=\"badge\" s-if=\"notifications >
0\">{notifications}</span>
</a>
<a href=\"#\" class=\"nav-item\"
s-class=\"{ active: currentPage === \'settings\' }\"
s-click=\"currentPage = \'settings\'\">
<i class=\"fas fa-cog\"></i>
<span s-show=\"!preferences.sidebarCollapsed\">Settings</span>
</a>
</nav>
</div>
*<!-- Main Content -->*
<div class=\"main-content\">
*<!-- Header -->*
<header class=\"header\">
<div class=\"header-left\">
<h1>{currentPage.charAt(0).toUpperCase() +
currentPage.slice(1)}</h1>
</div>
<div class=\"header-right\">
<div class=\"theme-switcher\">
<button s-click=\"theme.mode = \'light\'\"
s-class=\"{ active: theme.mode === \'light\' }\"
s-attr:title=\"\'Light mode\'\">
<i class=\"fas fa-sun\"></i>
</button>
<button s-click=\"theme.mode = \'dark\'\"
s-class=\"{ active: theme.mode === \'dark\' }\"
s-attr:title=\"\'Dark mode\'\">
<i class=\"fas fa-moon\"></i>
</button>
</div>
<div class=\"notifications\">
<i class=\"fas fa-bell\"></i>
<span class=\"badge\" s-if=\"notifications >
0\">{notifications}</span>
</div>
<div class=\"user-menu\">
<div class=\"avatar\">JD</div>
<span>John Doe</span>
</div>
</div>
</header>
*<!-- Dashboard Content -->*
<div s-if=\"currentPage === \'dashboard\'\"
class=\"dashboard-content\">
*<!-- Stats Grid -->*
<div class=\"stats-grid\">
<div s-for=\"stat in stats\"
s-key=\"stat.label\"
class=\"stat-card\"
s-style=\"{
borderLeftColor: stat.trend === \'up\' ? theme.success : theme.danger,
transform: preferences.animations && \'scale(1)\',
transition: preferences.animations ? \'all 0.3s\' : \'none\'
}\"
s-mouseenter=\"hoveredStat = stat.label\"
s-mouseleave=\"hoveredStat = null\"
s-class=\"{ \'stat-highlight\': hoveredStat === stat.label }\">
<div class=\"stat-icon\"
s-style=\"{
backgroundColor: theme.primary + \'20\',
color: theme.primary
}\">
<i class=\"fas {stat.icon}\"></i>
</div>
<div class=\"stat-content\">
<div class=\"stat-label\">{stat.label}</div>
<div class=\"stat-value\">{stat.value}</div>
<div class=\"stat-change\"
s-class=\"{ \'trend-up\': stat.trend === \'up\', \'trend-down\':
stat.trend === \'down\' }\">
<i class=\"fas\"
s-class=\"{
\'fa-arrow-up\': stat.trend === \'up\',
\'fa-arrow-down\': stat.trend === \'down\'
}\"></i>
{stat.change}
</div>
</div>
</div>
</div>
*<!-- Chart and Activities -->*
<div class=\"content-grid\">
*<!-- Chart Card -->*
<div class=\"card\">
<div class=\"card-header\">
<h3>Weekly Overview</h3>
<select s-model=\"chartPeriod\">
<option value=\"week\">Last 7 days</option>
<option value=\"month\">Last 30 days</option>
<option value=\"year\">Last year</option>
</select>
</div>
<div class=\"card-content\">
<div class=\"chart-container\">
*<!-- Simple bar chart using divs -->*
<div s-for=\"value, index in chartData\"
s-key=\"index\"
class=\"chart-bar-container\">
<div class=\"chart-bar\"
s-style=\"{
height: value + \'px\',
backgroundColor: theme.primary,
width: \'100%\',
transition: preferences.animations ? \'height 0.5s\' : \'none\'
}\"
s-attr:title=\"chartLabels\[index\] + \': \' + value\">
</div>
<div class=\"chart-label\">{chartLabels\[index\]}</div>
</div>
</div>
</div>
</div>
*<!-- Recent Activities -->*
<div class=\"card\">
<div class=\"card-header\">
<h3>Recent Activities</h3>
<a href=\"#\" class=\"view-all\">View All</a>
</div>
<div class=\"card-content\">
<div s-for=\"activity in activities\"
s-key=\"activity.time\"
class=\"activity-item\">
<div class=\"activity-avatar\"
s-style=\"{
backgroundColor: theme.primary + \'20\',
color: theme.primary
}\">
{activity.avatar}
</div>
<div class=\"activity-details\">
<div class=\"activity-text\">
<strong>{activity.user}</strong> {activity.action}
</div>
<div class=\"activity-time\">{activity.time}</div>
</div>
</div>
</div>
</div>
</div>
</div>
*<!-- Settings Page -->*
<div s-if=\"currentPage === \'settings\'\" class=\"settings-content\">
<div class=\"card\">
<div class=\"card-header\">
<h3>Theme Customization</h3>
</div>
<div class=\"card-content\">
<div class=\"settings-group\">
<label>Primary Color:</label>
<input type=\"color\" s-bind=\"theme.primary\">
</div>
<div class=\"settings-group\">
<label>Secondary Color:</label>
<input type=\"color\" s-bind=\"theme.secondary\">
</div>
<div class=\"settings-group\">
<label>Success Color:</label>
<input type=\"color\" s-bind=\"theme.success\">
</div>
<div class=\"settings-group\">
<label>Danger Color:</label>
<input type=\"color\" s-bind=\"theme.danger\">
</div>
</div>
</div>
<div class=\"card\">
<div class=\"card-header\">
<h3>User Preferences</h3>
</div>
<div class=\"card-content\">
<div class=\"settings-group\">
<label>
<input type=\"checkbox\" s-model=\"preferences.sidebarCollapsed\">
Collapse Sidebar
</label>
</div>
<div class=\"settings-group\">
<label>Font Size:</label>
<select s-model=\"preferences.fontSize\">
<option value=\"small\">Small</option>
<option value=\"medium\">Medium</option>
<option value=\"large\">Large</option>
</select>
</div>
<div class=\"settings-group\">
<label>
<input type=\"checkbox\" s-model=\"preferences.animations\">
Enable Animations
</label>
</div>
<div class=\"settings-group\">
<label>
<input type=\"checkbox\" s-model=\"preferences.compactMode\">
Compact Mode
</label>
</div>
<div class=\"settings-group\">
<label>
<input type=\"checkbox\" s-model=\"preferences.highContrast\">
High Contrast
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script type=\"module\">
import { createApp } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
createApp().mount(\'\[s-app\]\');
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto,
sans-serif;
background: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s, color 0.3s;
}
.app-container {
display: flex;
min-height: 100vh;
}
/* Sidebar Styles */
.sidebar {
background: var(--card-bg);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
padding: 20px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-color);
}
.logo {
display: flex;
align-items: center;
gap: 10px;
font-size: 20px;
font-weight: bold;
color: var(--primary-color);
}
.logo i {
font-size: 24px;
}
.collapse-btn {
width: 30px;
height: 30px;
border: none;
background: transparent;
color: var(--text-color);
cursor: pointer;
border-radius: 4px;
}
.collapse-btn:hover {
background: var(--border-color);
}
.sidebar-nav {
flex: 1;
padding: 20px 0;
}
.nav-item {
display: flex;
align-items: center;
gap: 15px;
padding: 12px 20px;
color: var(--text-color);
text-decoration: none;
transition: background 0.3s;
position: relative;
}
.nav-item i {
width: 20px;
}
.nav-item:hover {
background: var(--border-color);
}
.nav-item.active {
background: var(--primary-color);
color: white;
}
.nav-item .badge {
position: absolute;
right: 20px;
background: var(--danger-color);
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 12px;
}
/* Main Content Styles */
.main-content {
flex: 1;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.header-right {
display: flex;
align-items: center;
gap: 20px;
}
.theme-switcher {
display: flex;
gap: 5px;
background: var(--card-bg);
padding: 3px;
border-radius: 30px;
border: 1px solid var(--border-color);
}
.theme-switcher button {
padding: 8px 12px;
border: none;
background: transparent;
color: var(--text-color);
border-radius: 30px;
cursor: pointer;
}
.theme-switcher button.active {
background: var(--primary-color);
color: white;
}
.notifications {
position: relative;
cursor: pointer;
}
.notifications .badge {
position: absolute;
top: -8px;
right: -8px;
background: var(--danger-color);
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 12px;
}
.user-menu {
display: flex;
align-items: center;
gap: 10px;
}
.avatar {
width: 40px;
height: 40px;
background: var(--primary-color);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
/* Dashboard Content */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
display: flex;
align-items: center;
gap: 20px;
border-left-width: 4px;
border-left-style: solid;
}
.stat-card.stat-highlight {
transform: scale(1.02);
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.stat-content {
flex: 1;
}
.stat-label {
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
margin-bottom: 5px;
}
.stat-change {
font-size: 12px;
}
.stat-change.trend-up {
color: var(--success-color);
}
.stat-change.trend-down {
color: var(--danger-color);
}
/* Content Grid */
.content-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
}
.card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
}
.card-header {
padding: 20px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.card-content {
padding: 20px;
}
/* Chart Styles */
.chart-container {
display: flex;
align-items: flex-end;
justify-content: space-around;
height: 200px;
gap: 10px;
}
.chart-bar-container {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.chart-bar {
width: 100%;
background: var(--primary-color);
border-radius: 4px 4px 0 0;
min-height: 2px;
}
.chart-label {
font-size: 12px;
color: #666;
}
/* Activity List */
.activity-item {
display: flex;
align-items: center;
gap: 15px;
padding: 10px 0;
border-bottom: 1px solid var(--border-color);
}
.activity-item:last-child {
border-bottom: none;
}
.activity-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.activity-details {
flex: 1;
}
.activity-text {
margin-bottom: 3px;
}
.activity-time {
font-size: 12px;
color: #666;
}
.view-all {
color: var(--primary-color);
text-decoration: none;
font-size: 14px;
}
/* Settings */
.settings-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.settings-group {
margin: 15px 0;
}
.settings-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.settings-group input\[type=\"color\"\] {
width: 100%;
height: 40px;
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
}
.settings-group select {
width: 100%;
padding: 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--card-bg);
color: var(--text-color);
}
.settings-group input\[type=\"checkbox\"\] {
margin-right: 10px;
}
/* Compact Mode */
.compact-mode .stat-card {
padding: 10px;
}
.compact-mode .card-header,
.compact-mode .card-content {
padding: 10px;
}
/* High Contrast */
.high-contrast {
--primary-color: #0000ff !important;
--text-color: #000000 !important;
--bg-color: #ffffff !important;
--card-bg: #ffffff !important;
--border-color: #000000 !important;
}
.high-contrast .nav-item.active {
background: #000000 !important;
color: #ffffff !important;
}
</style>
</body>
</html>Chapter 8 Summary
You've now mastered dynamic styling and attributes in SimpliJS:
Dynamic attribute binding with s-attr for any HTML attribute
Boolean attributes and how they're handled specially
Data attributes for storing additional element information
Dynamic classes with both object and array syntax
Conditional classes based on complex state logic
Inline styles with s-style for fine-grained control
Conditional and computed styles for responsive designs
CSS custom properties integration with dynamic styles
Real-world dashboard project combining all concepts
You've seen how SimpliJS makes styling as reactive as data binding. Every aspect of presentation can respond to user interactions, preferences, and application state.
In the next chapter, we'll explore JavaScript-based components, moving beyond HTML-First to create reusable, encapsulated components with full programmatic control.
End of Chapter 8
Part 3: The JavaScript Layer - Going Deeper
Chapter 9: Your First JavaScript Component
Welcome to Part 3, where we transition from HTML-First development to creating reusable JavaScript components. While HTML-First is powerful for many scenarios, JavaScript components give you more control, reusability, and encapsulation. In this chapter, you'll learn how to create your first SimpliJS component using JavaScript.
9.1 Why JavaScript Components?
Before diving into code, let's understand why you might want to use JavaScript components instead of pure HTML-First.
The Case for Components
HTML-First is excellent for simple interactions and rapid prototyping. However, as applications grow, you'll encounter scenarios where JavaScript components shine:
Reusability: Write once, use everywhere
Encapsulation: Keep HTML, CSS, and JavaScript together
Complex Logic: Handle intricate business logic more cleanly
Code Organization: Break large applications into manageable pieces
Testing: Test components in isolation
Maintainability: Update one component instead of many places
HTML-First vs. JavaScript Components
<div s-app>
<h2>Comparison: HTML-First vs JavaScript Components</h2>
*<!-- HTML-First Counter -->*
<div class=\"example\">
<h3>HTML-First Counter</h3>
<div s-state=\"{ count: 0 }\">
<p>Count: {count}</p>
<button s-click=\"count++\">Increment</button>
</div>
<p class=\"note\">✓ Simple, no JavaScript needed</p>
<p class=\"note\">✗ Can\'t reuse without copy-paste</p>
</div>
*<!-- JavaScript Component Counter (we\'ll build this) -->*
<div class=\"example\">
<h3>JavaScript Component Counter</h3>
<my-counter></my-counter>
<my-counter></my-counter>
<my-counter></my-counter>
<p class=\"note\">✓ Reusable, encapsulated</p>
<p class=\"note\">✓ Each instance independent</p>
</div>
</div>
<style>
.example {
margin: 20px 0;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.note {
margin: 5px 0;
font-size: 14px;
color: #666;
}
</style>9.2 The component() Function
SimpliJS provides a component() function that lets you define custom elements. Think of it as creating your own HTML tags with superpowers.
Basic Component Structure
<!DOCTYPE html>
<html>
<head>
<title>My First Component</title>
</head>
<body>
*<!-- Use our custom component -->*
<my-greeting></my-greeting>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// Define a component*
component(\'my-greeting\', () => {
return {
render: () => \`<h1>Hello from my first component!</h1>\`
};
});
*// Mount the app*
createApp().mount(\'\[s-app\]\');
</script>
</body>
</html>Anatomy of a Component
Let's break down what's happening:
Component Name: 'my-greeting' - must contain a hyphen (custom element rule)
Setup Function: () => { ... } - runs once when component is created
Return Object: Contains the component's definition
render Method: Returns HTML string to display
component(\'my-component\', () => {
*// Setup code runs once*
console.log(\'Component is being created\');
return {
*// Render method returns HTML*
render: () => {
console.log(\'Component is rendering\');
return \`<div>Hello World!</div>\`;
}
};
});9.3 Creating Your First Component: A Reusable Counter
Let's build a reusable counter component that demonstrates the power of components.
Simple Counter Component
<!DOCTYPE html>
<html>
<head>
<title>Counter Component</title>
<style>
.counter {
display: inline-block;
padding: 20px;
margin: 10px;
border: 2px solid #007bff;
border-radius: 8px;
text-align: center;
font-family: Arial, sans-serif;
}
.counter button {
margin: 5px;
padding: 8px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.counter button:hover {
background: #0056b3;
}
.counter .count {
font-size: 24px;
font-weight: bold;
margin: 10px 0;
color: #333;
}
</style>
</head>
<body>
<h1>Reusable Counter Components</h1>
*<!-- Multiple independent counters -->*
<div class=\"counter-demo\">
<my-counter></my-counter>
<my-counter></my-counter>
<my-counter></my-counter>
</div>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// Define the counter component*
component(\'my-counter\', () => {
*// Create reactive state for this component instance*
const state = reactive({ count: 0 });
*// Component methods*
const increment = () => {
state.count++;
};
const decrement = () => {
state.count--;
};
const reset = () => {
state.count = 0;
};
*// Return the component definition*
return {
*// Render method - called whenever state changes*
render: () => \`
<div class=\"counter\">
<div class=\"count\">\${state.count}</div>
<div>
<button
onclick=\"this.closest(\'my-counter\').increment()\">+</button>
<button
onclick=\"this.closest(\'my-counter\').decrement()\">-</button>
<button
onclick=\"this.closest(\'my-counter\').reset()\">Reset</button>
</div>
</div>
\`,
*// Expose methods to HTML*
increment,
decrement,
reset
};
});
*// Mount the app (components are automatically available)*
createApp().mount(\'\[s-app\]\');
</script>
<style>
.counter-demo {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin: 20px 0;
}
</style>
</body>
</html>Understanding the Counter Component
Let's analyze each part:
const state = reactive({ count: 0 });This creates a reactive object. When state.count changes, the component automatically re-renders.
const increment = () => { state.count++; };Methods that modify state. They're exposed to HTML so buttons can call them.
render: () => \`
<div class=\"counter\">
<div class=\"count\">\${state.count}</div>
\...
</div>
\`
Returns HTML string. Notice we use \${state.count} to insert the current
value.
4. **Method Binding in HTML**:
html
<button onclick=\"this.closest(\'my-counter\').increment()\">
This finds the parent <my-counter> element and calls
its increment() method.9.4 Component Props: Making Components Configurable
Props (properties) allow you to pass data into components, making them configurable and reusable.
Basic Props Example
<!DOCTYPE html>
<html>
<head>
<title>Component Props</title>
<style>
.user-card {
border: 2px solid #007bff;
border-radius: 8px;
padding: 20px;
margin: 10px;
display: inline-block;
min-width: 200px;
}
.user-card h3 {
margin: 0 0 10px;
color: #007bff;
}
.user-card .role {
color: #666;
font-style: italic;
}
</style>
</head>
<body>
<h1>Component Props Demo</h1>
*<!-- Using props to configure components -->*
<user-card name=\"Alice\" role=\"Developer\"
department=\"Engineering\"></user-card>
<user-card name=\"Bob\" role=\"Designer\"
department=\"Creative\"></user-card>
<user-card name=\"Charlie\" role=\"Manager\"
department=\"Operations\"></user-card>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// Component with props*
component(\'user-card\', (element, props) => {
*// props contains all attributes passed to the element*
console.log(\'Props received:\', props);
return {
render: () => \`
<div class=\"user-card\">
<h3>\${props.name \|\| \'Unknown\'}</h3>
<p class=\"role\">\${props.role \|\| \'No role\'}</p>
<p>Department: \${props.department \|\| \'Not assigned\'}</p>
</div>
\`
};
});
createApp().mount(\'\[s-app\]\');
</script>
</body>
</html>Props with Default Values and Validation
<!DOCTYPE html>
<html>
<head>
<title>Advanced Props</title>
<style>
.product-card {
border: 2px solid #28a745;
border-radius: 8px;
padding: 20px;
margin: 10px;
display: inline-block;
width: 250px;
vertical-align: top;
}
.product-card.in-stock {
border-color: #28a745;
}
.product-card.out-of-stock {
border-color: #dc3545;
opacity: 0.7;
}
.price {
font-size: 20px;
font-weight: bold;
color: #28a745;
}
.stock-status {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 12px;
}
.in-stock .stock-status {
background: #d4edda;
color: #155724;
}
.out-of-stock .stock-status {
background: #f8d7da;
color: #721c24;
}
</style>
</head>
<body>
<h1>Product Catalog with Props</h1>
<div class=\"product-grid\">
<product-card
name=\"Laptop\"
price=\"999\"
stock=\"10\"
category=\"Electronics\"
rating=\"4.5\">
</product-card>
<product-card
name=\"Headphones\"
price=\"199\"
stock=\"0\"
category=\"Audio\"
rating=\"4.8\">
</product-card>
<product-card
name=\"Mouse\"
price=\"49\"
stock=\"25\"
category=\"Accessories\"
rating=\"4.2\">
</product-card>
</div>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'product-card\', (element, props) => {
*// Set default values*
const name = props.name \|\| \'Unnamed Product\';
const price = parseFloat(props.price) \|\| 0;
const stock = parseInt(props.stock) \|\| 0;
const category = props.category \|\| \'General\';
const rating = parseFloat(props.rating) \|\| 0;
*// Derived values*
const inStock = stock > 0;
const formattedPrice = price.toFixed(2);
const stars = \'★\'.repeat(Math.floor(rating)) + \'☆\'.repeat(5 -
Math.floor(rating));
return {
render: () => \`
<div class=\"product-card \${inStock ? \'in-stock\' :
\'out-of-stock\'}\">
<h3>\${name}</h3>
<p class=\"price\">\$\${formattedPrice}</p>
<p>Category: \${category}</p>
<p>Rating: \${stars} (\${rating})</p>
<p>
<span class=\"stock-status\">
\${inStock ? \`In Stock (\${stock})\` : \'Out of Stock\'}
</span>
</p>
</div>
\`
};
});
createApp().mount(\'\[s-app\]\');
</script>
<style>
.product-grid {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin: 20px 0;
}
</style>
</body>
</html>9.5 Component State with reactive()
The reactive() function creates deeply reactive objects that trigger re-renders when any property changes.
Reactive State Deep Dive
<!DOCTYPE html>
<html>
<head>
<title>Reactive State Deep Dive</title>
</head>
<body>
<todo-list></todo-list>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'todo-list\', () => {
*// Complex reactive state*
const state = reactive({
todos: \[
{ id: 1, text: \'Learn components\', completed: false, priority:
\'high\' },
{ id: 2, text: \'Build a project\', completed: false, priority:
\'medium\' },
{ id: 3, text: \'Master reactivity\', completed: false, priority:
\'low\' }
\],
newTodo: \'\',
filter: \'all\',
stats: {
total: 0,
completed: 0,
pending: 0
}
});
*// Update stats whenever todos change*
const updateStats = () => {
state.stats.total = state.todos.length;
state.stats.completed = state.todos.filter(t => t.completed).length;
state.stats.pending = state.todos.filter(t => !t.completed).length;
};
*// Call initially*
updateStats();
*// Methods*
const addTodo = () => {
if (state.newTodo.trim()) {
state.todos.push({
id: Date.now(),
text: state.newTodo,
completed: false,
priority: \'medium\'
});
state.newTodo = \'\';
updateStats();
}
};
const toggleTodo = (id) => {
const todo = state.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
updateStats();
}
};
const deleteTodo = (id) => {
state.todos = state.todos.filter(t => t.id !== id);
updateStats();
};
const setFilter = (filter) => {
state.filter = filter;
};
*// Filtered todos (computed property)*
const getFilteredTodos = () => {
switch(state.filter) {
case \'active\':
return state.todos.filter(t => !t.completed);
case \'completed\':
return state.todos.filter(t => t.completed);
default:
return state.todos;
}
};
return {
render: () => {
const filteredTodos = getFilteredTodos();
return \`
<div style=\"max-width: 500px; margin: 20px auto;\">
<h2>Todo List (\${state.stats.total} total)</h2>
<!-- Stats -->
<div style=\"display: flex; gap: 10px; margin: 20px 0;\">
<div style=\"flex:1; text-align: center;\">
<strong>Total</strong>
<div>\${state.stats.total}</div>
</div>
<div style=\"flex:1; text-align: center; color: #28a745;\">
<strong>Completed</strong>
<div>\${state.stats.completed}</div>
</div>
<div style=\"flex:1; text-align: center; color: #dc3545;\">
<strong>Pending</strong>
<div>\${state.stats.pending}</div>
</div>
</div>
<!-- Add Todo -->
<div style=\"display: flex; gap: 10px; margin: 20px 0;\">
<input type=\"text\"
value=\"\${state.newTodo}\"
oninput=\"this.closest(\'todo-list\').updateNewTodo(this.value)\"
placeholder=\"Add a new todo\...\"
style=\"flex:1; padding: 8px;\">
<button onclick=\"this.closest(\'todo-list\').addTodo()\"
style=\"padding: 8px 16px; background: #28a745; color: white; border:
none; border-radius: 4px;\">
Add
</button>
</div>
<!-- Filters -->
<div style=\"display: flex; gap: 10px; margin: 20px 0;\">
<button onclick=\"this.closest(\'todo-list\').setFilter(\'all\')\"
style=\"padding: 5px 10px; background: \${state.filter === \'all\' ?
\'#007bff\' : \'#f8f9fa\'}; color: \${state.filter === \'all\' ?
\'white\' : \'#333\'}; border: 1px solid #ddd; border-radius: 4px;
cursor: pointer;\">
All
</button>
<button onclick=\"this.closest(\'todo-list\').setFilter(\'active\')\"
style=\"padding: 5px 10px; background: \${state.filter === \'active\' ?
\'#007bff\' : \'#f8f9fa\'}; color: \${state.filter === \'active\' ?
\'white\' : \'#333\'}; border: 1px solid #ddd; border-radius: 4px;
cursor: pointer;\">
Active
</button>
<button
onclick=\"this.closest(\'todo-list\').setFilter(\'completed\')\"
style=\"padding: 5px 10px; background: \${state.filter === \'completed\'
? \'#007bff\' : \'#f8f9fa\'}; color: \${state.filter === \'completed\' ?
\'white\' : \'#333\'}; border: 1px solid #ddd; border-radius: 4px;
cursor: pointer;\">
Completed
</button>
</div>
<!-- Todo List -->
<div style=\"margin-top: 20px;\">
\${filteredTodos.map(todo => \`
<div style=\"display: flex; align-items: center; gap: 10px; padding:
10px; border-bottom: 1px solid #eee;\">
<input type=\"checkbox\"
\${todo.completed ? \'checked\' : \'\'}
onchange=\"this.closest(\'todo-list\').toggleTodo(\${todo.id})\">
<span style=\"flex:1; \${todo.completed ? \'text-decoration:
line-through; color: #999;\' : \'\'}\">
\${todo.text}
</span>
<span style=\"padding: 2px 6px; border-radius: 4px; font-size: 12px;
background: \${todo.priority === \'high\' ? \'#dc3545\' : todo.priority
=== \'medium\' ? \'#ffc107\' : \'#28a745\'};
color: \${todo.priority === \'high\' ? \'white\' : \'#333\'};\">
\${todo.priority}
</span>
<button onclick=\"this.closest(\'todo-list\').deleteTodo(\${todo.id})\"
style=\"padding: 5px 10px; background: #dc3545; color: white; border:
none; border-radius: 4px; cursor: pointer;\">
×
</button>
</div>
\`).join(\'\')}
\${filteredTodos.length === 0 ? \`
<p style=\"text-align: center; color: #999; padding: 40px;\">No todos found
</p>
` : ''}
</div>
</div>
`;
},
// Expose methods and state helpers
addTodo,
toggleTodo,
deleteTodo,
setFilter,
updateNewTodo: (value) => { state.newTodo = value; }
};
});
createApp().mount('[s-app]');
</script>
</body>
</html>
9.6 Component Lifecycle
Components have a lifecycle - they're created, mounted to the DOM, updated, and eventually destroyed. SimpliJS provides hooks to tap into these moments.
Lifecycle Hooks Example
<!DOCTYPE html>
<html>
<head>
<title>Component Lifecycle</title>
</head>
<body>
<lifecycle-demo></lifecycle-demo>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'lifecycle-demo\', () => {
const state = reactive({
count: 0,
logs: \[\]
});
*// onMount - called when component is added to DOM*
const onMount = () => {
state.logs.push(\'Component mounted at \' + new
Date().toLocaleTimeString());
console.log(\'Component mounted\');
*// Start a timer*
state.timer = setInterval(() => {
state.logs.push(\'Timer tick at \' + new Date().toLocaleTimeString());
}, 5000);
};
*// onUpdate - called after each render*
const onUpdate = () => {
console.log(\'Component updated, count:\', state.count);
};
*// onDestroy - called when component is removed from DOM*
const onDestroy = () => {
clearInterval(state.timer);
console.log(\'Component destroyed, cleaned up timer\');
};
*// onError - called if an error occurs*
const onError = (error) => {
console.error(\'Component error:\', error);
state.logs.push(\'ERROR: \' + error.message);
};
return {
render: () => \`
<div style=\"max-width: 500px; margin: 20px auto; padding: 20px;
border: 2px solid #007bff; border-radius: 8px;\">
<h2>Lifecycle Demo</h2>
<p>Count: \${state.count}</p>
<div style=\"margin: 20px 0;\">
<button onclick=\"this.closest(\'lifecycle-demo\').increment()\"
style=\"padding: 8px 16px; background: #007bff; color: white; border:
none; border-radius: 4px; margin-right: 10px;\">
Increment
</button>
<button onclick=\"this.closest(\'lifecycle-demo\').causeError()\"
style=\"padding: 8px 16px; background: #dc3545; color: white; border:
none; border-radius: 4px;\">
Cause Error
</button>
</div>
<div style=\"margin-top: 20px;\">
<h3>Event Log:</h3>
<div style=\"max-height: 200px; overflow-y: auto; background: #f8f9fa;
padding: 10px; border-radius: 4px;\">
\${state.logs.map(log => \`<div style=\"margin: 5px 0; font-size:
12px;\">\${log}</div>\`).join(\'\')}
\${state.logs.length === 0 ? \'<div style=\"color: #999;\">No events
yet</div>\' : \'\'}
</div>
</div>
<p style=\"margin-top: 20px; font-size: 12px; color: #666;\">Check the console to see lifecycle logs
</p>
</div>
`,
increment: () => {
state.count++;
},
causeError: () => {
// This will trigger onError
undefinedFunction();
},
onMount,
onUpdate,
onDestroy,
onError
};
});
createApp().mount('[s-app]');
</script>
</body>
</html>
Lifecycle Hook Reference
| Hook | Description | Use Cases |
|---|---|---|
| onMount | Called after component is added to DOM | API calls, timers, event listeners |
| onUpdate | Called after each render | React to changes, DOM measurements |
| onDestroy | Called before component is removed | Cleanup timers, remove listeners |
| onError | Called when error occurs in component | Error logging, fallback UI |
9.7 Component Methods and Event Handling
Components can expose methods that can be called from HTML or other components.
Method Exposure Patterns
<!DOCTYPE html>
<html>
<head>
<title>Component Methods</title>
</head>
<body>
<method-demo></method-demo>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'method-demo\', () => {
const state = reactive({
value: 0,
history: \[\]
});
*// Private methods (not exposed)*
const logAction = (action) => {
state.history.push({
action,
value: state.value,
timestamp: new Date().toLocaleTimeString()
});
};
*// Public methods (exposed)*
const increment = (amount = 1) => {
state.value += amount;
logAction(\`increment by \${amount}\`);
};
const decrement = (amount = 1) => {
state.value -= amount;
logAction(\`decrement by \${amount}\`);
};
const multiply = (factor) => {
state.value *= factor;
logAction(\`multiply by \${factor}\`);
};
const reset = () => {
state.value = 0;
logAction(\'reset\');
};
const getValue = () => {
return state.value;
};
const clearHistory = () => {
state.history = \[\];
};
return {
render: () => \`
<div style=\"max-width: 400px; margin: 20px auto; padding: 20px;
border: 2px solid #28a745; border-radius: 8px;\">
<h2>Method Demo</h2>
<div style=\"font-size: 48px; text-align: center; margin: 20px 0;\">
\${state.value}
</div>
<div style=\"display: grid; grid-template-columns: repeat(3, 1fr); gap:
10px; margin: 20px 0;\">
<button onclick=\"this.closest(\'method-demo\').increment()\"
style=\"padding: 10px; background: #28a745; color: white; border: none;
border-radius: 4px;\">
+1
</button>
<button onclick=\"this.closest(\'method-demo\').increment(5)\"
style=\"padding: 10px; background: #28a745; color: white; border: none;
border-radius: 4px;\">
+5
</button>
<button onclick=\"this.closest(\'method-demo\').increment(10)\"
style=\"padding: 10px; background: #28a745; color: white; border: none;
border-radius: 4px;\">
+10
</button>
<button onclick=\"this.closest(\'method-demo\').decrement()\"
style=\"padding: 10px; background: #dc3545; color: white; border: none;
border-radius: 4px;\">
-1
</button>
<button onclick=\"this.closest(\'method-demo\').decrement(5)\"
style=\"padding: 10px; background: #dc3545; color: white; border: none;
border-radius: 4px;\">
-5
</button>
<button onclick=\"this.closest(\'method-demo\').decrement(10)\"
style=\"padding: 10px; background: #dc3545; color: white; border: none;
border-radius: 4px;\">
-10
</button>
<button onclick=\"this.closest(\'method-demo\').multiply(2)\"
style=\"padding: 10px; background: #ffc107; color: #333; border: none;
border-radius: 4px;\">
×2
</button>
<button onclick=\"this.closest(\'method-demo\').reset()\"
style=\"padding: 10px; background: #6c757d; color: white; border: none;
border-radius: 4px;\">
Reset
</button>
<button onclick=\"this.closest(\'method-demo\').clearHistory()\"
style=\"padding: 10px; background: #17a2b8; color: white; border: none;
border-radius: 4px;\">
Clear Log
</button>
</div>
<div style=\"margin-top: 20px;\">
<h3>History</h3>
<div style=\"max-height: 150px; overflow-y: auto; background: #f8f9fa;
padding: 10px; border-radius: 4px;\">
\${state.history.map(h => \`
<div style=\"margin: 5px 0; font-size: 12px;\">
\[\${h.timestamp}\] \${h.action} → \${h.value}
</div>
\`).join(\'\')}
\${state.history.length === 0 ? \`
<div style=\"color: #999; text-align: center;\">No history yet</div>
\` : \'\'}
</div>
</div>
<p style=\"margin-top: 10px; font-size: 12px; color: #666;\">
Current value (via getValue): \${getValue()}
</p>
</div>
\`,
*// Expose public methods*
increment,
decrement,
multiply,
reset,
getValue,
clearHistory
};
});
createApp().mount(\'\[s-app\]\');
</script>
</body>
</html>9.8 Component Composition: Building Complex UIs
Components can be composed together to build complex UIs. Each component manages its own state and behavior.
Nested Components Example
<!DOCTYPE html>
<html>
<head>
<title>Component Composition</title>
<style>
* { box-sizing: border-box; }
body { font-family: Arial, sans-serif; margin: 20px; background:#f0f2f5; }
</style>
</head>
<body>
<blog-app></blog-app>
<script type="module">
import { createApp, component, reactive } from 'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js';
// Header Component
component('app-header', (element, props) => {
return {
render: () => `
<header style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 12px 12px 0 0;">
<div style="max-width: 1200px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center;">
<h1 style="margin: 0;">📝 ${props.title || 'My Blog'}</h1>
<nav style="display: flex; gap: 20px;">
<a href="#" style="color: white; text-decoration: none;">Home</a>
<a href="#" style="color: white; text-decoration: none;">About</a>
<a href="#" style="color: white; text-decoration: none;">Contact</a>
</nav>
</div>
</header>
`
};
});
// Post Component
component('blog-post', (element, props) => {
const state = reactive({
likes: 0,
showComments: false
});
const like = () => {
state.likes++;
};
return {
render: () => `
<article style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<h2>${props.title}</h2>
<div style="display: flex; gap: 10px; color: #666; font-size: 14px; margin: 10px 0;">
<span>By ${props.author}</span>
<span>•</span>
<span>${props.date}</span>
<span>•</span>
<span>${props.category}</span>
</div>
<p style="line-height: 1.6; color: #444;">${props.content}</p>
<div style="display: flex; align-items: center; gap: 20px; margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee;">
<button onclick="this.closest('blog-post').like()"
❤️ ${state.likes} Likes
</button>
<button onclick="this.closest('blog-post').toggleComments()"
style="padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">
💬 ${state.showComments ? 'Hide' : 'Show'} Comments
</button>
</div>
${state.showComments ? `
<div style="margin-top: 20px;">
<comment-section post-id="${props.id}"></comment-section>
</div>
` : ''}
</article>
`,
like,
toggleComments: () => { state.showComments = !state.showComments; }
};
});
// Comment Component
component('blog-comment', (element, props) => {
return {
render: () => `
<div style="padding: 10px; margin: 5px 0; background: #f8f9fa; border-radius: 4px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
<strong>${props.author}</strong>
<span style="color: #666; font-size: 12px;">${props.time}</span>
</div>
<p style="margin: 0; color: #333;">${props.content}</p>
</div>
`
};
});
// Comment Section Component
component('comment-section', (element, props) => {
const state = reactive({
comments: [
{ author: 'Alice', content: 'Great post! Very informative.', time: '5 min ago' },
{ author: 'Bob', content: 'Thanks for sharing!', time: '15 min ago' }
],
newComment: ''
});
const addComment = () => {
if (state.newComment.trim()) {
state.comments.push({
author: 'Current User',
content: state.newComment,
time: 'Just now'
});
state.newComment = '';
}
};
return {
render: () => `
<div style="margin-top: 20px;">
<h4 style="margin-bottom: 10px;">Comments (${state.comments.length})</h4>
<div style="margin-bottom: 15px; display: flex; gap: 10px;">
<input type="text"
value="${state.newComment}"
oninput="this.closest('comment-section').updateNewComment(this.value)"
placeholder="Add a comment..."
style="flex:1; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
<button onclick="this.closest('comment-section').addComment()"
style="padding: 8px 16px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer;">
Post
</button>
</div>
<div style="max-height: 300px; overflow-y: auto;">
${state.comments.map(comment => `
<blog-comment
author="${comment.author}"
content="${comment.content}"
time="${comment.time}">
</blog-comment>
`).join('')}
${state.comments.length === 0 ? `
<p style="text-align: center; color: #999; padding: 20px;">
No comments yet. Be the first to comment!
</p>
` : ''}
</div>
</div>
`,
addComment,
updateNewComment: (value) => { state.newComment = value; }
};
});
// Main Blog App Component
component('blog-app', () => {
const state = reactive({
posts: [
{
id: 1,
title: 'Getting Started with SimpliJS',
author: 'John Doe',
date: '2024-01-15',
category: 'Tutorial',
content: 'SimpliJS is a revolutionary framework that makes web development simple and enjoyable. In this post, we explore the basics of components and reactivity...'
},
{
id: 2,
title: 'Understanding Reactivity',
author: 'Jane Smith',
date: '2024-01-14',
category: 'Concepts',
content: 'Reactivity is at the heart of SimpliJS. Learn how the proxy-based system tracks changes and updates the DOM efficiently...'
},
{
id: 3,
title: 'Building Reusable Components',
author: 'Bob Johnson',
date: '2024-01-13',
category: 'Best Practices',
content: 'Components are the building blocks of SimpliJS applications. Discover patterns for creating reusable, maintainable components...'
}
]
});
return {
render: () => `
<div style="max-width: 1200px; margin: 0 auto;">
<app-header title="SimpliJS Blog"></app-header>
<main style="background: white; padding: 20px; border-radius: 0 0 12px 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<div style="display: grid; grid-template-columns: 3fr 1fr; gap: 20px;">
<div>
${state.posts.map(post => `
<blog-post
id="${post.id}"
title="${post.title}"
author="${post.author}"
date="${post.date}"
category="${post.category}"
content="${post.content}">
</blog-post>
`).join('')}
</div>
<aside>
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px;">
<h3>About</h3>
<p style="color: #666; line-height: 1.6;">
Welcome to the SimpliJS blog! Here you'll find tutorials,
best practices, and updates about the framework.
</p>
<h4 style="margin-top: 20px;">Categories</h4>
<ul style="list-style: none; padding: 0;">
<li style="margin: 5px 0;">
<a href="#" style="color: #007bff; text-decoration: none;">Tutorials</a>
</li>
<li style="margin: 5px 0;">
<a href="#" style="color: #007bff; text-decoration: none;">Concepts</a>
</li>
<li style="margin: 5px 0;">
<a href="#" style="color: #007bff; text-decoration: none;">Best Practices</a>
</li>
<li style="margin: 5px 0;">
<a href="#" style="color: #007bff; text-decoration: none;">News</a>
</li>
</ul>
</div>
</aside>
</div>
</main>
</div>
`
};
});
createApp().mount('[s-app]');
</script>
</body>
</html>
Chapter 9 Summary
You've now mastered JavaScript components in SimpliJS:
Why components matter for reusability and organization
The component() function for defining custom elements
Component state with reactive() for automatic updates
Props for making components configurable
Component methods for encapsulating behavior
Lifecycle hooks for managing component lifecycle
Component composition for building complex UIs
Method exposure patterns for interactivity
You've seen how components transform your development experience, allowing you to build reusable, encapsulated pieces that can be composed into sophisticated applications.
In the next chapter, we'll dive deeper into programmatic reactive state management with the reactive() function, exploring its full capabilities and advanced patterns.
End of Chapter 9
Chapter 10: Programmatic Reactive State with reactive()
Welcome to Chapter 10, where we dive deep into the heart of SimpliJS's reactivity system. While you've used s-state for HTML-First reactivity, the reactive() function gives you programmatic control over reactive state in your JavaScript components. This chapter will transform you from a reactive state user into a reactive state master.
10.1 Understanding Proxies: The Magic Behind Reactivity
Before we dive into code, let's understand what makes reactivity possible in SimpliJS. At its core, the reactive() function uses JavaScript Proxies—a powerful feature that allows us to intercept and customize operations on objects.
What is a Proxy?
A Proxy wraps an object and lets you intercept fundamental operations like reading properties, writing properties, checking if properties exist, and more.
*// A simple proxy example (conceptual)*
const original = { count: 0 };
const proxy = new Proxy(original, {
get(target, property) {
console.log(\`Reading \${property}: \${target\[property\]}\`);
return target\[property\];
},
set(target, property, value) {
console.log(\`Setting \${property} to \${value}\`);
target\[property\] = value;
return true;
}
});
proxy.count; *// Logs: \"Reading count: 0\"*
proxy.count = 5; *// Logs: \"Setting count to 5\"*How SimpliJS Uses Proxies
SimpliJS wraps your state objects in Proxies that track dependencies and trigger updates:
<div s-app>
<proxy-demo></proxy-demo>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'proxy-demo\', () => {
*// This creates a Proxy-wrapped reactive object*
const state = reactive({
count: 0,
user: {
name: \'Alice\',
preferences: {
theme: \'dark\'
}
}
});
*// Let\'s inspect what reactive() does*
console.log(\'Original state:\', { count: 0, user: { name: \'Alice\' }
});
console.log(\'Reactive state:\', state);
console.log(\'Is reactive state a Proxy?\', state instanceof Proxy);
return {
render: () => \`
<div style=\"padding: 20px;\">
<h2>Proxy Demo</h2>
<p>Count: \${state.count}</p>
<button onclick=\"this.closest(\'proxy-demo\').increment()\">
Increment
</button>
<p>Open the console to see Proxy operations</p>
</div>
\`,
increment: () => {
*// This operation is intercepted by the Proxy*
state.count++;
console.log(\'After increment, count:\', state.count);
}
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>Deep Reactivity
One of the most powerful features of SimpliJS's proxies is that they work deeply. Every nested object is also wrapped in a Proxy.
<div s-app>
<deep-proxy-demo></deep-proxy-demo>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'deep-proxy-demo\', () => {
const state = reactive({
user: {
profile: {
name: \'Bob\',
settings: {
theme: \'light\',
notifications: true,
privacy: {
showEmail: false,
shareData: true
}
}
},
posts: \[
{ id: 1, title: \'Deep Reactivity\' },
{ id: 2, title: \'Proxy Magic\' }
\]
}
});
*// Track changes at any depth*
const updateDeep = () => {
state.user.profile.settings.privacy.showEmail = true;
state.user.posts.push({ id: 3, title: \'New Post\' });
};
return {
render: () => \`
<div style=\"padding: 20px; max-width: 600px;\">
<h2>Deep Reactivity Demo</h2>
<div style=\"background: #f8f9fa; padding: 15px; border-radius:
8px;\">
<h3>User Settings (4 levels deep)</h3>
<p>Theme: \${state.user.profile.settings.theme}</p>
<p>Show Email: \${state.user.profile.settings.privacy.showEmail ?
\'Yes\' : \'No\'}</p>
<h4>Posts:</h4>
<ul>
\${state.user.posts.map(post => \`
<li>\${post.title}</li>
\`).join(\'\')}
</ul>
</div>
<div style=\"margin-top: 20px; display: flex; gap: 10px;\">
<button onclick=\"this.closest(\'deep-proxy-demo\').updateTheme()\"
style=\"padding: 8px 16px;\">
Toggle Theme
</button>
<button onclick=\"this.closest(\'deep-proxy-demo\').togglePrivacy()\"
style=\"padding: 8px 16px;\">
Toggle Privacy
</button>
<button onclick=\"this.closest(\'deep-proxy-demo\').addPost()\"
style=\"padding: 8px 16px;\">
Add Post
</button>
</div>
<div style=\"margin-top: 20px; font-size: 12px; color: #666;\">
<p><strong>Note:</strong> Changes at any depth trigger re-renders
automatically!</p>
</div>
</div>
\`,
updateTheme: () => {
state.user.profile.settings.theme =
state.user.profile.settings.theme === \'light\' ? \'dark\' : \'light\';
},
togglePrivacy: () => {
state.user.profile.settings.privacy.showEmail =
!state.user.profile.settings.privacy.showEmail;
},
addPost: () => {
state.user.posts.push({
id: state.user.posts.length + 1,
title: \`Post \${state.user.posts.length + 1}\`
});
}
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>10.2 Creating Reactive Objects
Now that we understand the theory, let's explore practical ways to create and use reactive objects.
Basic Reactive Creation
<div s-app>
<reactive-creation></reactive-creation>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'reactive-creation\', () => {
*// Different ways to create reactive state*
*// 1. Simple object*
const simple = reactive({
count: 0,
message: \'Hello\'
});
*// 2. Nested object*
const nested = reactive({
user: {
name: \'Alice\',
address: {
city: \'New York\',
zip: \'10001\'
}
}
});
*// 3. Array*
const list = reactive(\[\'Apple\', \'Banana\', \'Orange\'\]);
*// 4. Mixed types*
const complex = reactive({
id: 1,
tags: \[\'important\', \'featured\'\],
metadata: {
created: new Date(),
views: 0
}
});
return {
render: () => \`
<div style=\"padding: 20px; max-width: 600px;\">
<h2>Creating Reactive Objects</h2>
<div style=\"display: grid; gap: 20px;\">
<!-- Simple Object -->
<div style=\"background: #e3f2fd; padding: 15px; border-radius:
8px;\">
<h3>Simple Object</h3>
<p>Count: \${simple.count}</p>
<p>Message: \${simple.message}</p>
<button
onclick=\"this.closest(\'reactive-creation\').updateSimple()\">
Update
</button>
</div>
<!-- Nested Object -->
<div style=\"background: #d4edda; padding: 15px; border-radius:
8px;\">
<h3>Nested Object</h3>
<p>User: \${nested.user.name}</p>
<p>City: \${nested.user.address.city}</p>
<button
onclick=\"this.closest(\'reactive-creation\').updateNested()\">
Update Nested
</button>
</div>
<!-- Array -->
<div style=\"background: #fff3cd; padding: 15px; border-radius:
8px;\">
<h3>Array</h3>
<p>Items: \${list.join(\', \')}</p>
<button onclick=\"this.closest(\'reactive-creation\').updateArray()\">
Add Item
</button>
</div>
<!-- Complex -->
<div style=\"background: #f8d7da; padding: 15px; border-radius:
8px;\">
<h3>Complex</h3>
<p>ID: \${complex.id}</p>
<p>Tags: \${complex.tags.join(\', \')}</p>
<p>Views: \${complex.metadata.views}</p>
<button
onclick=\"this.closest(\'reactive-creation\').updateComplex()\">
Increment Views
</button>
</div>
</div>
</div>
\`,
updateSimple: () => {
simple.count++;
simple.message = simple.message === \'Hello\' ? \'World\' : \'Hello\';
},
updateNested: () => {
nested.user.name = nested.user.name === \'Alice\' ? \'Bob\' : \'Alice\';
nested.user.address.city = nested.user.address.city === \'New York\' ?
\'Boston\' : \'New York\';
},
updateArray: () => {
list.push(\`Item \${list.length + 1}\`);
},
updateComplex: () => {
complex.metadata.views++;
}
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>Reactive Arrays and Their Methods
Arrays created with reactive() have all their methods patched to trigger reactivity:
<div s-app>
<reactive-array></reactive-array>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'reactive-array\', () => {
const items = reactive(\[
{ id: 1, text: \'Item 1\', done: false },
{ id: 2, text: \'Item 2\', done: true },
{ id: 3, text: \'Item 3\', done: false }
\]);
const stats = reactive({
total: items.length,
completed: items.filter(i => i.done).length,
pending: items.filter(i => !i.done).length
});
*// Update stats whenever items change*
const updateStats = () => {
stats.total = items.length;
stats.completed = items.filter(i => i.done).length;
stats.pending = items.filter(i => !i.done).length;
};
return {
render: () => \`
<div style=\"padding: 20px; max-width: 500px;\">
<h2>Reactive Array Methods</h2>
<!-- Stats -->
<div style=\"display: flex; gap: 20px; margin-bottom: 20px;\">
<div style=\"flex:1; text-align: center; background: #007bff; color:
white; padding: 10px; border-radius: 4px;\">
Total: \${stats.total}
</div>
<div style=\"flex:1; text-align: center; background: #28a745; color:
white; padding: 10px; border-radius: 4px;\">
Completed: \${stats.completed}
</div>
<div style=\"flex:1; text-align: center; background: #dc3545; color:
white; padding: 10px; border-radius: 4px;\">
Pending: \${stats.pending}
</div>
</div>
<!-- Array Operations -->
<div style=\"display: grid; grid-template-columns: repeat(4, 1fr); gap:
10px; margin-bottom: 20px;\">
<button onclick=\"this.closest(\'reactive-array\').push()\"
style=\"padding: 8px;\">Push</button>
<button onclick=\"this.closest(\'reactive-array\').pop()\"
style=\"padding: 8px;\">Pop</button>
<button onclick=\"this.closest(\'reactive-array\').shift()\"
style=\"padding: 8px;\">Shift</button>
<button onclick=\"this.closest(\'reactive-array\').unshift()\"
style=\"padding: 8px;\">Unshift</button>
<button onclick=\"this.closest(\'reactive-array\').splice()\"
style=\"padding: 8px;\">Splice</button>
<button onclick=\"this.closest(\'reactive-array\').sort()\"
style=\"padding: 8px;\">Sort</button>
<button onclick=\"this.closest(\'reactive-array\').reverse()\"
style=\"padding: 8px;\">Reverse</button>
<button onclick=\"this.closest(\'reactive-array\').filter()\"
style=\"padding: 8px;\">Filter</button>
</div>
<!-- Items List -->
<div style=\"border: 1px solid #ddd; border-radius: 8px; overflow:
hidden;\">
\${items.map(item => \`
<div style=\"display: flex; align-items: center; padding: 10px;
border-bottom: 1px solid #eee;\">
<input type=\"checkbox\"
\${item.done ? \'checked\' : \'\'}
onchange=\"this.closest(\'reactive-array\').toggle(\${item.id})\"
style=\"margin-right: 10px;\">
<span style=\"flex:1; \${item.done ? \'text-decoration: line-through;
color: #999;\' : \'\'}\">
\${item.text}
</span>
<button
onclick=\"this.closest(\'reactive-array\').remove(\${item.id})\"
style=\"padding: 5px 10px; background: #dc3545; color: white; border:
none; border-radius: 4px;\">
×
</button>
</div>
\`).join(\'\')}
\${items.length === 0 ? \`
<div style=\"padding: 40px; text-align: center; color: #999;\">No items in the array
</div>
` : ''}
</div>
</div>
`,
// Array methods
push: () => {
items.push({
id: Date.now(),
text: `Item ${items.length + 1}`,
done: false
});
updateStats();
},
pop: () => {
items.pop();
updateStats();
},
shift: () => {
items.shift();
updateStats();
},
unshift: () => {
items.unshift({
id: Date.now(),
text: `New Item`,
done: false
});
updateStats();
},
splice: () => {
items.splice(1, 1);
updateStats();
},
sort: () => {
items.sort((a, b) => a.text.localeCompare(b.text));
updateStats();
},
reverse: () => {
items.reverse();
updateStats();
},
filter: () => {
// This reassigns the array, which works with reactivity
items.value = items.filter(i => !i.done);
updateStats();
},
toggle: (id) => {
const item = items.find(i => i.id === id);
if (item) {
item.done = !item.done;
updateStats();
}
},
remove: (id) => {
const index = items.findIndex(i => i.id === id);
if (index !== -1) {
items.splice(index, 1);
updateStats();
}
}
};
});
createApp().mount('[s-app]');
</script>
</div>
10.3 Computed Properties with computed()
While you can compute values directly in render methods, the computed() function provides lazy, cached computed values that only recalculate when dependencies change.
Basic Computed Properties
<div s-app>
<computed-demo></computed-demo>
<script type=\"module\">
import { createApp, component, reactive, computed } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'computed-demo\', () => {
const state = reactive({
firstName: \'John\',
lastName: \'Doe\',
price: 100,
quantity: 2,
taxRate: 0.08
});
*// Computed properties - these cache their values*
const fullName = computed(() => \`\${state.firstName}
\${state.lastName}\`);
const subtotal = computed(() => state.price * state.quantity);
const tax = computed(() => subtotal.value * state.taxRate);
const total = computed(() => subtotal.value + tax.value);
const greeting = computed(() => {
const hour = new Date().getHours();
const timeGreeting = hour < 12 ? \'Good morning\' :
hour < 18 ? \'Good afternoon\' : \'Good evening\';
return \`\${timeGreeting}, \${fullName.value}!\`;
});
return {
render: () => \`
<div style=\"padding: 20px; max-width: 400px;\">
<h2>Computed Properties Demo</h2>
<!-- Personal Info -->
<div style=\"background: #e3f2fd; padding: 15px; border-radius: 8px;
margin-bottom: 20px;\">
<h3>Personal Info</h3>
<p>\${greeting.value}</p>
<div style=\"margin: 10px 0;\">
<input type=\"text\"
value=\"\${state.firstName}\"
oninput=\"this.closest(\'computed-demo\').updateFirstName(this.value)\"
placeholder=\"First name\"
style=\"width: 100%; padding: 5px; margin: 5px 0;\">
<input type=\"text\"
value=\"\${state.lastName}\"
oninput=\"this.closest(\'computed-demo\').updateLastName(this.value)\"
placeholder=\"Last name\"
style=\"width: 100%; padding: 5px; margin: 5px 0;\">
</div>
<p><strong>Full name (computed):</strong> \${fullName.value}</p>
</div>
<!-- Shopping Cart -->
<div style=\"background: #d4edda; padding: 15px; border-radius:
8px;\">
<h3>Shopping Cart</h3>
<div style=\"margin: 10px 0;\">
<label>Price: \$\${state.price}</label>
<input type=\"range\"
value=\"\${state.price}\"
oninput=\"this.closest(\'computed-demo\').updatePrice(this.value)\"
min=\"0\" max=\"200\" step=\"1\"
style=\"width: 100%;\">
</div>
<div style=\"margin: 10px 0;\">
<label>Quantity: \${state.quantity}</label>
<input type=\"range\"
value=\"\${state.quantity}\"
oninput=\"this.closest(\'computed-demo\').updateQuantity(this.value)\"
min=\"1\" max=\"10\" step=\"1\"
style=\"width: 100%;\">
</div>
<div style=\"margin: 10px 0;\">
<label>Tax Rate: \${(state.taxRate * 100).toFixed(0)}%</label>
<input type=\"range\"
value=\"\${state.taxRate}\"
oninput=\"this.closest(\'computed-demo\').updateTaxRate(this.value)\"
min=\"0\" max=\"0.15\" step=\"0.01\"
style=\"width: 100%;\">
</div>
<hr>
<p><strong>Subtotal (computed):</strong>
\$\${subtotal.value.toFixed(2)}</p>
<p><strong>Tax (computed):</strong>
\$\${tax.value.toFixed(2)}</p>
<p><strong>Total (computed):</strong>
\$\${total.value.toFixed(2)}</p>
</div>
<div style=\"margin-top: 20px; font-size: 12px; color: #666;\">
<p>Computed values cache results and only recalculate when
dependencies change.</p>
</div>
</div>
\`,
updateFirstName: (value) => { state.firstName = value; },
updateLastName: (value) => { state.lastName = value; },
updatePrice: (value) => { state.price = parseFloat(value); },
updateQuantity: (value) => { state.quantity = parseInt(value); },
updateTaxRate: (value) => { state.taxRate = parseFloat(value); }
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>Computed with Multiple Dependencies
<div s-app>
<shopping-cart></shopping-cart>
<script type=\"module\">
import { createApp, component, reactive, computed } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'shopping-cart\', () => {
const state = reactive({
items: \[
{ id: 1, name: \'Laptop\', price: 999, quantity: 1 },
{ id: 2, name: \'Mouse\', price: 49, quantity: 2 },
{ id: 3, name: \'Keyboard\', price: 129, quantity: 1 }
\],
discountCode: \'\',
shippingMethod: \'standard\'
});
*// Complex computed properties*
const subtotal = computed(() =>
state.items.reduce((sum, item) => sum + (item.price * item.quantity),
0)
);
const itemCount = computed(() =>
state.items.reduce((sum, item) => sum + item.quantity, 0)
);
const discount = computed(() => {
*// Apply discount based on code*
if (state.discountCode === \'SAVE10\') return subtotal.value * 0.1;
if (state.discountCode === \'SAVE20\') return subtotal.value * 0.2;
if (subtotal.value > 1000) return subtotal.value * 0.05; *// 5% for
orders over \$1000*
return 0;
});
const shipping = computed(() => {
switch(state.shippingMethod) {
case \'express\': return 15;
case \'next-day\': return 25;
default: return 5;
}
});
const tax = computed(() => (subtotal.value - discount.value) * 0.08);
const total = computed(() =>
subtotal.value - discount.value + shipping.value + tax.value
);
const summary = computed(() => ({
subtotal: subtotal.value,
discount: discount.value,
shipping: shipping.value,
tax: tax.value,
total: total.value,
itemCount: itemCount.value
}));
return {
render: () => \`
<div style=\"max-width: 600px; margin: 20px auto; padding: 20px;
background: white; border-radius: 12px; box-shadow: 0 2px 4px
rgba(0,0,0,0.1);\">
<h2>Shopping Cart</h2>
<!-- Cart Items -->
<div style=\"margin: 20px 0;\">
\${state.items.map(item => \`
<div style=\"display: flex; align-items: center; padding: 10px;
border-bottom: 1px solid #eee;\">
<div style=\"flex:2;\">\${item.name}</div>
<div style=\"flex:1;\">\$\${item.price}</div>
<div style=\"flex:1;\">
<input type=\"number\"
value=\"\${item.quantity}\"
min=\"1\"
onchange=\"this.closest(\'shopping-cart\').updateQuantity(\${item.id},
this.value)\"
style=\"width: 60px; padding: 5px;\">
</div>
<div style=\"flex:1; font-weight: bold;\">
\$\${(item.price * item.quantity).toFixed(2)}
</div>
<button
onclick=\"this.closest(\'shopping-cart\').removeItem(\${item.id})\"
style=\"padding: 5px 10px; background: #dc3545; color: white; border:
none; border-radius: 4px;\">
×
</button>
</div>
\`).join(\'\')}
</div>
<!-- Cart Summary -->
<div style=\"background: #f8f9fa; padding: 20px; border-radius:
8px;\">
<h3>Order Summary</h3>
<div style=\"margin: 10px 0;\">
<label>Discount Code:</label>
<input type=\"text\"
value=\"\${state.discountCode}\"
oninput=\"this.closest(\'shopping-cart\').updateDiscountCode(this.value)\"
placeholder=\"Enter code (SAVE10, SAVE20)\"
style=\"width: 100%; padding: 8px; margin: 5px 0;\">
</div>
<div style=\"margin: 10px 0;\">
<label>Shipping Method:</label>
<select
onchange=\"this.closest(\'shopping-cart\').updateShipping(this.value)\"
style=\"width: 100%; padding: 8px;\">
<option value=\"standard\" \${state.shippingMethod === \'standard\' ?
\'selected\' : \'\'}>Standard (\$5)</option>
<option value=\"express\" \${state.shippingMethod === \'express\' ?
\'selected\' : \'\'}>Express (\$15)</option>
<option value=\"next-day\" \${state.shippingMethod === \'next-day\' ?
\'selected\' : \'\'}>Next Day (\$25)</option>
</select>
</div>
<hr>
<div style=\"display: grid; gap: 10px;\">
<div style=\"display: flex; justify-content: space-between;\">
<span>Subtotal ({itemCount.value} items):</span>
<span>\$\${subtotal.value.toFixed(2)}</span>
</div>
<div style=\"display: flex; justify-content: space-between; color:#28a745;">
<span>Discount:</span>
<span>-$${discount.value.toFixed(2)}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>Shipping:</span>
<span>$${shipping.value.toFixed(2)}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>Tax (8%):</span>
<span>$${tax.value.toFixed(2)}</span>
</div>
<div style="display: flex; justify-content: space-between; font-size: 20px; font-weight: bold; border-top: 2px solid #333; padding-top: 10px;">
<span>Total:</span>
<span>$${total.value.toFixed(2)}</span>
</div>
</div>
</div>
<!-- JSON Summary -->
<div style="margin-top: 20px;">
<h4>Computed Summary Object:</h4>
<pre style="background: #f8f9fa; padding: 10px; border-radius: 4px; overflow-x: auto;">
${JSON.stringify(summary.value, null, 2)}
</pre>
</div>
</div>
`,
updateQuantity: (id, quantity) => {
const item = state.items.find(i => i.id === id);
if (item) item.quantity = parseInt(quantity);
},
removeItem: (id) => {
state.items = state.items.filter(i => i.id !== id);
},
updateDiscountCode: (code) => {
state.discountCode = code.toUpperCase();
},
updateShipping: (method) => {
state.shippingMethod = method;
}
};
});
createApp().mount('[s-app]');
</script>
</div>
10.4 Watchers with watch()
The watch() function lets you react to changes in reactive state, perfect for side effects like API calls, localStorage persistence, or analytics.
Basic Watch Examples
<div s-app>
<watch-demo></watch-demo>
<script type=\"module\">
import { createApp, component, reactive, watch } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'watch-demo\', () => {
const state = reactive({
username: \'\',
email: \'\',
age: 25,
preferences: {
theme: \'light\',
notifications: true
}
});
const logs = reactive(\[\]);
*// Watch a single property*
watch(() => state.username, (newVal, oldVal) => {
logs.push({
type: \'username\',
message: \`Username changed from \"\${oldVal}\" to \"\${newVal}\"\`,
timestamp: new Date().toLocaleTimeString()
});
*// Simulate API call to check username availability*
if (newVal.length > 3) {
setTimeout(() => {
logs.push({
type: \'api\',
message: \`Username \"\${newVal}\" is available!\`,
timestamp: new Date().toLocaleTimeString()
});
}, 500);
}
});
*// Watch email with validation*
watch(() => state.email, (newVal, oldVal) => {
const isValid = newVal.includes(\'@\') && newVal.includes(\'.\');
logs.push({
type: \'email\',
message: \`Email \"\${newVal}\" is \${isValid ? \'valid\' :
\'invalid\'}\`,
timestamp: new Date().toLocaleTimeString()
});
});
*// Watch multiple values using a computed-like function*
watch(() => \`\${state.username}:\${state.email}\`, () => {
logs.push({
type: \'combined\',
message: \`Profile updated: \${state.username} (\${state.email})\`,
timestamp: new Date().toLocaleTimeString()
});
});
*// Watch nested property*
watch(() => state.preferences.theme, (newVal, oldVal) => {
logs.push({
type: \'theme\',
message: \`Theme changed from \${oldVal} to \${newVal}\`,
timestamp: new Date().toLocaleTimeString()
});
*// Apply theme to body*
document.body.style.backgroundColor = newVal === \'dark\' ? \'#333\' :
\'#f8f9fa\';
document.body.style.color = newVal === \'dark\' ? \'white\' : \'#333\';
});
*// Watch with immediate execution*
watch(() => state.age, (newVal, oldVal) => {
const category = newVal < 18 ? \'minor\' : newVal < 65 ? \'adult\' :
\'senior\';
logs.push({
type: \'age\',
message: \`Age \${newVal} (\${category})\`,
timestamp: new Date().toLocaleTimeString()
});
}, { immediate: true }); *// Runs immediately with initial value*
return {
render: () => \`
<div style=\"max-width: 600px; margin: 20px auto; padding: 20px;
background: white; border-radius: 12px; box-shadow: 0 2px 4px
rgba(0,0,0,0.1);\">
<h2>Watch Demo</h2>
<div style=\"display: grid; gap: 20px;\">
<!-- Inputs -->
<div style=\"background: #f8f9fa; padding: 15px; border-radius:
8px;\">
<h3>Watch Triggers</h3>
<div style=\"margin: 10px 0;\">
<label>Username:</label>
<input type=\"text\"
value=\"\${state.username}\"
oninput=\"this.closest(\'watch-demo\').updateUsername(this.value)\"
placeholder=\"Enter username\"
style=\"width: 100%; padding: 8px;\">
</div>
<div style=\"margin: 10px 0;\">
<label>Email:</label>
<input type=\"email\"
value=\"\${state.email}\"
oninput=\"this.closest(\'watch-demo\').updateEmail(this.value)\"
placeholder=\"Enter email\"
style=\"width: 100%; padding: 8px;\">
</div>
<div style=\"margin: 10px 0;\">
<label>Age: \${state.age}</label>
<input type=\"range\"
value=\"\${state.age}\"
oninput=\"this.closest(\'watch-demo\').updateAge(this.value)\"
min=\"0\" max=\"100\"
style=\"width: 100%;\">
</div>
<div style=\"margin: 10px 0;\">
<label>Theme:</label>
<select
onchange=\"this.closest(\'watch-demo\').updateTheme(this.value)\"
style=\"width: 100%; padding: 8px;\">
<option value=\"light\" \${state.preferences.theme === \'light\' ?
\'selected\' : \'\'}>Light</option>
<option value=\"dark\" \${state.preferences.theme === \'dark\' ?
\'selected\' : \'\'}>Dark</option>
</select>
</div>
</div>
<!-- Watch Logs -->
<div style=\"background: #333; color: #0f0; padding: 15px;
border-radius: 8px; font-family: monospace;\">
<h3 style=\"color: #0f0; margin-top: 0;\">Watch Logs</h3>
<div style=\"max-height: 300px; overflow-y: auto;\">
\${logs.slice().reverse().map(log => \`
<div style=\"margin: 5px 0; font-size: 12px;\">
\[\${log.timestamp}\] \${log.message}
</div>
\`).join(\'\')}
\${logs.length === 0 ? \`
<div style=\"color: #666;\">No logs yet. Change some values!</div>
\` : \'\'}
</div>
</div>
</div>
</div>
\`,
updateUsername: (value) => { state.username = value; },
updateEmail: (value) => { state.email = value; },
updateAge: (value) => { state.age = parseInt(value); },
updateTheme: (value) => { state.preferences.theme = value; }
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>Practical Watch Examples
<div s-app>
<practical-watch></practical-watch>
<script type=\"module\">
import { createApp, component, reactive, watch } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'practical-watch\', () => {
const state = reactive({
searchQuery: \'\',
results: \[\],
filters: {
category: \'all\',
priceRange: \[0, 1000\],
inStock: false
},
sortBy: \'relevance\',
currentPage: 1
});
const ui = reactive({
isLoading: false,
error: null,
lastSearch: null,
searchCount: 0
});
*// Debounced search*
let searchTimeout;
watch(() => state.searchQuery, (newQuery) => {
ui.isLoading = true;
ui.error = null;
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
*// Simulate API call*
ui.isLoading = false;
ui.lastSearch = newQuery;
ui.searchCount++;
*// Generate mock results*
state.results = Array.from({ length: 5 }, (_, i) => ({
id: i,
title: \`Result \${i + 1} for \"\${newQuery}\"\`,
price: Math.floor(Math.random() * 500) + 50,
category: \[\'Electronics\', \'Books\',
\'Clothing\'\]\[Math.floor(Math.random() * 3)\],
inStock: Math.random() > 0.3
}));
if (newQuery.length > 0 && newQuery.length < 3) {
ui.error = \'Search query must be at least 3 characters\';
}
}, 500);
});
*// Watch filters to update results*
watch(() => JSON.stringify(state.filters), () => {
console.log(\'Filters changed, would update results\', state.filters);
*// In real app, would refetch with filters*
});
*// Auto-save to localStorage*
watch(() => JSON.stringify(state), (newState) => {
localStorage.setItem(\'search-preferences\', newState);
console.log(\'Saved to localStorage\');
}, { debounce: 1000 }); *// Debounce for performance*
*// Track search analytics*
watch(() => state.searchQuery, (newVal, oldVal) => {
if (newVal.length > 2) {
*// Send to analytics*
console.log(\'Analytics: Search performed\', newVal);
}
});
return {
render: () => \`
<div style=\"max-width: 600px; margin: 20px auto; padding: 20px;
background: white; border-radius: 12px; box-shadow: 0 2px 4px
rgba(0,0,0,0.1);\">
<h2>Practical Watch Examples</h2>
<!-- Search Box -->
<div style=\"margin: 20px 0;\">
<input type=\"text\"
value=\"\${state.searchQuery}\"
oninput=\"this.closest(\'practical-watch\').updateSearch(this.value)\"
placeholder=\"Search products\...\"
style=\"width: 100%; padding: 12px; border: 2px solid #ddd;
border-radius: 8px; font-size: 16px;\">
\${ui.isLoading ? \`
<div style=\"margin-top: 10px; color: #007bff;\">Searching\...</div>
\` : \'\'}
\${ui.error ? \`
<div style=\"margin-top: 10px; color: #dc3545;\">\${ui.error}</div>
\` : \'\'}
\${ui.lastSearch ? \`
<div style=\"margin-top: 5px; font-size: 12px; color: #666;\">
Last search: \"\${ui.lastSearch}\" (\${ui.searchCount} searches)
</div>
\` : \'\'}
</div>
<!-- Filters -->
<div style=\"background: #f8f9fa; padding: 15px; border-radius: 8px;
margin: 20px 0;\">
<h3>Filters</h3>
<div style=\"margin: 10px 0;\">
<label>Category:</label>
<select
onchange=\"this.closest(\'practical-watch\').updateCategory(this.value)\"
style=\"width: 100%; padding: 8px;\">
<option value=\"all\" \${state.filters.category === \'all\' ?
\'selected\' : \'\'}>All</option>
<option value=\"Electronics\" \${state.filters.category ===
\'Electronics\' ? \'selected\' : \'\'}>Electronics</option>
<option value=\"Books\" \${state.filters.category === \'Books\' ?
\'selected\' : \'\'}>Books</option>
<option value=\"Clothing\" \${state.filters.category === \'Clothing\' ?
\'selected\' : \'\'}>Clothing</option>
</select>
</div>
<div style=\"margin: 10px 0;\">
<label>Price Range: \$\${state.filters.priceRange\[0\]} -
\$\${state.filters.priceRange\[1\]}</label>
<input type=\"range\"
value=\"\${state.filters.priceRange\[0\]}\"
oninput=\"this.closest(\'practical-watch\').updatePriceMin(this.value)\"
min=\"0\" max=\"1000\"
style=\"width: 100%;\">
<input type=\"range\"
value=\"\${state.filters.priceRange\[1\]}\"
oninput=\"this.closest(\'practical-watch\').updatePriceMax(this.value)\"
min=\"0\" max=\"1000\"
style=\"width: 100%;\">
</div>
<div style=\"margin: 10px 0;\">
<label>
<input type=\"checkbox\"
\${state.filters.inStock ? \'checked\' : \'\'}
onchange=\"this.closest(\'practical-watch\').toggleInStock()\">In Stock Only
</label>
</div>
</div>
<!-- Results -->
<div style="margin: 20px 0;">
<h3>Results (${state.results.length})</h3>
${state.results.map(result => `
<div style="padding: 15px; margin: 10px 0; border: 1px solid #ddd; border-radius: 8px;">
<h4 style="margin: 0 0 5px;">${result.title}</h4>
<div style="display: flex; gap: 20px; color: #666; font-size: 14px;">
<span>$${result.price}</span>
<span>${result.category}</span>
<span style="color: ${result.inStock ? '#28a745' : '#dc3545'}">
${result.inStock ? 'In Stock' : 'Out of Stock'}
</span>
</div>
</div>
`).join('')}
${state.results.length === 0 && state.searchQuery && !ui.isLoading ? `
<div style="text-align: center; padding: 40px; color: #666;">
No results found for "${state.searchQuery}"
</div>
` : ''}
</div>
<div style="margin-top: 20px; font-size: 12px; color: #666;">
<p>Watch is used for:</p>
<ul>
<li>Debounced search (500ms delay)</li>
<li>Auto-save filters to localStorage</li>
<li>Analytics tracking</li>
<li>Error validation</li>
</ul>
</div>
</div>
`,
updateSearch: (value) => { state.searchQuery = value; },
updateCategory: (value) => { state.filters.category = value; },
updatePriceMin: (value) => {
state.filters.priceRange[0] = parseInt(value);
if (state.filters.priceRange[0] > state.filters.priceRange[1]) {
state.filters.priceRange[1] = state.filters.priceRange[0];
}
},
updatePriceMax: (value) => {
state.filters.priceRange[1] = parseInt(value);
if (state.filters.priceRange[1] < state.filters.priceRange[0]) {
state.filters.priceRange[0] = state.filters.priceRange[1];
}
},
toggleInStock: () => { state.filters.inStock = !state.filters.inStock; }
};
});
createApp().mount('[s-app]');
</script>
</div>
10.5 Advanced Reactive Patterns
Now let's explore some advanced patterns for working with reactive state.
Reactive State Factories
<div s-app>
<state-factory></state-factory>
<script type=\"module\">
import { createApp, component, reactive, computed, watch } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// Factory function for creating todo state*
function createTodoState(initialTodos = \[\]) {
const state = reactive({
todos: initialTodos,
filter: \'all\',
searchTerm: \'\'
});
*// Computed properties*
const filteredTodos = computed(() => {
return state.todos
.filter(todo => {
if (state.filter === \'active\') return !todo.completed;
if (state.filter === \'completed\') return todo.completed;
return true;
})
.filter(todo =>
todo.text.toLowerCase().includes(state.searchTerm.toLowerCase())
);
});
const stats = computed(() => ({
total: state.todos.length,
completed: state.todos.filter(t => t.completed).length,
pending: state.todos.filter(t => !t.completed).length
}));
*// Actions*
const actions = {
addTodo(text) {
state.todos.push({
id: Date.now(),
text,
completed: false,
createdAt: new Date()
});
},
toggleTodo(id) {
const todo = state.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
},
removeTodo(id) {
state.todos = state.todos.filter(t => t.id !== id);
},
setFilter(filter) {
state.filter = filter;
},
setSearchTerm(term) {
state.searchTerm = term;
},
clearCompleted() {
state.todos = state.todos.filter(t => !t.completed);
}
};
*// Auto-save to localStorage*
watch(() => JSON.stringify(state.todos), (todosJson) => {
localStorage.setItem(\'todos\', todosJson);
});
return {
state,
filteredTodos,
stats,
\...actions
};
}
component(\'state-factory\', () => {
*// Create multiple independent todo lists using the factory*
const workTodos = createTodoState(\[
{ id: 1, text: \'Complete project\', completed: false },
{ id: 2, text: \'Review code\', completed: true },
{ id: 3, text: \'Update documentation\', completed: false }
\]);
const personalTodos = createTodoState(\[
{ id: 1, text: \'Buy groceries\', completed: false },
{ id: 2, text: \'Call mom\', completed: false },
{ id: 3, text: \'Workout\', completed: true }
\]);
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto;\">
<h2>Reactive State Factory</h2>
<div style=\"display: grid; grid-template-columns: 1fr 1fr; gap:
20px;\">
<!-- Work Todos -->
<div style=\"background: #f8f9fa; padding: 20px; border-radius:
8px;\">
<h3 style=\"color: #007bff;\">Work Todos</h3>
<div style=\"display: flex; gap: 10px; margin: 10px 0;\">
<input type=\"text\"
id=\"workInput\"
placeholder=\"New work todo\...\"
style=\"flex:1; padding: 8px;\">
<button onclick=\"document.getElementById(\'workInput\').value &&
this.closest(\'state-factory\').addWorkTodo(document.getElementById(\'workInput\').value)\"
style=\"padding: 8px 16px; background: #007bff; color: white; border:
none; border-radius: 4px;\">
Add
</button>
</div>
<div style=\"margin: 10px 0; font-size: 14px; color: #666;\">
Stats: \${workTodos.stats.value.total} total,
\${workTodos.stats.value.completed} completed,
\${workTodos.stats.value.pending} pending
</div>
\${workTodos.filteredTodos.value.map(todo => \`
<div style=\"display: flex; align-items: center; padding: 8px;
border-bottom: 1px solid #eee;\">
<input type=\"checkbox\"
\${todo.completed ? \'checked\' : \'\'}
onchange=\"this.closest(\'state-factory\').toggleWorkTodo(\${todo.id})\"
style=\"margin-right: 10px;\">
<span style=\"flex:1; \${todo.completed ? \'text-decoration:
line-through; color: #999;\' : \'\'}\">
\${todo.text}
</span>
<button
onclick=\"this.closest(\'state-factory\').removeWorkTodo(\${todo.id})\"
style=\"padding: 3px 8px; background: #dc3545; color: white; border:
none; border-radius: 4px;\">
×
</button>
</div>
\`).join(\'\')}
</div>
<!-- Personal Todos -->
<div style=\"background: #f8f9fa; padding: 20px; border-radius:
8px;\">
<h3 style=\"color: #28a745;\">Personal Todos</h3>
<div style=\"display: flex; gap: 10px; margin: 10px 0;\">
<input type=\"text\"
id=\"personalInput\"
placeholder=\"New personal todo\...\"
style=\"flex:1; padding: 8px;\">
<button onclick=\"document.getElementById(\'personalInput\').value &&
this.closest(\'state-factory\').addPersonalTodo(document.getElementById(\'personalInput\').value)\"
style=\"padding: 8px 16px; background: #28a745; color: white; border:
none; border-radius: 4px;\">
Add
</button>
</div>
<div style=\"margin: 10px 0; font-size: 14px; color: #666;\">
Stats: \${personalTodos.stats.value.total} total,
\${personalTodos.stats.value.completed} completed,
\${personalTodos.stats.value.pending} pending
</div>
\${personalTodos.filteredTodos.value.map(todo => \`
<div style=\"display: flex; align-items: center; padding: 8px;
border-bottom: 1px solid #eee;\">
<input type=\"checkbox\"
\${todo.completed ? \'checked\' : \'\'}
onchange=\"this.closest(\'state-factory\').togglePersonalTodo(\${todo.id})\"
style=\"margin-right: 10px;\">
<span style=\"flex:1; \${todo.completed ? \'text-decoration:
line-through; color: #999;\' : \'\'}\">
\${todo.text}
</span>
<button
onclick=\"this.closest(\'state-factory\').removePersonalTodo(\${todo.id})\"
style=\"padding: 3px 8px; background: #dc3545; color: white; border:
none; border-radius: 4px;\">
×
</button>
</div>
\`).join(\'\')}
</div>
</div>
</div>
\`,
*// Work todo methods*
addWorkTodo: (text) => {
workTodos.addTodo(text);
document.getElementById(\'workInput\').value = \'\';
},
toggleWorkTodo: (id) => workTodos.toggleTodo(id),
removeWorkTodo: (id) => workTodos.removeTodo(id),
*// Personal todo methods*
addPersonalTodo: (text) => {
personalTodos.addTodo(text);
document.getElementById(\'personalInput\').value = \'\';
},
togglePersonalTodo: (id) => personalTodos.toggleTodo(id),
removePersonalTodo: (id) => personalTodos.removeTodo(id)
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>State Composition
<div s-app>
<state-composition></state-composition>
<script type=\"module\">
import { createApp, component, reactive, computed } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// User module*
function createUserModule() {
const state = reactive({
currentUser: null,
preferences: {
theme: \'light\',
language: \'en\',
notifications: true
}
});
return {
user: state,
login(username) {
state.currentUser = {
id: Date.now(),
username,
role: \'user\',
lastLogin: new Date()
};
},
logout() {
state.currentUser = null;
},
updatePreferences(prefs) {
Object.assign(state.preferences, prefs);
}
};
}
*// Cart module*
function createCartModule() {
const state = reactive({
items: \[\],
couponCode: null
});
const totals = computed(() => {
const subtotal = state.items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
const discount = state.couponCode === \'SAVE10\' ? subtotal * 0.1 : 0;
return {
subtotal,
discount,
total: subtotal - discount
};
});
return {
cart: state,
totals,
addItem(product) {
const existing = state.items.find(i => i.id === product.id);
if (existing) {
existing.quantity++;
} else {
state.items.push({ \...product, quantity: 1 });
}
},
removeItem(productId) {
state.items = state.items.filter(i => i.id !== productId);
},
updateQuantity(productId, quantity) {
const item = state.items.find(i => i.id === productId);
if (item) {
item.quantity = Math.max(1, quantity);
}
},
applyCoupon(code) {
state.couponCode = code;
}
};
}
*// Combine modules into app state*
component(\'state-composition\', () => {
const user = createUserModule();
const cart = createCartModule();
*// Sample products*
const products = reactive(\[
{ id: 1, name: \'Laptop\', price: 999 },
{ id: 2, name: \'Mouse\', price: 49 },
{ id: 3, name: \'Keyboard\', price: 129 },
{ id: 4, name: \'Monitor\', price: 299 }
\]);
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto;\">
<h2>State Composition</h2>
<!-- User Section -->
<div style=\"background: #e3f2fd; padding: 20px; border-radius: 8px;
margin-bottom: 20px;\">
<h3>User Module</h3>
\${!user.user.currentUser ? \`
<div>
<input type=\"text\" id=\"username\" placeholder=\"Enter username\"
style=\"padding: 8px; margin-right: 10px;\">
<button onclick=\"const username =
document.getElementById(\'username\').value;
if(username) this.closest(\'state-composition\').login(username)\"
style=\"padding: 8px 16px; background: #007bff; color: white; border:
none; border-radius: 4px;\">
Login
</button>
</div>
\` : \`
<div style=\"display: flex; justify-content: space-between;
align-items: center;\">
<div>
<strong>Logged in as:</strong> \${user.user.currentUser.username}
<br>
<small>Last login: \${new
Date(user.user.currentUser.lastLogin).toLocaleString()}</small>
</div>
<button onclick=\"this.closest(\'state-composition\').logout()\"
style=\"padding: 5px 10px; background: #dc3545; color: white; border:
none; border-radius: 4px;\">
Logout
</button>
</div>
\`}
</div>
<!-- Products Section -->
<div style=\"background: #d4edda; padding: 20px; border-radius: 8px;
margin-bottom: 20px;\">
<h3>Products</h3>
<div style=\"display: grid; grid-template-columns: repeat(2, 1fr); gap:
10px;\">
\${products.map(product => \`
<div style=\"display: flex; justify-content: space-between;
align-items: center; padding: 10px; background: white; border-radius:
4px;\">
<div>
<strong>\${product.name}</strong>
<br>
<span>\$\${product.price}</span>
</div>
<button
onclick=\"this.closest(\'state-composition\').addToCart(\${product.id})\"
style=\"padding: 5px 10px; background: #28a745; color: white; border:
none; border-radius: 4px;\">Add to Cart
</button>
</div>
`).join('')}
</div>
</div>
<!-- Cart Section -->
<div style="background: #fff3cd; padding: 20px; border-radius: 8px;">
<h3>Shopping Cart</h3>
${cart.cart.items.length === 0 ? `
<p style="color: #666; text-align: center;">Cart is empty</p>
` : `
<div style="margin: 10px 0;">
${cart.cart.items.map(item => `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 10px; border-bottom: 1px solid #ddd;">
<div style="flex:2;">${item.name}</div>
<div style="flex:1;">$${item.price}</div>
<div style="flex:1;">
<input type="number"
value="${item.quantity}"
min="1"
onchange="this.closest('state-composition').updateQuantity(${item.id}, this.value)"
style="width: 60px; padding: 5px;">
</div>
<div style="flex:1; font-weight: bold;">
$${(item.price * item.quantity).toFixed(2)}
</div>
<button onclick="this.closest('state-composition').removeFromCart(${item.id})"
style="padding: 3px 8px; background: #dc3545; color: white; border: none; border-radius: 4px;">
×
</button>
</div>
`).join('')}
</div>
<div style="margin-top: 20px; text-align: right;">
<div>Subtotal: $${cart.totals.value.subtotal.toFixed(2)}</div>
${cart.cart.couponCode ? `
<div style="color: #28a745;">
Discount: -$${cart.totals.value.discount.toFixed(2)}
(Code: ${cart.cart.couponCode})
</div>
` : ''}
<div style="font-size: 20px; font-weight: bold;">
Total: $${cart.totals.value.total.toFixed(2)}
</div>
</div>
<div style="margin-top: 20px;">
<input type="text" id="couponCode" placeholder="Enter coupon code"
style="padding: 8px; margin-right: 10px;">
<button onclick="this.closest('state-composition').applyCoupon(document.getElementById('couponCode').value)"
style="padding: 8px 16px; background: #ffc107; color: #333; border: none; border-radius: 4px;">
Apply Coupon
</button>
</div>
`}
</div>
</div>
`,
// User methods
login: (username) => user.login(username),
logout: () => user.logout(),
// Cart methods
addToCart: (productId) => {
const product = products.find(p => p.id === productId);
if (product) cart.addItem(product);
},
removeFromCart: (productId) => cart.removeItem(productId),
updateQuantity: (productId, quantity) => cart.updateQuantity(productId, parseInt(quantity)),
applyCoupon: (code) => cart.applyCoupon(code)
};
});
createApp().mount('[s-app]');
</script>
</div>
Chapter 10 Summary
You've now mastered programmatic reactive state in SimpliJS:
Proxy-based reactivity and how it works under the hood
Creating reactive objects with the reactive() function
Deep reactivity for nested objects and arrays
Array methods and how they trigger updates
Computed properties with computed() for cached derived values
Watchers with watch() for side effects and reactions
Advanced patterns including state factories and composition
Practical applications like search debouncing, localStorage persistence, and analytics
You've seen how reactive(), computed(), and watch() form a powerful trio for managing application state. These tools give you fine-grained control over reactivity while maintaining the simplicity that makes SimpliJS special.
In the next chapter, we'll explore component logic in depth, including methods, events, and refs for DOM access.
End of Chapter 10
Chapter 11: Component Logic: Methods, Events, and Refs
Welcome to Chapter 11, where we explore the full power of component logic in SimpliJS. While HTML-First development is great for simple interactions, JavaScript components give you complete control over behavior, DOM access, and complex logic. This chapter will transform you from a component user into a component architect.
11.1 Component Methods Deep Dive
Methods are the behavior of your components. They encapsulate logic, respond to user actions, and manipulate state. Let's explore methods in depth.
Method Fundamentals
<!DOCTYPE html>
<html>
<head>
<title>Component Methods</title>
<style>
.method-demo {
max-width: 600px;
margin: 20px auto;
font-family: Arial, sans-serif;
}
.counter-card {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
text-align: center;
}
.counter-value {
font-size: 72px;
font-weight: bold;
color: #007bff;
margin: 20px 0;
}
.button-group {
display: flex;
gap: 10px;
justify-content: center;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-primary:hover {
background: #0056b3;
transform: translateY(-2px);
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover {
background: #218838;
}
.btn-warning {
background: #ffc107;
color: #333;
}
.btn-danger {
background: #dc3545;
color: white;
}
.method-log {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
max-height: 200px;
overflow-y: auto;
font-family: monospace;
}
</style>
</head>
<body>
<method-fundamentals></method-fundamentals>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'method-fundamentals\', () => {
const state = reactive({
count: 0,
lastAction: null,
actionHistory: \[\]
});
*// Simple methods*
const increment = () => {
state.count++;
logAction(\'increment\');
};
const decrement = () => {
state.count--;
logAction(\'decrement\');
};
const reset = () => {
state.count = 0;
logAction(\'reset\');
};
*// Methods with parameters*
const add = (amount) => {
state.count += amount;
logAction(\`add \${amount}\`);
};
const multiply = (factor) => {
state.count *= factor;
logAction(\`multiply by \${factor}\`);
};
*// Methods with conditional logic*
const safeDecrement = () => {
if (state.count > 0) {
state.count--;
logAction(\'safe decrement\');
} else {
logAction(\'decrement prevented - already zero\');
}
};
*// Methods returning values*
const getCount = () => state.count;
const isEven = () => state.count % 2 === 0;
const getParity = () => isEven() ? \'even\' : \'odd\';
*// Helper method*
const logAction = (action) => {
state.lastAction = action;
state.actionHistory.unshift({
action,
value: state.count,
timestamp: new Date().toLocaleTimeString()
});
if (state.actionHistory.length > 10) state.actionHistory.pop();
};
return {
render: () => \`
<div class=\"method-demo\">
<div class=\"counter-card\">
<h2>Method Fundamentals</h2>
<div class=\"counter-value\">\${state.count}</div>
<div class=\"button-group\">
<button class=\"btn btn-primary\"
onclick=\"this.closest(\'method-fundamentals\').increment()\">
+1
</button>
<button class=\"btn btn-primary\"
onclick=\"this.closest(\'method-fundamentals\').add(5)\">
+5
</button>
<button class=\"btn btn-success\"
onclick=\"this.closest(\'method-fundamentals\').multiply(2)\">
×2
</button>
</div>
<div class=\"button-group\" style=\"margin-top: 10px;\">
<button class=\"btn btn-warning\"
onclick=\"this.closest(\'method-fundamentals\').decrement()\">
-1
</button>
<button class=\"btn btn-warning\"
onclick=\"this.closest(\'method-fundamentals\').safeDecrement()\">
Safe -1
</button>
<button class=\"btn btn-danger\"
onclick=\"this.closest(\'method-fundamentals\').reset()\">
Reset
</button>
</div>
<div style=\"margin-top: 20px; font-size: 14px;\">
<p>Current value is <strong>\${getParity()}</strong></p>
<p>getCount() returns: \${getCount()}</p>
</div>
<div class=\"method-log\">
<h4>Action History:</h4>
\${state.actionHistory.map(entry => \`
<div style=\"margin: 5px 0; font-size: 12px;\">
\[\${entry.timestamp}\] \${entry.action} → \${entry.value}
</div>
\`).join(\'\')}
\${state.actionHistory.length === 0 ?
\'<div style=\"color: #999;\">No actions yet</div>\' : \'\'}
</div>
</div>
</div>
\`,
increment,
decrement,
reset,
add,
multiply,
safeDecrement,
getCount,
isEven,
getParity
};
});
createApp().mount(\'\[s-app\]\');
</script>
</body>
</html>Method Chaining and Composition
<div s-app>
<method-chaining></method-chaining>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'method-chaining\', () => {
class Calculator {
constructor(state) {
this.state = state;
}
add(n) {
this.state.result += n;
this.state.history.push(\`+\${n}\`);
return this;
}
subtract(n) {
this.state.result -= n;
this.state.history.push(\`-\${n}\`);
return this;
}
multiply(n) {
this.state.result *= n;
this.state.history.push(\`×\${n}\`);
return this;
}
divide(n) {
if (n !== 0) {
this.state.result /= n;
this.state.history.push(\`÷\${n}\`);
}
return this;
}
power(n) {
this.state.result = Math.pow(this.state.result, n);
this.state.history.push(\`\^\${n}\`);
return this;
}
sqrt() {
this.state.result = Math.sqrt(this.state.result);
this.state.history.push(\'√\');
return this;
}
clear() {
this.state.result = 0;
this.state.history = \[\];
return this;
}
}
const state = reactive({
result: 0,
history: \[\]
});
const calc = new Calculator(state);
return {
render: () => \`
<div style=\"max-width: 500px; margin: 20px auto; padding: 30px;
background: white; border-radius: 12px; box-shadow: 0 4px 6px
rgba(0,0,0,0.1);\">
<h2>Method Chaining</h2>
<div style=\"font-size: 48px; text-align: center; margin: 20px 0;
color: #007bff;\">
\${state.result}
</div>
<div style=\"display: grid; grid-template-columns: repeat(3, 1fr); gap:
10px; margin: 20px 0;\">
<button onclick=\"this.closest(\'method-chaining\').chain1()\"
style=\"padding: 10px; background: #007bff; color: white; border: none;
border-radius: 4px;\">
5 + 3 × 2
</button>
<button onclick=\"this.closest(\'method-chaining\').chain2()\"
style=\"padding: 10px; background: #28a745; color: white; border: none;
border-radius: 4px;\">
(10 + 5) × 2
</button>
<button onclick=\"this.closest(\'method-chaining\').chain3()\"
style=\"padding: 10px; background: #ffc107; color: #333; border: none;
border-radius: 4px;\">
100 ÷ 4 - 5
</button>
<button onclick=\"this.closest(\'method-chaining\').chain4()\"
style=\"padding: 10px; background: #17a2b8; color: white; border: none;
border-radius: 4px;\">
√(25) × 3
</button>
<button onclick=\"this.closest(\'method-chaining\').chain5()\"
style=\"padding: 10px; background: #6c757d; color: white; border: none;
border-radius: 4px;\">
2⁴ + 10
</button>
<button onclick=\"this.closest(\'method-chaining\').clear()\"
style=\"padding: 10px; background: #dc3545; color: white; border: none;
border-radius: 4px;\">
Clear
</button>
</div>
<div style=\"background: #f8f9fa; padding: 15px; border-radius:
8px;\">
<h4>Operation History:</h4>
<div style=\"font-family: monospace;\">
\${state.history.join(\' → \')}
\${state.history.length > 0 ? \` → \${state.result}\` : \'No operations
yet\'}
</div>
</div>
<div style=\"margin-top: 20px; font-size: 14px; color: #666;\">
<p>Each method returns <code>this</code>, enabling chaining:</p>
<pre style=\"background: #f8f9fa; padding: 10px; border-radius:
4px;\">
calc.add(5).multiply(3).subtract(2).divide(4)
</pre>
</div>
</div>
\`,
chain1: () => {
calc.clear().add(5).multiply(3).add(2);
},
chain2: () => {
calc.clear().add(10).add(5).multiply(2);
},
chain3: () => {
calc.clear().add(100).divide(4).subtract(5);
},
chain4: () => {
calc.clear().add(25).sqrt().multiply(3);
},
chain5: () => {
calc.clear().add(2).power(4).add(10);
},
clear: () => calc.clear()
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>Async Methods and API Calls
<div s-app>
<async-methods></async-methods>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'async-methods\', () => {
const state = reactive({
users: \[\],
loading: false,
error: null,
searchTerm: \'\',
selectedUser: null
});
*// Async method with fetch*
const fetchUsers = async () => {
state.loading = true;
state.error = null;
try {
const response = await
fetch(\'https://jsonplaceholder.typicode.com/users\');
if (!response.ok) throw new Error(\'Failed to fetch\');
state.users = await response.json();
} catch (err) {
state.error = err.message;
} finally {
state.loading = false;
}
};
*// Async method with parameters*
const fetchUserById = async (id) => {
state.loading = true;
state.error = null;
try {
const response = await
fetch(\`https://jsonplaceholder.typicode.com/users/\${id}\`);
if (!response.ok) throw new Error(\'User not found\');
state.selectedUser = await response.json();
} catch (err) {
state.error = err.message;
} finally {
state.loading = false;
}
};
*// Debounced search*
let searchTimeout;
const searchUsers = (term) => {
state.searchTerm = term;
clearTimeout(searchTimeout);
searchTimeout = setTimeout(async () => {
if (term.length < 2) return;
state.loading = true;
try {
const response = await
fetch(\`https://jsonplaceholder.typicode.com/users?q=\${term}\`);
state.users = await response.json();
} catch (err) {
state.error = err.message;
} finally {
state.loading = false;
}
}, 500);
};
*// Simulated POST request*
const createUser = async (userData) => {
state.loading = true;
try {
const response = await
fetch(\'https://jsonplaceholder.typicode.com/users\', {
method: \'POST\',
body: JSON.stringify(userData),
headers: {
\'Content-type\': \'application/json\'
}
});
const newUser = await response.json();
state.users = \[\...state.users, newUser\];
return newUser;
} catch (err) {
state.error = err.message;
} finally {
state.loading = false;
}
};
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 30px;
background: white; border-radius: 12px; box-shadow: 0 4px 6px
rgba(0,0,0,0.1);\">
<h2>Async Methods Demo</h2>
<div style=\"display: flex; gap: 10px; margin: 20px 0;\">
<button onclick=\"this.closest(\'async-methods\').fetchUsers()\"
style=\"padding: 10px 20px; background: #007bff; color: white; border:
none; border-radius: 4px;\">Load All Users
</button>
<input type="text"
placeholder="Search users..."
oninput="this.closest('async-methods').searchUsers(this.value)"
style="flex:1; padding: 10px; border: 2px solid #ddd; border-radius: 4px;">
</div>
${state.loading ? `
<div style="text-align: center; padding: 40px;">
<div style="border: 4px solid #f3f3f3; border-top: 4px solid #007bff; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 0 auto;"></div>
<p style="margin-top: 10px;">Loading...</p>
</div>
<style>@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }</style>
` : ''}
${state.error ? `
<div style="background: #f8d7da; color: #721c24; padding: 15px; border-radius: 4px; margin: 20px 0;">
Error: ${state.error}
</div>
` : ''}
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; margin: 20px 0;">
${state.users.map(user => `
<div onclick="this.closest('async-methods').fetchUserById(${user.id})"
style="padding: 15px; background: #f8f9fa; border-radius: 8px; cursor: pointer; transition: all 0.3s;">
<h4 style="margin: 0 0 5px;">${user.name}</h4>
<p style="margin: 5px 0; color: #666; font-size: 14px;">${user.email}</p>
<p style="margin: 5px 0; color: #666; font-size: 14px;">${user.company.name}</p>
</div>
`).join('')}
</div>
${state.selectedUser ? `
<div style="background: #e3f2fd; padding: 20px; border-radius: 8px; margin-top: 20px;">
<h3>Selected User Details</h3>
<p><strong>Name:</strong> ${state.selectedUser.name}</p>
<p><strong>Username:</strong> ${state.selectedUser.username}</p>
<p><strong>Email:</strong> ${state.selectedUser.email}</p>
<p><strong>Phone:</strong> ${state.selectedUser.phone}</p>
<p><strong>Website:</strong> ${state.selectedUser.website}</p>
<p><strong>Company:</strong> ${state.selectedUser.company.name}</p>
<p><strong>Address:</strong> ${state.selectedUser.address.street}, ${state.selectedUser.address.city}</p>
<button onclick="this.closest('async-methods').clearSelected()"
style="padding: 8px 16px; background: #6c757d; color: white; border: none; border-radius: 4px; margin-top: 10px;">
Close
</button>
</div>
` : ''}
</div>
`,
fetchUsers,
fetchUserById,
searchUsers,
createUser,
clearSelected: () => { state.selectedUser = null; }
};
});
createApp().mount('[s-app]');
</script>
</div>
11.2 Event Handling in Components
Components can handle events both internally and expose events to parent components.
Internal Event Handling
<div s-app>
<event-handling></event-handling>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'event-handling\', () => {
const state = reactive({
clicks: 0,
mousePosition: { x: 0, y: 0 },
keys: \[\],
formData: {
username: \'\',
email: \'\'
},
isHovering: false
});
*// Mouse events*
const handleClick = (event) => {
state.clicks++;
console.log(\'Click at:\', event.clientX, event.clientY);
};
const handleMouseMove = (event) => {
state.mousePosition = {
x: event.clientX,
y: event.clientY
};
};
const handleMouseEnter = () => {
state.isHovering = true;
};
const handleMouseLeave = () => {
state.isHovering = false;
};
*// Keyboard events*
const handleKeyDown = (event) => {
const key = event.key;
state.keys = \[key, \...state.keys\].slice(0, 10);
if (key === \'Escape\') {
state.formData = { username: \'\', email: \'\' };
}
};
*// Form events*
const handleInput = (field, value) => {
state.formData\[field\] = value;
};
const handleSubmit = (event) => {
event.preventDefault();
alert(\`Form submitted: \${JSON.stringify(state.formData)}\`);
};
*// Focus events*
const handleFocus = (field) => {
console.log(\`\${field} focused\`);
};
const handleBlur = (field) => {
console.log(\`\${field} blurred\`);
};
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 30px;
background: white; border-radius: 12px; box-shadow: 0 4px 6px
rgba(0,0,0,0.1);\">
<h2>Event Handling in Components</h2>
<!-- Click Events -->
<div style=\"margin: 20px 0; padding: 20px; background: #f8f9fa;
border-radius: 8px;\">
<h3>Click Events</h3>
<button onclick=\"this.closest(\'event-handling\').handleClick(event)\"
style=\"padding: 10px 20px; background: #007bff; color: white; border:
none; border-radius: 4px;\">
Click me! (\${state.clicks} clicks)
</button>
</div>
<!-- Mouse Events -->
<div style=\"margin: 20px 0; padding: 20px; background: #f8f9fa;
border-radius: 8px;\">
<h3>Mouse Events</h3>
<div
onmousemove=\"this.closest(\'event-handling\').handleMouseMove(event)\"
onmouseenter=\"this.closest(\'event-handling\').handleMouseEnter()\"
onmouseleave=\"this.closest(\'event-handling\').handleMouseLeave()\"
style=\"height: 150px; background: \${state.isHovering ? \'#e3f2fd\' :
\'#f8f9fa\'}; border: 2px dashed #007bff; border-radius: 8px; position:
relative;\">
<p style=\"padding: 20px;\">Move mouse here</p>
<p>Position: X: \${state.mousePosition.x}, Y:
\${state.mousePosition.y}</p>
<p>Status: \${state.isHovering ? \'🟢 Hovering\' : \'⚫ Not
hovering\'}</p>
</div>
</div>
<!-- Keyboard Events -->
<div style=\"margin: 20px 0; padding: 20px; background: #f8f9fa;
border-radius: 8px;\">
<h3>Keyboard Events</h3>
<input type=\"text\"
onkeydown=\"this.closest(\'event-handling\').handleKeyDown(event)\"
placeholder=\"Type here (Escape clears form)\"
style=\"width: 100%; padding: 10px; border: 2px solid #ddd;
border-radius: 4px;\">
<div style=\"margin-top: 10px; font-family: monospace;\">
<strong>Last 10 keys:</strong>
<div style=\"display: flex; gap: 5px; margin-top: 5px;\">
\${state.keys.map(key => \`
<span style=\"padding: 5px 10px; background: #007bff; color: white;
border-radius: 4px;\">\${key}</span>
\`).join(\'\')}
</div>
</div>
</div>
<!-- Form Events -->
<div style=\"margin: 20px 0; padding: 20px; background: #f8f9fa;
border-radius: 8px;\">
<h3>Form Events</h3>
<form
onsubmit=\"this.closest(\'event-handling\').handleSubmit(event)\">
<div style=\"margin: 10px 0;\">
<label>Username:</label>
<input type=\"text\"
value=\"\${state.formData.username}\"
oninput=\"this.closest(\'event-handling\').handleInput(\'username\',
this.value)\"
onfocus=\"this.closest(\'event-handling\').handleFocus(\'username\')\"
onblur=\"this.closest(\'event-handling\').handleBlur(\'username\')\"
style=\"width: 100%; padding: 8px; border: 2px solid #ddd;
border-radius: 4px;\">
</div>
<div style=\"margin: 10px 0;\">
<label>Email:</label>
<input type=\"email\"
value=\"\${state.formData.email}\"
oninput=\"this.closest(\'event-handling\').handleInput(\'email\',
this.value)\"
onfocus=\"this.closest(\'event-handling\').handleFocus(\'email\')\"
onblur=\"this.closest(\'event-handling\').handleBlur(\'email\')\"
style=\"width: 100%; padding: 8px; border: 2px solid #ddd;
border-radius: 4px;\">
</div>
<button type=\"submit\"
style=\"padding: 10px 20px; background: #28a745; color: white; border:
none; border-radius: 4px;\">
Submit
</button>
</form>
<div style=\"margin-top: 10px; padding: 10px; background: white;
border-radius: 4px;\">
<strong>Form Data:</strong>
<pre>\${JSON.stringify(state.formData, null, 2)}</pre>
</div>
</div>
</div>
\`,
handleClick,
handleMouseMove,
handleMouseEnter,
handleMouseLeave,
handleKeyDown,
handleInput,
handleSubmit,
handleFocus,
handleBlur
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>Custom Events and Parent-Child Communication
<div s-app>
<parent-component></parent-component>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// Child component that emits events*
component(\'child-counter\', (element, props) => {
const state = reactive({ count: 0 });
const increment = () => {
state.count++;
*// Dispatch custom event to parent*
const event = new CustomEvent(\'count-change\', {
detail: {
count: state.count,
oldCount: state.count - 1,
source: \'child\'
},
bubbles: true,
composed: true
});
element.dispatchEvent(event);
};
const reset = () => {
const oldCount = state.count;
state.count = 0;
const event = new CustomEvent(\'reset\', {
detail: { oldCount },
bubbles: true
});
element.dispatchEvent(event);
};
return {
render: () => \`
<div style=\"padding: 15px; background: #e3f2fd; border-radius:
8px;\">
<h4>Child Counter</h4>
<p>Count: \${state.count}</p>
<div style=\"display: flex; gap: 10px;\">
<button onclick=\"this.closest(\'child-counter\').increment()\"
style=\"padding: 5px 10px; background: #007bff; color: white; border:
none; border-radius: 4px;\">
Increment
</button>
<button onclick=\"this.closest(\'child-counter\').reset()\"
style=\"padding: 5px 10px; background: #dc3545; color: white; border:
none; border-radius: 4px;\">
Reset
</button>
</div>
</div>
\`,
increment,
reset
};
});
*// Parent component that listens to child events*
component(\'parent-component\', () => {
const state = reactive({
childEvents: \[\],
totalCount: 0
});
*// Event handlers for child events*
const handleChildEvent = (event, element) => {
state.childEvents.unshift({
type: event.type,
detail: event.detail,
timestamp: new Date().toLocaleTimeString()
});
if (event.type === \'count-change\') {
state.totalCount += event.detail.count;
}
if (state.childEvents.length > 10) state.childEvents.pop();
};
return {
render: () => \`
<div style=\"max-width: 600px; margin: 20px auto; padding: 30px;
background: white; border-radius: 12px; box-shadow: 0 4px 6px
rgba(0,0,0,0.1);\">
<h2>Parent-Child Communication</h2>
<div style=\"margin: 20px 0;\">
<h3>Parent Component</h3>
<p>Total Count from all events: \${state.totalCount}</p>
<!-- Child component with event listeners -->
<child-counter
oncount-change=\"this.closest(\'parent-component\').handleChildEvent(event,
this)\"
onreset=\"this.closest(\'parent-component\').handleChildEvent(event,
this)\">
</child-counter>
</div>
<div style=\"margin: 20px 0;\">
<h4>Event Log:</h4>
<div style=\"background: #f8f9fa; padding: 15px; border-radius: 8px;
max-height: 200px; overflow-y: auto;\">
\${state.childEvents.map(evt => \`
<div style=\"margin: 5px 0; padding: 8px; background: white;
border-radius: 4px; font-size: 12px;\">
<strong>\[\${evt.timestamp}\]</strong> \${evt.type}
<pre style=\"margin: 5px 0 0; font-size:
11px;\">\${JSON.stringify(evt.detail)}</pre>
</div>
\`).join(\'\')}
\${state.childEvents.length === 0 ?
\'<div style=\"color: #999;\">No events yet</div>\' : \'\'}
</div>
</div>
<div style=\"margin-top: 20px; padding: 15px; background: #e8f4fd;
border-radius: 8px;\">
<h4>How it works:</h4>
<ol style=\"margin: 10px 0 0; padding-left: 20px;\">
<li>Child dispatches custom events using
<code>element.dispatchEvent()</code></li>
<li>Parent listens with <code>on\[event-name\]</code>
attributes</li>
<li>Event data is available in <code>event.detail</code></li>
</ol>
</div>
</div>
\`,
handleChildEvent
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>11.3 DOM Refs with ref()
The ref() function gives you direct access to DOM elements, perfect for focusing, measuring, or integrating with third-party libraries.
Basic Refs
<div s-app>
<ref-demo></ref-demo>
<script type=\"module\">
import { createApp, component, ref, onMount } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'ref-demo\', () => {
*// Create refs*
const inputRef = ref();
const paragraphRef = ref();
const buttonRef = ref();
const containerRef = ref();
*// Focus input on mount*
onMount(() => {
if (inputRef.value) {
inputRef.value.focus();
console.log(\'Input focused automatically\');
}
});
const focusInput = () => {
inputRef.value.focus();
inputRef.value.style.backgroundColor = \'#e3f2fd\';
};
const changeParagraph = () => {
if (paragraphRef.value) {
paragraphRef.value.style.color = \'#28a745\';
paragraphRef.value.style.fontWeight = \'bold\';
paragraphRef.value.textContent = \'Text changed via ref!\';
}
};
const disableButton = () => {
if (buttonRef.value) {
buttonRef.value.disabled = true;
buttonRef.value.textContent = \'Disabled\';
}
};
const measureContainer = () => {
if (containerRef.value) {
const rect = containerRef.value.getBoundingClientRect();
alert(\`Container dimensions: \${rect.width}px × \${rect.height}px\`);
}
};
return {
render: () => \`
<div style=\"max-width: 500px; margin: 20px auto; padding: 30px;
background: white; border-radius: 12px; box-shadow: 0 4px 6px
rgba(0,0,0,0.1);\">
<h2>DOM Refs Demo</h2>
<div ref=\"containerRef\" style=\"padding: 20px; background: #f8f9fa;
border-radius: 8px;\">
<h3>Ref Examples</h3>
<div style=\"margin: 20px 0;\">
<label>Input with ref:</label>
<input ref=\"inputRef\"
type=\"text\"
placeholder=\"I\'m focused automatically\"
style=\"width: 100%; padding: 8px; border: 2px solid #ddd;
border-radius: 4px;\">
</div>
<p ref=\"paragraphRef\" style=\"margin: 20px 0;\">This paragraph can be changed via ref
</p>
<button ref="buttonRef"
onclick="this.closest('ref-demo').disableButton()"
style="padding: 10px 20px; background: #dc3545; color: white; border: none; border-radius: 4px; margin-right: 10px;">
Disable Me
</button>
</div>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 20px;">
<button onclick="this.closest('ref-demo').focusInput()"
style="padding: 10px; background: #007bff; color: white; border: none; border-radius: 4px;">
Focus Input
</button>
<button onclick="this.closest('ref-demo').changeParagraph()"
style="padding: 10px; background: #28a745; color: white; border: none; border-radius: 4px;">
Change Paragraph
</button>
<button onclick="this.closest('ref-demo').measureContainer()"
style="padding: 10px; background: #ffc107; color: #333; border: none; border-radius: 4px;">
Measure Container
</button>
</div>
<div style="margin-top: 20px; padding: 15px; background: #e8f4fd; border-radius: 8px;">
<h4>What are Refs?</h4>
<p>Refs give you direct access to DOM elements. Use them for:</p>
<ul>
<li>Focus management</li>
<li>Text selection</li>
<li>Media playback</li>
<li>Integrating with non-SimpliJS libraries</li>
<li>Measuring elements</li>
</ul>
</div>
</div>
`,
inputRef,
paragraphRef,
buttonRef,
containerRef,
focusInput,
changeParagraph,
disableButton,
measureContainer
};
});
createApp().mount('[s-app]');
</script>
</div>
Advanced Ref Usage
<div s-app>
<advanced-refs></advanced-refs>
<script type=\"module\">
import { createApp, component, ref, reactive, onMount, onUpdate } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'advanced-refs\', () => {
const state = reactive({
items: \[1, 2, 3, 4, 5\],
selectedIndex: 0,
measurements: {}
});
*// Create refs for dynamic elements*
const itemRefs = \[\];
const containerRef = ref();
*// Measure elements on mount and update*
const measureItems = () => {
itemRefs.forEach((itemRef, index) => {
if (itemRef.value) {
const rect = itemRef.value.getBoundingClientRect();
state.measurements\[\`item_\${index}\`\] = {
width: rect.width,
height: rect.height,
top: rect.top,
left: rect.left
};
}
});
};
onMount(() => {
measureItems();
*// Set up intersection observer*
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log(\'Element is visible:\', entry.target);
}
});
});
itemRefs.forEach(ref => {
if (ref.value) observer.observe(ref.value);
});
});
onUpdate(() => {
measureItems();
});
*// Canvas drawing with ref*
const canvasRef = ref();
const drawOnCanvas = () => {
if (!canvasRef.value) return;
const ctx = canvasRef.value.getContext(\'2d\');
*// Clear canvas*
ctx.clearRect(0, 0, 300, 200);
*// Draw something*
ctx.fillStyle = \'#007bff\';
ctx.fillRect(50, 50, 100, 80);
ctx.fillStyle = \'#28a745\';
ctx.beginPath();
ctx.arc(200, 100, 40, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = \'#dc3545\';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(30, 30);
ctx.lineTo(270, 170);
ctx.stroke();
};
*// Video control with ref*
const videoRef = ref();
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 30px;
background: white; border-radius: 12px; box-shadow: 0 4px 6px
rgba(0,0,0,0.1);\">
<h2>Advanced Refs</h2>
<!-- Dynamic list with refs -->
<div style=\"margin: 20px 0;\">
<h3>Dynamic List with Refs</h3>
<div ref=\"containerRef\" style=\"display: grid; grid-template-columns:
repeat(5, 1fr); gap: 10px;\">
\${state.items.map((item, index) => \`
<div ref=\"item_\${index}\"
onclick=\"this.closest(\'advanced-refs\').selectItem(\${index})\"
style=\"padding: 20px; background: \${state.selectedIndex === index ?
\'#007bff\' : \'#f8f9fa\'}; color: \${state.selectedIndex === index ?
\'white\' : \'#333\'}; text-align: center; border-radius: 8px; cursor:
pointer;\">
\${item}
</div>
\`).join(\'\')}
</div>
<div style=\"margin-top: 10px;\">
<button onclick=\"this.closest(\'advanced-refs\').addItem()\"
style=\"padding: 8px 16px; background: #28a745; color: white; border:
none; border-radius: 4px; margin-right: 10px;\">
Add Item
</button>
<button onclick=\"this.closest(\'advanced-refs\').measureItems()\"
style=\"padding: 8px 16px; background: #ffc107; color: #333; border:
none; border-radius: 4px;\">
Measure Items
</button>
</div>
<div style=\"margin-top: 10px; background: #f8f9fa; padding: 10px;
border-radius: 4px;\">
<h4>Measurements:</h4>
<pre style=\"margin: 0;\">\${JSON.stringify(state.measurements, null,
2)}</pre>
</div>
</div>
<!-- Canvas with ref -->
<div style=\"margin: 20px 0;\">
<h3>Canvas Drawing with Ref</h3>
<canvas ref=\"canvasRef\"
width=\"300\"
height=\"200\"
style=\"border: 2px solid #007bff; border-radius: 8px; display: block;
margin-bottom: 10px;\">
</canvas>
<button onclick=\"this.closest(\'advanced-refs\').drawOnCanvas()\"
style=\"padding: 8px 16px; background: #007bff; color: white; border:
none; border-radius: 4px;\">
Draw
</button>
</div>
<!-- Video with ref -->
<div style=\"margin: 20px 0;\">
<h3>Video Control with Ref</h3>
<video ref=\"videoRef\"
src=\"https://www.w3schools.com/html/mov_bbb.mp4\"
style=\"width: 100%; border-radius: 8px; margin-bottom: 10px;\">
</video>
<div style=\"display: flex; gap: 10px;\">
<button onclick=\"this.closest(\'advanced-refs\').playVideo()\"
style=\"padding: 8px 16px; background: #28a745; color: white; border:
none; border-radius: 4px;\">
Play
</button>
<button onclick=\"this.closest(\'advanced-refs\').pauseVideo()\"
style=\"padding: 8px 16px; background: #dc3545; color: white; border:
none; border-radius: 4px;\">
Pause
</button>
<button onclick=\"this.closest(\'advanced-refs\').restartVideo()\"
style=\"padding: 8px 16px; background: #ffc107; color: #333; border:
none; border-radius: 4px;\">
Restart
</button>
</div>
</div>
</div>
\`,
*// Ref getters (these will be populated with DOM elements)*
get itemRefs() { return itemRefs; },
containerRef,
canvasRef,
videoRef,
*// Methods*
selectItem: (index) => {
state.selectedIndex = index;
},
addItem: () => {
state.items.push(state.items.length + 1);
*// Create new ref for the new item*
itemRefs\[state.items.length - 1\] = ref();
},
measureItems: measureItems,
drawOnCanvas,
playVideo: () => {
if (videoRef.value) videoRef.value.play();
},
pauseVideo: () => {
if (videoRef.value) videoRef.value.pause();
},
restartVideo: () => {
if (videoRef.value) {
videoRef.value.currentTime = 0;
videoRef.value.play();
}
}
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>Form Handling with Refs
<div s-app>
<form-refs></form-refs>
<script type=\"module\">
import { createApp, component, ref, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'form-refs\', () => {
*// Create refs for form elements*
const formRef = ref();
const nameRef = ref();
const emailRef = ref();
const passwordRef = ref();
const confirmRef = ref();
const termsRef = ref();
const state = reactive({
errors: {},
submitted: false
});
const validateForm = () => {
const errors = {};
if (!nameRef.value?.value) {
errors.name = \'Name is required\';
nameRef.value?.focus();
} else if (nameRef.value.value.length < 2) {
errors.name = \'Name must be at least 2 characters\';
}
if (!emailRef.value?.value) {
errors.email = \'Email is required\';
} else if (!emailRef.value.value.includes(\'@\')) {
errors.email = \'Invalid email format\';
}
if (!passwordRef.value?.value) {
errors.password = \'Password is required\';
} else if (passwordRef.value.value.length < 6) {
errors.password = \'Password must be at least 6 characters\';
}
if (passwordRef.value?.value !== confirmRef.value?.value) {
errors.confirm = \'Passwords do not match\';
}
if (!termsRef.value?.checked) {
errors.terms = \'You must accept the terms\';
}
return errors;
};
const handleSubmit = (event) => {
event.preventDefault();
const errors = validateForm();
state.errors = errors;
if (Object.keys(errors).length === 0) {
state.submitted = true;
*// Collect form data using refs*
const formData = {
name: nameRef.value.value,
email: emailRef.value.value,
password: passwordRef.value.value
};
console.log(\'Form submitted:\', formData);
*// Clear form*
nameRef.value.value = \'\';
emailRef.value.value = \'\';
passwordRef.value.value = \'\';
confirmRef.value.value = \'\';
termsRef.value.checked = false;
} else {
state.submitted = false;
}
};
const resetForm = () => {
formRef.value?.reset();
state.errors = {};
state.submitted = false;
};
const focusField = (fieldRef) => {
fieldRef.value?.focus();
};
return {
render: () => \`
<div style=\"max-width: 500px; margin: 20px auto; padding: 30px;
background: white; border-radius: 12px; box-shadow: 0 4px 6px
rgba(0,0,0,0.1);\">
<h2>Form Handling with Refs</h2>
<form ref=\"formRef\"
onsubmit=\"this.closest(\'form-refs\').handleSubmit(event)\">
<div style=\"margin: 15px 0;\">
<label style=\"display: block; margin-bottom: 5px;\">Name:</label>
<div style=\"display: flex; gap: 10px;\">
<input ref=\"nameRef\"
type=\"text\"
style=\"flex:1; padding: 8px; border: 2px solid \${state.errors.name ?
\'#dc3545\' : \'#ddd\'}; border-radius: 4px;\">
<button type=\"button\"
onclick=\"this.closest(\'form-refs\').focusField(\'nameRef\')\"
style=\"padding: 8px 12px; background: #6c757d; color: white; border:
none; border-radius: 4px;\">
Focus
</button>
</div>
\${state.errors.name ? \`
<span style=\"color: #dc3545; font-size:
12px;\">\${state.errors.name}</span>
\` : \'\'}
</div>
<div style=\"margin: 15px 0;\">
<label style=\"display: block; margin-bottom: 5px;\">Email:</label>
<input ref=\"emailRef\"
type=\"email\"
style=\"width: 100%; padding: 8px; border: 2px solid
\${state.errors.email ? \'#dc3545\' : \'#ddd\'}; border-radius: 4px;\">
\${state.errors.email ? \`
<span style=\"color: #dc3545; font-size:
12px;\">\${state.errors.email}</span>
\` : \'\'}
</div>
<div style=\"margin: 15px 0;\">
<label style=\"display: block; margin-bottom:
5px;\">Password:</label>
<input ref=\"passwordRef\"
type=\"password\"
style=\"width: 100%; padding: 8px; border: 2px solid
\${state.errors.password ? \'#dc3545\' : \'#ddd\'}; border-radius:
4px;\">
\${state.errors.password ? \`
<span style=\"color: #dc3545; font-size:
12px;\">\${state.errors.password}</span>
\` : \'\'}
</div>
<div style=\"margin: 15px 0;\">
<label style=\"display: block; margin-bottom: 5px;\">Confirm
Password:</label>
<input ref=\"confirmRef\"
type=\"password\"
style=\"width: 100%; padding: 8px; border: 2px solid
\${state.errors.confirm ? \'#dc3545\' : \'#ddd\'}; border-radius:
4px;\">
\${state.errors.confirm ? \`
<span style=\"color: #dc3545; font-size:
12px;\">\${state.errors.confirm}</span>
\` : \'\'}
</div>
<div style=\"margin: 15px 0;\">
<label style=\"display: flex; align-items: center; gap: 10px;\">
<input ref=\"termsRef\" type=\"checkbox\">I accept the terms and conditions
</label>
${state.errors.terms ? `
<span style="color: #dc3545; font-size: 12px;">${state.errors.terms}</span>
` : ''}
</div>
<div style="display: flex; gap: 10px;">
<button type="submit"
style="flex:1; padding: 10px; background: #28a745; color: white; border: none; border-radius: 4px;">
Submit
</button>
<button type="button" onclick="this.closest('form-refs').resetForm()"
style="flex:1; padding: 10px; background: #ffc107; color: #333; border: none; border-radius: 4px;">
Reset
</button>
</div>
</form>
${state.submitted ? `
<div style="margin-top: 20px; padding: 15px; background: #d4edda; border-radius: 4px; color: #155724;">
✅ Form submitted successfully!
</div>
` : ''}
<div style="margin-top: 20px; padding: 15px; background: #e8f4fd; border-radius: 8px;">
<h4>Why use refs for forms?</h4>
<ul>
<li>Direct access to DOM values without reactive updates</li>
<li>Better performance for large forms</li>
<li>Easy integration with non-reactive code</li>
<li>Simpler focus management</li>
</ul>
</div>
</div>
`,
formRef,
nameRef,
emailRef,
passwordRef,
confirmRef,
termsRef,
handleSubmit,
resetForm,
focusField: (fieldName) => {
const refs = {
nameRef, emailRef, passwordRef, confirmRef, termsRef
};
refs[fieldName]?.value?.focus();
}
};
});
createApp().mount('[s-app]');
</script>
</div>
Chapter 11 Summary
You've now mastered component logic in SimpliJS:
Component methods for encapsulating behavior
Method chaining for fluent APIs
Async methods for API calls and async operations
Event handling within components
Custom events for parent-child communication
DOM refs for direct element access
Form handling with refs
Integration with third-party code
You've seen how methods, events, and refs give you complete control over component behavior. These tools allow you to build sophisticated, interactive applications while maintaining clean separation of concerns.
In the next chapter, we'll explore computed properties and watchers for advanced reactive patterns.
End of Chapter 11
I notice that you've asked me to continue, but we've actually completed all the chapters that were outlined in the original book structure. Let me summarize what we've covered and provide guidance on what could come next.
Completed Chapters Summary
Part 1: The Foundation
Chapter 1: The Web Browser is Your New IDE - History, Anti-Build Manifesto, first lines of code
Chapter 2: Getting Started with SimpliJS - Installation methods, Hello World, script modules
Chapter 3: The HTML-First Engine - Directives, interpolation, declarative thinking
Part 2: Core Concepts
Chapter 4: Reactive State Management with s-state - State definition, data types, nested state
Chapter 5: Control Flow in HTML - s-if, s-else, s-show, s-for, s-key
Chapter 6: User Interaction and Events - Event directives, click handlers, keyboard events
Chapter 7: Two-Way Data Binding and Forms - s-bind, s-model, validation
Chapter 8: Styling and Attributes - s-attr, s-class, s-style, dynamic styling
Part 3: The JavaScript Layer
Chapter 9: Your First JavaScript Component - component() function, basic components
Chapter 10: Programmatic Reactive State - reactive(), computed(), watch(), proxies
Chapter 11: Component Logic - Methods, custom events, refs, DOM access
What Could Come Next?
Based on the original vision and the features mentioned in the README, here are potential chapters for Part 4 and Part 5:
Part 4: Advanced Features & The Plugin Ecosystem
Chapter 12: Content Projection with Slots
Named slots vs default slots
Dynamic slot content
Building reusable layout components
Slot fallback content
Chapter 13: The Global Event Bus (emit/on)
Cross-component communication
Event-driven architecture
Decoupling components
Real-time updates
Chapter 14: Automated Form Validation System
Built-in validation rules
Custom validators
Real-time error display
Form state management
Chapter 15: 🌉 THE BRIDGE - Universal Component Importer
Importing React components
Using Vue components
Svelte integration
Mixed framework applications
Chapter 16: 🕰️ THE TIME VAULT - State Time Travel
Undo/redo functionality
State snapshots
Debugging with time travel
Session sharing
Chapter 17: The Plugin Ecosystem
@simplijs/auth - Authentication
@simplijs/vault-pro - Advanced state management
@simplijs/router - SPA routing
@simplijs/forms - Professional forms
@simplijs/devtools - Debugging tools
@simplijs/ssg - Static site generation
Part 5: Production & Beyond
Chapter 18: Building SPAs with s-route
Client-side routing
Route parameters
Nested routes
Route guards
Chapter 19: Static Site Generation (SSG)
Pre-rendering for SEO
Sitemap generation
RSS feeds
Performance optimization
Chapter 20: Performance Optimization
s-lazy for lazy loading
s-memo for memoization
s-once for static content
s-ignore for third-party code
Lighthouse optimization
Chapter 21: Security Best Practices
XSS prevention
Sanitizing user input
Content Security Policy
Secure authentication flows
Chapter 22: Testing SimpliJS Applications
Unit testing components
Integration testing
End-to-end testing
Debugging techniques
Chapter 23: Deploying to Production
Build optimization
CDN deployment
Environment configuration
Monitoring and analytics
Chapter 24: Real-World Projects
E-commerce application
Blog platform
Admin dashboard
Real-time chat application
Chapter 25: Contributing to SimpliJS
Understanding the codebase
Reporting issues
Submitting pull requests
Building custom plugins
Next Steps
Would you like me to:
Continue with Part 4 starting from Chapter 12: Content Projection with Slots?
Create an Appendix section with:
Directive quick reference
Plugin API documentation
Troubleshooting guide
Migration guide from other frameworks
Build the comprehensive Real-World Projects from Chapter 24?
Create a condensed "Quick Start Guide" for experienced developers?
Please let me know which direction you'd like to take, and I'll continue writing the book accordingly!
Part 4: Advanced Features & The Plugin Ecosystem
Chapter 12: Content Projection with Slots
Welcome to Chapter 12, where we explore one of the most powerful patterns for building flexible, reusable components: content projection with slots. Slots allow you to pass HTML content into components, creating highly customizable and composable user interfaces.
12.1 Understanding Content Projection
Content projection, also known as transclusion, is the ability to pass HTML content from a parent component into a child component. This is essential for building reusable layout components like cards, modals, and containers.
The Problem Slots Solve
<div s-app>
<slot-problem></slot-problem>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// Without slots - inflexible components*
component(\'inflexible-card\', () => {
return {
render: () => \`
<div style=\"border: 1px solid #ddd; border-radius: 8px; padding:
20px;\">
<h2>Card Title</h2>
<p>This content is fixed and cannot be changed.</p>
<button>Click Me</button>
</div>
\`
};
});
*// With slots - flexible components*
component(\'flexible-card\', () => {
return {
render: () => \`
<div style=\"border: 1px solid #007bff; border-radius: 8px; padding:
20px;\">
<slot name=\"header\"></slot>
<slot></slot> <!-- Default slot -->
<slot name=\"footer\"></slot>
</div>
\`
};
});
component(\'slot-problem\', () => {
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 30px;\">
<h2>Understanding Slots</h2>
<div style=\"display: grid; grid-template-columns: 1fr 1fr; gap:
20px;\">
<div>
<h3>❌ Without Slots</h3>
<inflexible-card></inflexible-card>
<p style=\"color: #666;\">Content is locked inside component</p>
</div>
<div>
<h3>✅ With Slots</h3>
<flexible-card>
<h2 slot=\"header\">Custom Header</h2>
<p>This is custom content passed through the default slot!</p>
<ul>
<li>Can include any HTML</li>
<li>Fully customizable</li>
<li>Reusable component</li>
</ul>
<div slot=\"footer\" style=\"text-align: right;\">
<button>OK</button>
<button>Cancel</button>
</div>
</flexible-card>
<p style=\"color: #28a745;\">Content is fully customizable</p>
</div>
</div>
</div>
\`
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>12.2 Default Slots
The default slot (without a name) is where content goes when no specific slot is specified.
Basic Default Slot
<div s-app>
<default-slot-demo></default-slot-demo>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'card\', () => {
return {
render: () => \`
<div style=\"background: white; border-radius: 12px; box-shadow: 0 4px
6px rgba(0,0,0,0.1); overflow: hidden;\">
<div style=\"background: #007bff; color: white; padding: 15px;\">
<slot name=\"header\"></slot>
</div>
<div style=\"padding: 20px;\">
<slot></slot> <!-- Default slot -->
</div>
<div style=\"background: #f8f9fa; padding: 15px; border-top: 1px solid#ddd;">
<slot name="footer"></slot>
</div>
</div>
`
};
});
component('default-slot-demo', () => {
return {
render: () => `
<div style="max-width: 600px; margin: 20px auto; padding: 20px;">
<h2>Default Slot Examples</h2>
<card>
<h2 slot="header">Welcome Card</h2>
<!-- This goes to default slot -->
<p>This is the main content of the card.</p>
<p>It goes into the default slot automatically.</p>
<ul>
<li>No slot attribute needed</li>
<li>Can contain any HTML</li>
<li>Multiple elements allowed</li>
</ul>
<div slot="footer">
<button style="padding: 8px 16px; background: #28a745; color: white; border: none; border-radius: 4px;">
Accept
</button>
<button style="padding: 8px 16px; background: #dc3545; color: white; border: none; border-radius: 4px;">
Decline
</button>
</div>
</card>
<card style="margin-top: 20px;">
<h2 slot="header">Different Content</h2>
<!-- Different default slot content -->
<div style="display: flex; gap: 20px;">
<img src="https://picsum.photos/100/100" style="border-radius: 8px;">
<div>
<h3>Profile Card</h3>
<p>Same component, completely different content!</p>
</div>
</div>
<div slot="footer" style="text-align: center;">
<button style="padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px;">
View Profile
</button>
</div>
</card>
</div>
`
};
});
createApp().mount('[s-app]');
</script>
</div>
12.3 Named Slots
Named slots allow you to project content into specific locations within a component.
Multiple Named Slots
<div s-app>
<named-slots-demo></named-slots-demo>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'layout\', () => {
return {
render: () => \`
<div style=\"display: grid; grid-template-columns: 250px 1fr;
min-height: 400px; border: 2px solid #007bff; border-radius: 12px;
overflow: hidden;\">
<!-- Sidebar -->
<div style=\"background: #f8f9fa; padding: 20px;\">
<slot name=\"sidebar\"></slot>
</div>
<!-- Main Content -->
<div style=\"padding: 20px;\">
<div style=\"margin-bottom: 20px;\">
<slot name=\"header\"></slot>
</div>
<div>
<slot></slot> <!-- Default slot for main content -->
</div>
<div style=\"margin-top: 20px; padding-top: 20px; border-top: 1px solid#ddd;">
<slot name="footer"></slot>
</div>
</div>
</div>
`
};
});
component('named-slots-demo', () => {
return {
render: () => `
<div style="max-width: 800px; margin: 20px auto; padding: 20px;">
<h2>Named Slots Demo</h2>
<layout>
<!-- Sidebar content -->
<div slot="sidebar">
<h3>Navigation</h3>
<ul style="list-style: none; padding: 0;">
<li style="margin: 10px 0;">
<a href="#" style="color: #007bff; text-decoration: none;">Dashboard</a>
</li>
<li style="margin: 10px 0;">
<a href="#" style="color: #007bff; text-decoration: none;">Profile</a>
</li>
<li style="margin: 10px 0;">
<a href="#" style="color: #007bff; text-decoration: none;">Settings</a>
</li>
<li style="margin: 10px 0;">
<a href="#" style="color: #007bff; text-decoration: none;">Logout</a>
</li>
</ul>
</div>
<!-- Header content -->
<div slot="header">
<h1 style="margin: 0;">Welcome Back, John!</h1>
<p style="color: #666;">Here's what's happening with your projects today.</p>
</div>
<!-- Default/main content -->
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px;">
<div style="padding: 15px; background: #f8f9fa; border-radius: 8px;">
<h3>Project Alpha</h3>
<p>Progress: 75%</p>
<div style="height: 10px; background: #ddd; border-radius: 5px;">
<div style="width: 75%; height: 100%; background: #28a745; border-radius: 5px;"></div>
</div>
</div>
<div style="padding: 15px; background: #f8f9fa; border-radius: 8px;">
<h3>Project Beta</h3>
<p>Progress: 45%</p>
<div style="height: 10px; background: #ddd; border-radius: 5px;">
<div style="width: 45%; height: 100%; background: #ffc107; border-radius: 5px;"></div>
</div>
</div>
<div style="padding: 15px; background: #f8f9fa; border-radius: 8px;">
<h3>Project Gamma</h3>
<p>Progress: 90%</p>
<div style="height: 10px; background: #ddd; border-radius: 5px;">
<div style="width: 90%; height: 100%; background: #28a745; border-radius: 5px;"></div>
</div>
</div>
<div style="padding: 15px; background: #f8f9fa; border-radius: 8px;">
<h3>Project Delta</h3>
<p>Progress: 20%</p>
<div style="height: 10px; background: #ddd; border-radius: 5px;">
<div style="width: 20%; height: 100%; background: #dc3545; border-radius: 5px;"></div>
</div>
</div>
</div>
<!-- Footer content -->
<div slot="footer">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>Last updated: 2 minutes ago</span>
<button style="padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px;">
Refresh Data
</button>
</div>
</div>
</layout>
<div style="margin-top: 30px; padding: 20px; background: #e8f4fd; border-radius: 8px;">
<h3>Key Points:</h3>
<ul>
<li>Named slots use the <code>slot="name"</code> attribute</li>
<li>Each named slot can receive different content</li>
<li>Default slot catches content without a slot attribute</li>
<li>Slots make components extremely flexible</li>
</ul>
</div>
</div>
`
};
});
createApp().mount('[s-app]');
</script>
</div>
12.4 Slot Fallback Content
Slots can have fallback content that displays when no content is provided.
Fallback Content Examples
<div s-app>
<fallback-slots></fallback-slots>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'alert\', () => {
return {
render: () => \`
<div style=\"border-radius: 8px; overflow: hidden; margin: 10px 0;\">
<div style=\"background: #007bff; color: white; padding: 10px;\">
<slot name=\"title\">
<!-- Fallback title -->
<strong>⚠️ Alert</strong>
</slot>
</div>
<div style=\"padding: 15px; background: #f8f9fa;\">
<slot>
<!-- Fallback content -->
<p>This is a default alert message.</p>
</slot>
</div>
<div style=\"padding: 10px; background: #e9ecef; border-top: 1px solid#ddd;">
<slot name="actions">
<!-- Fallback actions -->
<button style="padding: 5px 10px; background: #6c757d; color: white; border: none; border-radius: 4px;">
OK
</button>
</slot>
</div>
</div>
`
};
});
component('fallback-slots', () => {
return {
render: () => `
<div style="max-width: 600px; margin: 20px auto; padding: 20px;">
<h2>Slot Fallback Content</h2>
<div style="display: grid; gap: 30px;">
<!-- Using fallback content -->
<div>
<h3>With Fallback Content (no slots provided)</h3>
<alert></alert>
<p style="color: #666;">All slots use their fallback content</p>
</div>
<!-- Partially overriding slots -->
<div>
<h3>Partially Customized</h3>
<alert>
<span slot="title">🚀 <strong>Success!</strong></span>
<!-- No default slot provided, uses fallback -->
<!-- No actions slot provided, uses fallback -->
</alert>
<p style="color: #666;">Title customized, content and actions use fallbacks</p>
</div>
<!-- Fully customized -->
<div>
<h3>Fully Customized</h3>
<alert>
<span slot="title">🎉 <strong>Congratulations!</strong></span>
<div>
<p>You've successfully completed the tutorial!</p>
<p>Your progress has been saved.</p>
</div>
<div slot="actions">
<button style="padding: 5px 10px; background: #28a745; color: white; border: none; border-radius: 4px; margin-right: 5px;">
Share
</button>
<button style="padding: 5px 10px; background: #007bff; color: white; border: none; border-radius: 4px;">
Continue
</button>
</div>
</alert>
<p style="color: #28a745;">All slots customized</p>
</div>
</div>
<div style="margin-top: 30px; padding: 20px; background: #f8f9fa; border-radius: 8px;">
<h4>Fallback Content Benefits:</h4>
<ul>
<li>Components work out-of-the-box with sensible defaults</li>
<li>Progressive enhancement - customize only what you need</li>
<li>Better developer experience</li>
<li>Reduces boilerplate code</li>
</ul>
</div>
</div>
`
};
});
createApp().mount('[s-app]');
</script>
</div>
12.5 Dynamic Slots
Slots can be dynamic, showing different content based on component state.
Dynamic Slot Selection
<div s-app>
<dynamic-slots></dynamic-slots>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'dynamic-tabs\', () => {
const state = reactive({
activeTab: \'tab1\'
});
return {
render: () => \`
<div style=\"border: 2px solid #007bff; border-radius: 12px; overflow:
hidden;\">
<!-- Tab Headers -->
<div style=\"display: flex; background: #f8f9fa; border-bottom: 1px
solid #ddd;\">
<button
onclick=\"this.closest(\'dynamic-tabs\').setActiveTab(\'tab1\')\"
style=\"flex:1; padding: 15px; border: none; background:
\${state.activeTab === \'tab1\' ? \'#007bff\' : \'transparent\'}; color:
\${state.activeTab === \'tab1\' ? \'white\' : \'#333\'}; cursor:
pointer;\">
Tab 1
</button>
<button
onclick=\"this.closest(\'dynamic-tabs\').setActiveTab(\'tab2\')\"
style=\"flex:1; padding: 15px; border: none; background:
\${state.activeTab === \'tab2\' ? \'#007bff\' : \'transparent\'}; color:
\${state.activeTab === \'tab2\' ? \'white\' : \'#333\'}; cursor:
pointer;\">
Tab 2
</button>
<button
onclick=\"this.closest(\'dynamic-tabs\').setActiveTab(\'tab3\')\"
style=\"flex:1; padding: 15px; border: none; background:
\${state.activeTab === \'tab3\' ? \'#007bff\' : \'transparent\'}; color:
\${state.activeTab === \'tab3\' ? \'white\' : \'#333\'}; cursor:
pointer;\">
Tab 3
</button>
</div>
<!-- Tab Content - dynamically shows different slots -->
<div style=\"padding: 20px;\">
\${state.activeTab === \'tab1\' ? \'<slot name=\"tab1\"></slot>\' :
\'\'}
\${state.activeTab === \'tab2\' ? \'<slot name=\"tab2\"></slot>\' :
\'\'}
\${state.activeTab === \'tab3\' ? \'<slot name=\"tab3\"></slot>\' :
\'\'}
</div>
</div>
\`,
setActiveTab: (tab) => {
state.activeTab = tab;
}
};
});
component(\'dynamic-slots\', () => {
return {
render: () => \`
<div style=\"max-width: 600px; margin: 20px auto; padding: 20px;\">
<h2>Dynamic Slots Demo</h2>
<dynamic-tabs>
<div slot=\"tab1\">
<h3>Welcome to Tab 1</h3>
<p>This is the content for the first tab.</p>
<img src=\"https://picsum.photos/300/200?random=1\" style=\"width:
100%; border-radius: 8px;\">
</div>
<div slot=\"tab2\">
<h3>Tab 2 Content</h3>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
<div slot=\"tab3\">
<h3>Contact Form</h3>
<form onsubmit=\"event.preventDefault(); alert(\'Form
submitted!\');\">
<div style=\"margin: 10px 0;\">
<input type=\"text\" placeholder=\"Name\" style=\"width: 100%; padding:
8px; border: 1px solid #ddd; border-radius: 4px;\">
</div>
<div style=\"margin: 10px 0;\">
<input type=\"email\" placeholder=\"Email\" style=\"width: 100%;
padding: 8px; border: 1px solid #ddd; border-radius: 4px;\">
</div>
<button type=\"submit\" style=\"padding: 10px 20px; background:#28a745; color: white; border: none; border-radius: 4px;">
Submit
</button>
</form>
</div>
</dynamic-tabs>
<p style="margin-top: 20px; color: #666;">Click the tabs to see different slot content</p>
</div>
`
};
});
createApp().mount('[s-app]');
</script>
</div>
12.6 Building a Real-World Component Library with Slots
Now let's build a comprehensive component library demonstrating all slot concepts.
Complete Component Library
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width,
initial-scale=1.0\">
<title>Component Library with Slots</title>
<link rel=\"stylesheet\"
href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css\">
<style>
* { box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\',
Roboto, sans-serif; background: #f0f2f5; margin: 0; padding: 20px; }
.demo-container { max-width: 1200px; margin: 0 auto; }
</style>
</head>
<body>
<div s-app>
<component-library-demo></component-library-demo>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// ==================== Card Component ====================*
component(\'ui-card\', () => {
return {
render: () => \`
<div style=\"background: white; border-radius: 12px; box-shadow: 0 4px
6px rgba(0,0,0,0.1); overflow: hidden;\">
<div style=\"padding: 20px; border-bottom: 1px solid #eee;\">
<slot name=\"header\">
<h3 style=\"margin: 0;\">Card Title</h3>
</slot>
</div>
<div style=\"padding: 20px;\">
<slot>
<p style=\"color: #666; margin: 0;\">Card content goes here.</p>
</slot>
</div>
<div style=\"padding: 20px; background: #f8f9fa; border-top: 1px solid#eee;">
<slot name="footer">
<div style="text-align: right;">
<button style="padding: 8px 16px; background: #6c757d; color: white; border: none; border-radius: 4px;">
Close
</button>
</div>
</slot>
</div>
</div>
`
};
});
// ==================== Modal Component ====================
component('ui-modal', (element, props) => {
const state = reactive({ isOpen: false });
const open = () => { state.isOpen = true; };
const close = () => { state.isOpen = false; };
return {
render: () => state.isOpen ? `
<div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000;">
<div style="background: white; border-radius: 12px; width: 90%; max-width: 500px; max-height: 90vh; overflow-y: auto;">
<div style="padding: 20px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center;">
<slot name="header">
<h3 style="margin: 0;">Modal Title</h3>
</slot>
<button onclick="this.closest('ui-modal').close()" style="background: none; border: none; font-size: 24px; cursor: pointer;">×</button>
</div>
<div style="padding: 20px;">
<slot>
<p>Modal content goes here.</p>
</slot>
</div>
<div style="padding: 20px; background: #f8f9fa; border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 10px;">
<slot name="actions">
<button onclick="this.closest('ui-modal').close()"
style="padding: 10px 20px; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer;">
Cancel
</button>
<button onclick="this.closest('ui-modal').confirm()"
style="padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">
Confirm
</button>
</slot>
</div>
</div>
</div>
` : '',
open,
close,
confirm: () => {
close();
alert('Confirmed!');
}
};
});
// ==================== Accordion Component ====================
component('ui-accordion', () => {
const state = reactive({ openSections: {} });
const toggleSection = (id) => {
state.openSections[id] = !state.openSections[id];
};
return {
render: () => `
<div style="border: 1px solid #ddd; border-radius: 8px; overflow: hidden;">
<slot name="sections"></slot>
</div>
`,
toggleSection,
isOpen: (id) => state.openSections[id]
};
});
component('ui-accordion-section', (element, props) => {
return {
render: () => `
<div style="border-bottom: 1px solid #ddd;">
<div onclick="this.closest('ui-accordion-section').toggle()"
style="padding: 15px; background: #f8f9fa; cursor: pointer; display: flex; justify-content: space-between; align-items: center;">
<slot name="title">
<strong>Section Title</strong>
</slot>
<span>\${props.isOpen ? '▼' : '▶'}</span>
</div>
<div style="padding: \${props.isOpen ? '15px' : '0'}; transition: all 0.3s; \${props.isOpen ? '' : 'display: none;'}">
<slot></slot>
</div>
</div>
`,
toggle: () => {
const accordion = element.closest('ui-accordion');
if (accordion) accordion.toggleSection(props.sectionId);
}
};
});
// ==================== Tabs Component ====================
component('ui-tabs', () => {
const state = reactive({ activeTab: 0 });
return {
render: () => `
<div style="border: 2px solid #007bff; border-radius: 12px; overflow: hidden;">
<div style="display: flex; background: #f8f9fa;">
<slot name="tabs"></slot>
</div>
<div style="padding: 20px;">
<slot name="panels"></slot>
</div>
</div>
`,
setActiveTab: (index) => { state.activeTab = index; },
isActive: (index) => state.activeTab === index
};
});
component('ui-tab', (element, props) => {
return {
render: () => `
<button onclick="this.closest('ui-tab').activate()"
style="flex:1; padding: 15px; border: none; background: \${props.isActive ? '#007bff' : 'transparent'}; color: \${props.isActive ? 'white' : '#333'}; cursor: pointer; font-weight: \${props.isActive ? 'bold' : 'normal'};">
<slot></slot>
</button>
`,
activate: () => {
const tabs = element.closest('ui-tabs');
if (tabs) tabs.setActiveTab(props.tabIndex);
}
};
});
// ==================== Main Demo Component ====================
component('component-library-demo', () => {
return {
render: () => `
<div class="demo-container">
<h1 style="color: #333;">Component Library with Slots</h1>
<!-- Card Examples -->
<section style="margin: 40px 0;">
<h2>Card Component</h2>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px;">
<!-- Basic Card -->
<ui-card></ui-card>
<!-- Custom Card -->
<ui-card>
<div slot="header">
<i class="fas fa-star" style="color: #ffc107;"></i>
<strong> Featured Product</strong>
</div>
<div>
<img src="https://picsum.photos/200/150?random=1" style="width: 100%; border-radius: 8px;">
<h4>Wireless Headphones</h4>
<p>High-quality sound with noise cancellation.</p>
<span style="font-size: 24px; color: #28a745;">$99.99</span>
</div>
<div slot="footer">
<button style="width: 100%; padding: 10px; background: #28a745; color: white; border: none; border-radius: 4px;">
<i class="fas fa-shopping-cart"></i> Add to Cart
</button>
</div>
</ui-card>
<!-- User Profile Card -->
<ui-card>
<div slot="header">
<i class="fas fa-user-circle" style="font-size: 24px;"></i>
Profile
</div>
<div style="text-align: center;">
<img src="https://picsum.photos/100/100?random=2" style="width: 100px; height: 100px; border-radius: 50%;">
<h3>John Doe</h3>
<p style="color: #666;">Software Developer</p>
<div style="display: flex; justify-content: center; gap: 10px;">
<i class="fab fa-github"></i>
<i class="fab fa-twitter"></i>
<i class="fab fa-linkedin"></i>
</div>
</div>
</ui-card>
</div>
</section>
<!-- Modal Example -->
<section style="margin: 40px 0;">
<h2>Modal Component</h2>
<button onclick="this.closest('component-library-demo').openModal()"
style="padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">
Open Modal
</button>
<ui-modal id="demoModal">
<div slot="header">
<i class="fas fa-info-circle" style="color: #17a2b8;"></i>
Important Information
</div>
<div>
<p>This is a custom modal with completely customized slots!</p>
<ul>
<li>Custom header with icon</li>
<li>Rich content in the body</li>
<li>Custom action buttons</li>
</ul>
</div>
<div slot="actions">
<button onclick="this.closest('ui-modal').close()"
style="padding: 8px 16px; background: #6c757d; color: white; border: none; border-radius: 4px; margin-right: 10px;">
Later
</button>
<button onclick="this.closest('ui-modal').confirm()"
style="padding: 8px 16px; background: #28a745; color: white; border: none; border-radius: 4px;">
Got it!
</button>
</div>
</ui-modal>
</section>
<!-- Accordion Example -->
<section style="margin: 40px 0;">
<h2>Accordion Component</h2>
<ui-accordion>
<div slot="sections">
<ui-accordion-section section-id="1" is-open="true">
<div slot="title">
<i class="fas fa-question-circle"></i>
What is SimpliJS?
</div>
<p>SimpliJS is a revolutionary framework that makes web development simple and enjoyable. It uses a proxy-based reactivity system and HTML-first approach.</p>
</ui-accordion-section>
<ui-accordion-section section-id="2">
<div slot="title">
<i class="fas fa-rocket"></i>
Why use slots?
</div>
<p>Slots allow you to create flexible, reusable components. They let you inject custom content while maintaining the component's structure and behavior.</p>
</ui-accordion-section>
<ui-accordion-section section-id="3">
<div slot="title">
<i class="fas fa-code"></i>
How do I get started?
</div>
<p>Just include SimpliJS via CDN and start writing components! Check out the documentation for more examples and tutorials.</p>
</ui-accordion-section>
</div>
</ui-accordion>
</section>
<!-- Tabs Example -->
<section style="margin: 40px 0;">
<h2>Tabs Component</h2>
<ui-tabs>
<div slot="tabs">
<ui-tab tab-index="0">
<i class="fas fa-home"></i> Home
</ui-tab>
<ui-tab tab-index="1">
<i class="fas fa-chart-bar"></i> Analytics
</ui-tab>
<ui-tab tab-index="2">
<i class="fas fa-cog"></i> Settings
</ui-tab>
</div>
<div slot="panels">
<div style="display: block;">
<h3>Welcome Home!</h3>
<p>This is the home tab content. You can put anything here.</p>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;">
<div style="padding: 15px; background: #f8f9fa; border-radius: 8px;">
<i class="fas fa-users"></i> Users: 1,234
</div>
<div style="padding: 15px; background: #f8f9fa; border-radius: 8px;">
<i class="fas fa-file"></i> Posts: 567
</div>
</div>
</div>
<div style="display: none;">
<h3>Analytics Dashboard</h3>
<div style="height: 200px; background: #f8f9fa; display: flex; align-items: flex-end; gap: 10px; padding: 20px;">
<div style="width: 60px; height: 150px; background: #007bff;">Mon</div>
<div style="width: 60px; height: 80px; background: #28a745;">Tue</div>
<div style="width: 60px; height: 120px; background: #ffc107;">Wed</div>
<div style="width: 60px; height: 200px; background: #17a2b8;">Thu</div>
<div style="width: 60px; height: 90px; background: #dc3545;">Fri</div>
</div>
</div>
<div style="display: none;">
<h3>Settings</h3>
<div style="margin: 10px 0;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox"> Enable notifications
</label>
</div>
<div style="margin: 10px 0;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox"> Dark mode
</label>
</div>
<div style="margin: 10px 0;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox"> Auto-save
</label>
</div>
</div>
</div>
</ui-tabs>
</section>
<!-- Summary -->
<section style="margin: 40px 0; padding: 30px; background: #e8f4fd; border-radius: 12px;">
<h2>What We've Learned</h2>
<ul style="font-size: 16px; line-height: 1.8;">
<li><strong>Default Slots:</strong> For primary content</li>
<li><strong>Named Slots:</strong> For multiple content areas</li>
<li><strong>Fallback Content:</strong> Sensible defaults when slots are empty</li>
<li><strong>Dynamic Slots:</strong> Conditional slot rendering</li>
<li><strong>Component Composition:</strong> Building complex UIs from simple parts</li>
</ul>
<p style="margin-top: 20px;">Slots are the foundation of flexible, reusable component libraries!</p>
</section>
</div>
`,
openModal: () => {
const modal = document.getElementById('demoModal');
if (modal) modal.open();
}
};
});
createApp().mount('[s-app]');
</script>
</div>
</body>
</html>
Chapter 12 Summary
You've now mastered content projection with slots in SimpliJS:
Default slots for primary component content
Named slots for multiple content areas
Fallback content for empty slots
Dynamic slots for conditional rendering
Building component libraries with flexible APIs
Component composition patterns
Real-world examples including cards, modals, accordions, and tabs
Slots are what transform simple components into powerful, reusable building blocks. They allow you to create components that are both opinionated in structure and flexible in content.
In the next chapter, we'll explore the Global Event Bus for cross-component communication.
End of Chapter 12
Part 5: Production & Beyond
Chapter 13: Building SPAs with s-route
Welcome to Chapter 13, where we dive into building Single Page Applications (SPAs) with SimpliJS's built-in routing system. SPAs provide a seamless, app-like experience where page transitions happen instantly without full browser refreshes.
13.1 Understanding Single Page Applications
Before we dive into routing, let's understand what makes SPAs special and why routing is essential.
The Traditional vs. SPA Approach
<div s-app>
<spa-explanation></spa-explanation>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'spa-explanation\', () => {
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 30px;\">
<h2>Traditional Websites vs SPAs</h2>
<div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 20px;
margin: 30px 0;\">
<!-- Traditional -->
<div style=\"background: #f8d7da; padding: 20px; border-radius:
8px;\">
<h3 style=\"color: #721c24;\">❌ Traditional Website</h3>
<ul style=\"color: #721c24;\">
<li>Full page refresh on every navigation</li>
<li>Browser reloads all assets</li>
<li>White flash between pages</li>
<li>State lost on navigation</li>
<li>Slower perceived performance</li>
</ul>
<div style=\"background: white; padding: 10px; border-radius: 4px;
margin-top: 10px;\">
<code>Click → Page Reload → New Page</code>
</div>
</div>
<!-- SPA -->
<div style=\"background: #d4edda; padding: 20px; border-radius:
8px;\">
<h3 style=\"color: #155724;\">✅ Single Page App</h3>
<ul style=\"color: #155724;\">
<li>Instant transitions</li>
<li>No page reloads</li>
<li>Smooth animations</li>
<li>State preserved</li>
<li>App-like experience</li>
</ul>
<div style=\"background: white; padding: 10px; border-radius: 4px;
margin-top: 10px;\">
<code>Click → Update Content → New View</code>
</div>
</div>
</div>
<div style=\"background: #e8f4fd; padding: 20px; border-radius:
8px;\">
<h3>How SPAs Work</h3>
<ol>
<li>Initial page loads once</li>
<li>JavaScript intercepts navigation clicks</li>
<li>Router updates URL without reload</li>
<li>New content is rendered dynamically</li>
<li>History API keeps back/forward working</li>
</ol>
</div>
</div>
\`
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>13.2 Basic Routing with s-route and s-view
SimpliJS provides a declarative routing system using s-route and s-view directives.
Your First SPA
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width,
initial-scale=1.0\">
<title>My First SPA</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background: #f0f2f5;
}
.nav-bar {
background: #007bff;
padding: 1rem;
color: white;
}
.nav-bar a {
color: white;
text-decoration: none;
margin-right: 20px;
padding: 5px 10px;
border-radius: 4px;
}
.nav-bar a:hover {
background: rgba(255,255,255,0.2);
}
.nav-bar a.active {
background: rgba(255,255,255,0.3);
font-weight: bold;
}
.container {
max-width: 800px;
margin: 20px auto;
padding: 20px;
}
.page {
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div s-app>
*<!-- Navigation -->*
<nav class=\"nav-bar\">
<a href=\"#\" s-link=\"/\">Home</a>
<a href=\"#\" s-link=\"/about\">About</a>
<a href=\"#\" s-link=\"/contact\">Contact</a>
</nav>
*<!-- Router outlet - where pages render -->*
<div class=\"container\">
<div s-view></div>
</div>
*<!-- Route definitions -->*
<div s-route=\"/\">
<div class=\"page\">
<h1>🏠 Home Page</h1>
<p>Welcome to my first Single Page Application!</p>
<p>This content loads instantly without page refresh.</p>
<div style=\"background: #e3f2fd; padding: 20px; border-radius:
8px;\">
<h3>Quick Stats</h3>
<p>Users: 1,234</p>
<p>Posts: 567</p>
<p>Comments: 890</p>
</div>
</div>
</div>
<div s-route=\"/about\">
<div class=\"page\">
<h1>📖 About Us</h1>
<p>Learn more about our company and mission.</p>
<div style=\"display: grid; gap: 20px;\">
<div style=\"padding: 15px; background: #f8f9fa; border-radius:
8px;\">
<h3>Our Mission</h3>
<p>To make web development simple and accessible to everyone.</p>
</div>
<div style=\"padding: 15px; background: #f8f9fa; border-radius:
8px;\">
<h3>Our Team</h3>
<p>We\'re a passionate group of developers dedicated to creating
amazing tools.</p>
</div>
</div>
</div>
</div>
<div s-route=\"/contact\">
<div class=\"page\">
<h1>📧 Contact Us</h1>
<form onsubmit=\"event.preventDefault(); alert(\'Message sent!\');\">
<div style=\"margin: 15px 0;\">
<label style=\"display: block; margin-bottom: 5px;\">Name:</label>
<input type=\"text\" style=\"width: 100%; padding: 8px; border: 1px
solid #ddd; border-radius: 4px;\">
</div>
<div style=\"margin: 15px 0;\">
<label style=\"display: block; margin-bottom: 5px;\">Email:</label>
<input type=\"email\" style=\"width: 100%; padding: 8px; border: 1px
solid #ddd; border-radius: 4px;\">
</div>
<div style=\"margin: 15px 0;\">
<label style=\"display: block; margin-bottom:
5px;\">Message:</label>
<textarea rows=\"5\" style=\"width: 100%; padding: 8px; border: 1px
solid #ddd; border-radius: 4px;\"></textarea>
</div>
<button type=\"submit\" style=\"padding: 10px 20px; background:#007bff; color: white; border: none; border-radius: 4px;">
Send Message
</button>
</form>
</div>
</div>
</div>
<script type="module">
import { createApp } from 'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js';
createApp().mount('[s-app]');
</script>
</body>
</html>
13.3 The s-link Directive for Navigation
The s-link directive provides smooth, JavaScript-powered navigation without page reloads.
Advanced Link Features
<div s-app>
<link-demo></link-demo>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'link-demo\', () => {
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 20px;\">
<h2>s-link Features</h2>
<!-- Navigation -->
<nav style=\"display: flex; gap: 20px; margin: 20px 0; padding: 15px;
background: #f8f9fa; border-radius: 8px;\">
<a href=\"#\" s-link=\"/\">Home</a>
<a href=\"#\" s-link=\"/products\"
s-link-active=\"active-link\">Products</a>
<a href=\"#\" s-link=\"/services\"
s-link-active=\"active-link\">Services</a>
<a href=\"#\" s-link=\"/blog\"
s-link-active=\"active-link\">Blog</a>
<!-- External link (not handled by router) -->
<a href=\"https://example.com\" target=\"_blank\">External</a>
<!-- Link with query params -->
<a href=\"#\" s-link=\"/search?q=simplijs\">Search</a>
<!-- Link with hash -->
<a href=\"#\" s-link=\"/faq#section-2\">FAQ Section 2</a>
</nav>
<!-- Router outlet -->
<div s-view></div>
<!-- Routes -->
<div s-route=\"/\">
<div style=\"background: white; padding: 30px; border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);\">
<h1>Home Page</h1>
<p>Try the different link types above.</p>
</div>
</div>
<div s-route=\"/products\">
<div style=\"background: white; padding: 30px; border-radius: 8px;\">
<h1>Products</h1>
<p>Browse our products</p>
</div>
</div>
<div s-route=\"/services\">
<div style=\"background: white; padding: 30px; border-radius: 8px;\">
<h1>Services</h1>
<p>Our services</p>
</div>
</div>
<div s-route=\"/blog\">
<div style=\"background: white; padding: 30px; border-radius: 8px;\">
<h1>Blog</h1>
<p>Latest posts</p>
</div>
</div>
<div s-route=\"/search\">
<div style=\"background: white; padding: 30px; border-radius: 8px;\">
<h1>Search Results</h1>
<p>Showing results for: <strong>simplijs</strong></p>
</div>
</div>
<div s-route=\"/faq\">
<div style=\"background: white; padding: 30px; border-radius: 8px;\">
<h1>FAQ</h1>
<div id=\"section-1\">
<h3>Section 1</h3>
<p>Content\...</p>
</div>
<div id=\"section-2\">
<h3>Section 2</h3>
<p>This is where the hash link goes</p>
</div>
</div>
</div>
<style>
.active-link {
font-weight: bold;
color: #007bff !important;
border-bottom: 2px solid #007bff;
}
</style>
</div>
\`
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>13.4 Route Parameters and Dynamic Routing
Real applications need dynamic routes like /users/123 or /posts/my-first-post.
Dynamic Routes with Parameters
<div s-app>
<param-routing></param-routing>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'param-routing\', () => {
const state = reactive({
users: \[
{ id: 1, name: \'Alice\', email: \'alice@example.com\', role: \'Admin\'
},
{ id: 2, name: \'Bob\', email: \'bob@example.com\', role: \'User\' },
{ id: 3, name: \'Charlie\', email: \'charlie@example.com\', role:
\'User\' },
{ id: 4, name: \'Diana\', email: \'diana@example.com\', role: \'Editor\'
}
\],
posts: \[
{ id: 101, title: \'Getting Started with SimpliJS\', author: \'Alice\'
},
{ id: 102, title: \'Advanced Routing Techniques\', author: \'Bob\' },
{ id: 103, title: \'State Management Patterns\', author: \'Charlie\' }
\]
});
return {
render: () => \`
<div style=\"max-width: 1000px; margin: 20px auto; padding: 20px;\">
<h2>Dynamic Routing Demo</h2>
<!-- Navigation -->
<nav style=\"display: flex; gap: 20px; margin: 20px 0; padding: 15px;
background: #f8f9fa; border-radius: 8px;\">
<a href=\"#\" s-link=\"/\">Home</a>
<a href=\"#\" s-link=\"/users\">Users</a>
<a href=\"#\" s-link=\"/posts\">Posts</a>
</nav>
<!-- Router outlet -->
<div s-view></div>
<!-- Home Route -->
<div s-route=\"/\">
<div style=\"background: white; padding: 30px; border-radius: 8px;\">
<h1>Welcome to Dynamic Routing Demo</h1>
<p>Click on users or posts to see dynamic routes in action.</p>
<div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 20px;
margin-top: 30px;\">
<div style=\"padding: 20px; background: #e3f2fd; border-radius:
8px;\">
<h3>Users: \${state.users.length}</h3>
<p>Click a user to see their profile</p>
</div>
<div style=\"padding: 20px; background: #d4edda; border-radius:
8px;\">
<h3>Posts: \${state.posts.length}</h3>
<p>Click a post to read it</p>
</div>
</div>
</div>
</div>
<!-- Users List Route -->
<div s-route=\"/users\">
<div style=\"background: white; padding: 30px; border-radius: 8px;\">
<h1>Users</h1>
<div style=\"display: grid; gap: 15px;\">
\${state.users.map(user => \`
<div style=\"padding: 15px; background: #f8f9fa; border-radius: 8px;
display: flex; justify-content: space-between; align-items: center;\">
<div>
<strong>\${user.name}</strong>
<br>
<span style=\"color: #666;\">\${user.email}</span>
</div>
<div>
<span style=\"padding: 3px 8px; background: #007bff; color: white;
border-radius: 4px; margin-right: 10px;\">
\${user.role}
</span>
<a href=\"#\" s-link=\"/users/\${user.id}\" style=\"color:#007bff;">View Profile →</a>
</div>
</div>
`).join('')}
</div>
</div>
</div>
<!-- User Detail Route - using :id parameter -->
<div s-route="/users/:id">
<user-detail></user-detail>
</div>
<!-- Posts List Route -->
<div s-route="/posts">
<div style="background: white; padding: 30px; border-radius: 8px;">
<h1>Posts</h1>
<div style="display: grid; gap: 15px;">
${state.posts.map(post => `
<div style="padding: 15px; background: #f8f9fa; border-radius: 8px; display: flex; justify-content: space-between; align-items: center;">
<div>
<strong>${post.title}</strong>
<br>
<span style="color: #666;">By ${post.author}</span>
</div>
<a href="#" s-link="/posts/${post.id}" style="color: #007bff;">Read →</a>
</div>
`).join('')}
</div>
</div>
</div>
<!-- Post Detail Route -->
<div s-route="/posts/:id">
<post-detail></post-detail>
</div>
<!-- 404 Route - catch all -->
<div s-route="/:404">
<div style="background: white; padding: 30px; border-radius: 8px; text-align: center;">
<h1 style="font-size: 72px; color: #dc3545;">404</h1>
<h2>Page Not Found</h2>
<p>The page you're looking for doesn't exist.</p>
<a href="#" s-link="/" style="display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 4px;">
Go Home
</a>
</div>
</div>
</div>
`,
users: state.users,
posts: state.posts
};
});
// User Detail Component
component('user-detail', (element, props) => {
// In a real app, you'd fetch this data based on the route param
// For demo, we'll use hardcoded data based on the URL
const url = window.location.pathname;
const id = url.split('/').pop();
const users = {
1: { name: 'Alice', email: 'alice@example.com', role: 'Admin', bio: 'Lead developer and SimpliJS enthusiast.' },
2: { name: 'Bob', email: 'bob@example.com', role: 'User', bio: 'Frontend developer learning SimpliJS.' },
3: { name: 'Charlie', email: 'charlie@example.com', role: 'User', bio: 'Backend developer exploring frontend.' },
4: { name: 'Diana', email: 'diana@example.com', role: 'Editor', bio: 'Content manager and documentation writer.' }
};
const user = users[id] || { name: 'Unknown', email: '', role: '', bio: 'User not found' };
return {
render: () => `
<div style="background: white; padding: 30px; border-radius: 8px;">
<a href="#" s-link="/users" style="color: #007bff; text-decoration: none;">← Back to Users</a>
<div style="margin-top: 30px; text-align: center;">
<div style="width: 100px; height: 100px; background: #007bff; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 36px; margin: 0 auto;">
${user.name[0]}
</div>
<h1 style="margin: 20px 0 10px;">${user.name}</h1>
<p style="color: #666;">${user.email}</p>
<span style="display: inline-block; padding: 5px 15px; background: #007bff; color: white; border-radius: 20px;">
${user.role}
</span>
<p style="margin-top: 30px; padding: 20px; background: #f8f9fa; border-radius: 8px;">
${user.bio}
</p>
</div>
</div>
`
};
});
// Post Detail Component
component('post-detail', () => {
const url = window.location.pathname;
const id = url.split('/').pop();
const posts = {
101: {
title: 'Getting Started with SimpliJS',
author: 'Alice',
date: '2024-01-15',
content: 'SimpliJS is a revolutionary framework that makes web development simple and enjoyable. In this post, we explore the basics...',
tags: ['tutorial', 'beginner']
},
102: {
title: 'Advanced Routing Techniques',
author: 'Bob',
date: '2024-01-14',
content: 'Learn how to implement complex routing patterns in your SimpliJS applications...',
tags: ['advanced', 'routing']
},
103: {
title: 'State Management Patterns',
author: 'Charlie',
date: '2024-01-13',
content: 'Discover different approaches to managing state in large SimpliJS applications...',
tags: ['state', 'architecture']
}
};
const post = posts[id] || { title: 'Post Not Found', author: '', date: '', content: '', tags: [] };
return {
render: () => `
<div style="background: white; padding: 30px; border-radius: 8px;">
<a href="#" s-link="/posts" style="color: #007bff; text-decoration: none;">← Back to Posts</a>
<article style="margin-top: 30px;">
<h1>${post.title}</h1>
<div style="display: flex; gap: 20px; color: #666; margin: 20px 0;">
<span>By ${post.author}</span>
<span>📅 ${post.date}</span>
</div>
<div style="display: flex; gap: 10px; margin: 20px 0;">
${post.tags.map(tag => `
<span style="padding: 3px 10px; background: #e3f2fd; color: #007bff; border-radius: 20px;">${tag}</span>
`).join('')}
</div>
<div style="line-height: 1.8; color: #333;">
${post.content}
</div>
</article>
</div>
`
};
});
createApp().mount('[s-app]');
</script>
</div>
13.5 Nested Routes
Complex applications often need nested routing, like /dashboard/settings/profile.
Nested Routing Example
<div s-app>
<nested-routing></nested-routing>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'nested-routing\', () => {
return {
render: () => \`
<div style=\"max-width: 1000px; margin: 20px auto; padding: 20px;\">
<h2>Nested Routes Demo</h2>
<!-- Main Navigation -->
<nav style=\"display: flex; gap: 20px; margin: 20px 0; padding: 15px;
background: #f8f9fa; border-radius: 8px;\">
<a href=\"#\" s-link=\"/\">Home</a>
<a href=\"#\" s-link=\"/dashboard\">Dashboard</a>
<a href=\"#\" s-link=\"/settings\">Settings</a>
</nav>
<!-- Main Router Outlet -->
<div s-view></div>
<!-- Home Route -->
<div s-route=\"/\">
<div style=\"background: white; padding: 30px; border-radius: 8px;\">
<h1>Welcome Home</h1>
<p>Click on Dashboard to see nested routes in action.</p>
</div>
</div>
<!-- Dashboard Route with Nested Views -->
<div s-route=\"/dashboard\">
<div style=\"background: white; border-radius: 8px; overflow:
hidden;\">
<!-- Dashboard Navigation -->
<div style=\"display: flex; background: #f8f9fa; border-bottom: 1px
solid #ddd;\">
<a href=\"#\" s-link=\"/dashboard/overview\" style=\"flex:1; padding:
15px; text-align: center; text-decoration: none; color:#333;">Overview</a>
<a href="#" s-link="/dashboard/analytics" style="flex:1; padding: 15px; text-align: center; text-decoration: none; color: #333;">Analytics</a>
<a href="#" s-link="/dashboard/reports" style="flex:1; padding: 15px; text-align: center; text-decoration: none; color: #333;">Reports</a>
<a href="#" s-link="/dashboard/users" style="flex:1; padding: 15px; text-align: center; text-decoration: none; color: #333;">Users</a>
</div>
<!-- Nested Router Outlet -->
<div style="padding: 20px;">
<div s-view></div>
</div>
</div>
</div>
<!-- Nested Dashboard Routes -->
<div s-route="/dashboard/overview">
<div>
<h2>Dashboard Overview</h2>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-top: 20px;">
<div style="padding: 20px; background: #e3f2fd; border-radius: 8px;">
<h3>Users</h3>
<p style="font-size: 36px;">1,234</p>
</div>
<div style="padding: 20px; background: #d4edda; border-radius: 8px;">
<h3>Revenue</h3>
<p style="font-size: 36px;">$45.6K</p>
</div>
<div style="padding: 20px; background: #fff3cd; border-radius: 8px;">
<h3>Orders</h3>
<p style="font-size: 36px;">789</p>
</div>
</div>
</div>
</div>
<div s-route="/dashboard/analytics">
<div>
<h2>Analytics</h2>
<div style="height: 200px; background: #f8f9fa; display: flex; align-items: flex-end; gap: 20px; padding: 20px;">
<div style="width: 60px; height: 120px; background: #007bff;">Mon</div>
<div style="width: 60px; height: 180px; background: #28a745;">Tue</div>
<div style="width: 60px; height: 90px; background: #ffc107;">Wed</div>
<div style="width: 60px; height: 150px; background: #17a2b8;">Thu</div>
<div style="width: 60px; height: 110px; background: #dc3545;">Fri</div>
</div>
</div>
</div>
<div s-route="/dashboard/reports">
<div>
<h2>Reports</h2>
<ul style="list-style: none; padding: 0;">
<li style="padding: 15px; background: #f8f9fa; margin: 10px 0; border-radius: 4px;">
📊 Monthly Report - January 2024
</li>
<li style="padding: 15px; background: #f8f9fa; margin: 10px 0; border-radius: 4px;">
📊 Quarterly Report - Q4 2023
</li>
<li style="padding: 15px; background: #f8f9fa; margin: 10px 0; border-radius: 4px;">
📊 Annual Report - 2023
</li>
</ul>
</div>
</div>
<div s-route="/dashboard/users">
<div>
<h2>User Management</h2>
<table style="width: 100%; border-collapse: collapse;">
<tr style="background: #f8f9fa;">
<th style="padding: 10px; text-align: left;">Name</th>
<th style="padding: 10px; text-align: left;">Email</th>
<th style="padding: 10px; text-align: left;">Role</th>
</tr>
<tr>
<td style="padding: 10px;">Alice</td>
<td style="padding: 10px;">alice@example.com</td>
<td style="padding: 10px;">Admin</td>
</tr>
<tr>
<td style="padding: 10px;">Bob</td>
<td style="padding: 10px;">bob@example.com</td>
<td style="padding: 10px;">User</td>
</tr>
<tr>
<td style="padding: 10px;">Charlie</td>
<td style="padding: 10px;">charlie@example.com</td>
<td style="padding: 10px;">User</td>
</tr>
</table>
</div>
</div>
<!-- Settings Route with Nested Views -->
<div s-route="/settings">
<div style="background: white; border-radius: 8px; overflow: hidden;">
<!-- Settings Navigation -->
<div style="display: flex; background: #f8f9fa; border-bottom: 1px solid #ddd;">
<a href="#" s-link="/settings/profile" style="flex:1; padding: 15px; text-align: center;">Profile</a>
<a href="#" s-link="/settings/account" style="flex:1; padding: 15px; text-align: center;">Account</a>
<a href="#" s-link="/settings/notifications" style="flex:1; padding: 15px; text-align: center;">Notifications</a>
<a href="#" s-link="/settings/privacy" style="flex:1; padding: 15px; text-align: center;">Privacy</a>
</div>
<!-- Nested Router Outlet -->
<div style="padding: 20px;">
<div s-view></div>
</div>
</div>
</div>
<!-- Nested Settings Routes -->
<div s-route="/settings/profile">
<div>
<h2>Profile Settings</h2>
<form>
<div style="margin: 15px 0;">
<label style="display: block;">Name:</label>
<input type="text" value="John Doe" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div style="margin: 15px 0;">
<label style="display: block;">Email:</label>
<input type="email" value="john@example.com" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div style="margin: 15px 0;">
<label style="display: block;">Bio:</label>
<textarea rows="4" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">Developer and SimpliJS enthusiast</textarea>
</div>
</form>
</div>
</div>
<div s-route="/settings/account">
<div>
<h2>Account Settings</h2>
<div style="margin: 15px 0;">
<button style="padding: 10px 20px; background: #ffc107; color: #333; border: none; border-radius: 4px;">Change Password</button>
</div>
<div style="margin: 15px 0;">
<button style="padding: 10px 20px; background: #28a745; color: white; border: none; border-radius: 4px;">Upgrade Account</button>
</div>
<div style="margin: 15px 0;">
<button style="padding: 10px 20px; background: #dc3545; color: white; border: none; border-radius: 4px;">Delete Account</button>
</div>
</div>
</div>
<div s-route="/settings/notifications">
<div>
<h2>Notification Settings</h2>
<div style="margin: 15px 0;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" checked> Email notifications
</label>
</div>
<div style="margin: 15px 0;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" checked> Push notifications
</label>
</div>
<div style="margin: 15px 0;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox"> SMS notifications
</label>
</div>
</div>
</div>
<div s-route="/settings/privacy">
<div>
<h2>Privacy Settings</h2>
<div style="margin: 15px 0;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" checked> Make profile public
</label>
</div>
<div style="margin: 15px 0;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox"> Show email
</label>
</div>
<div style="margin: 15px 0;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" checked> Allow search engines to index
</label>
</div>
</div>
</div>
</div>
`
};
});
createApp().mount('[s-app]');
</script>
</div>
13.6 Route Guards and Authentication
Protect routes based on user authentication status or permissions.
Authentication with Route Guards
<div s-app>
<auth-routing></auth-routing>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'auth-routing\', () => {
const state = reactive({
isAuthenticated: false,
currentUser: null,
loginError: \'\'
});
const login = (username, password) => {
*// Simple demo authentication*
if (username === \'demo\' && password === \'password\') {
state.isAuthenticated = true;
state.currentUser = { name: \'Demo User\', role: \'user\' };
state.loginError = \'\';
*// Redirect to dashboard*
window.location.hash = \'/dashboard\';
} else {
state.loginError = \'Invalid credentials\';
}
};
const logout = () => {
state.isAuthenticated = false;
state.currentUser = null;
window.location.hash = \'/\';
};
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 20px;\">
<h2>Authentication with Route Guards</h2>
<!-- Navigation -->
<nav style=\"display: flex; gap: 20px; margin: 20px 0; padding: 15px;
background: #f8f9fa; border-radius: 8px; align-items: center;\">
<a href=\"#\" s-link=\"/\">Home</a>
<a href=\"#\" s-link=\"/public\">Public</a>
\${state.isAuthenticated ? \`
<a href=\"#\" s-link=\"/dashboard\">Dashboard</a>
<a href=\"#\" s-link=\"/profile\">Profile</a>
<span style=\"flex:1; text-align: right;\">
Welcome, \${state.currentUser?.name}!
<button onclick=\"this.closest(\'auth-routing\').logout()\"
style=\"margin-left: 10px; padding: 5px 10px; background: #dc3545;
color: white; border: none; border-radius: 4px;\">
Logout
</button>
</span>
\` : \'\'}
</nav>
<!-- Router outlet -->
<div s-view></div>
<!-- Public Routes (no auth needed) -->
<div s-route=\"/\">
<div style=\"background: white; padding: 30px; border-radius: 8px;\">
<h1>Welcome to the App</h1>
<p>This is a public page. Anyone can see this.</p>
<div style=\"margin-top: 30px; padding: 20px; background: #e3f2fd;
border-radius: 8px;\">
<h3>Not logged in?</h3>
<p>Click below to access the login form.</p>
<a href=\"#\" s-link=\"/login\" style=\"display: inline-block; padding:
10px 20px; background: #28a745; color: white; text-decoration: none;
border-radius: 4px;\">Go to Login
</a>
</div>
</div>
</div>
<div s-route="/public">
<div style="background: white; padding: 30px; border-radius: 8px;">
<h1>Public Information</h1>
<p>This content is accessible to everyone.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
</div>
</div>
<!-- Login Route -->
<div s-route="/login">
<div style="background: white; padding: 30px; border-radius: 8px; max-width: 400px; margin: 0 auto;">
<h2>Login</h2>
${state.loginError ? `
<div style="padding: 15px; background: #f8d7da; color: #721c24; border-radius: 4px; margin: 20px 0;">
${state.loginError}
</div>
` : ''}
<form onsubmit="event.preventDefault();
this.closest('auth-routing').login(
document.getElementById('username').value,
document.getElementById('password').value
);">
<div style="margin: 15px 0;">
<label>Username:</label>
<input type="text" id="username" value="demo" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div style="margin: 15px 0;">
<label>Password:</label>
<input type="password" id="password" value="password" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<button type="submit" style="width: 100%; padding: 10px; background: #007bff; color: white; border: none; border-radius: 4px;">
Sign In
</button>
</form>
<p style="margin-top: 20px; font-size: 14px; color: #666;">
Demo credentials: demo / password
</p>
</div>
</div>
<!-- Protected Routes (require auth) -->
<div s-route="/dashboard" s-guard="auth">
${state.isAuthenticated ? `
<div style="background: white; padding: 30px; border-radius: 8px;">
<h1>Dashboard</h1>
<p>Welcome to your protected dashboard!</p>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-top: 30px;">
<div style="padding: 20px; background: #e3f2fd; border-radius: 8px;">
<h3>Stats</h3>
<p>Total visits: 1,234</p>
</div>
<div style="padding: 20px; background: #d4edda; border-radius: 8px;">
<h3>Activity</h3>
<p>Last login: Today</p>
</div>
</div>
</div>
` : `
<div style="background: white; padding: 30px; border-radius: 8px; text-align: center;">
<h2>🔒 Authentication Required</h2>
<p>Please log in to access this page.</p>
<a href="#" s-link="/login" style="display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 4px;">
Go to Login
</a>
</div>
`}
</div>
<div s-route="/profile" s-guard="auth">
${state.isAuthenticated ? `
<div style="background: white; padding: 30px; border-radius: 8px;">
<h1>User Profile</h1>
<div style="margin: 20px 0;">
<p><strong>Name:</strong> ${state.currentUser?.name}</p>
<p><strong>Role:</strong> ${state.currentUser?.role}</p>
</div>
<button onclick="this.closest('auth-routing').logout()"
style="padding: 10px 20px; background: #dc3545; color: white; border: none; border-radius: 4px;">
Logout
</button>
</div>
` : `
<div style="background: white; padding: 30px; border-radius: 8px; text-align: center;">
<h2>🔒 Authentication Required</h2>
<p>Please log in to view your profile.</p>
<a href="#" s-link="/login" style="display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 4px;">
Go to Login
</a>
</div>
`}
</div>
<!-- 404 Route -->
<div s-route="/:404">
<div style="background: white; padding: 30px; border-radius: 8px; text-align: center;">
<h1 style="font-size: 72px; color: #dc3545;">404</h1>
<h2>Page Not Found</h2>
<a href="#" s-link="/" style="display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 4px;">
Go Home
</a>
</div>
</div>
</div>
`,
login,
logout,
isAuthenticated: state.isAuthenticated
};
});
createApp().mount('[s-app]');
</script>
</div>
13.7 Route Transitions and Animations
Add smooth transitions between routes for a polished user experience.
Animated Route Transitions
<div s-app>
<animated-routing></animated-routing>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'animated-routing\', () => {
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 20px;\">
<h2>Animated Route Transitions</h2>
<style>
.fade-enter {
opacity: 0;
transform: translateY(20px);
}
.fade-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 0.3s, transform 0.3s;
}
.fade-exit {
opacity: 1;
transform: translateY(0);
}
.fade-exit-active {
opacity: 0;
transform: translateY(-20px);
transition: opacity 0.3s, transform 0.3s;
}
.slide-enter {
transform: translateX(100%);
}
.slide-enter-active {
transform: translateX(0);
transition: transform 0.4s ease;
}
.slide-exit {
transform: translateX(0);
}
.slide-exit-active {
transform: translateX(-100%);
transition: transform 0.4s ease;
}
.page {
position: absolute;
width: 100%;
left: 0;
right: 0;
}
.router-container {
position: relative;
min-height: 400px;
overflow: hidden;
}
</style>
<!-- Transition Controls -->
<div style=\"margin: 20px 0; padding: 15px; background: #f8f9fa;
border-radius: 8px;\">
<label style=\"margin-right: 20px;\">
<input type=\"radio\" name=\"transition\" value=\"fade\" checked
onchange=\"this.closest(\'animated-routing\').setTransition(\'fade\')\">
Fade
</label>
<label>
<input type=\"radio\" name=\"transition\" value=\"slide\"
onchange=\"this.closest(\'animated-routing\').setTransition(\'slide\')\">
Slide
</label>
</div>
<!-- Navigation -->
<nav style=\"display: flex; gap: 10px; margin: 20px 0;\">
<a href=\"#\" s-link=\"/page1\" style=\"flex:1; padding: 15px;
background: #007bff; color: white; text-align: center; text-decoration:
none; border-radius: 4px;\">Page 1</a>
<a href=\"#\" s-link=\"/page2\" style=\"flex:1; padding: 15px;
background: #28a745; color: white; text-align: center; text-decoration:
none; border-radius: 4px;\">Page 2</a>
<a href=\"#\" s-link=\"/page3\" style=\"flex:1; padding: 15px;
background: #ffc107; color: #333; text-align: center; text-decoration:
none; border-radius: 4px;\">Page 3</a>
</nav>
<!-- Router Container with Transition -->
<div class=\"router-container\" id=\"routerContainer\">
<div s-view id=\"routerView\"></div>
</div>
<!-- Routes -->
<div s-route=\"/page1\">
<div class=\"page\" style=\"background: white; padding: 40px;
border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);\">
<h1 style=\"color: #007bff;\">Page 1</h1>
<p>This is the first page with animated transitions.</p>
<div style=\"height: 200px; background: linear-gradient(135deg,#007bff, #00d4ff); border-radius: 8px;"></div>
</div>
</div>
<div s-route="/page2">
<div class="page" style="background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<h1 style="color: #28a745;">Page 2</h1>
<p>This is the second page with animated transitions.</p>
<div style="height: 200px; background: linear-gradient(135deg, #28a745, #8bc34a); border-radius: 8px;"></div>
</div>
</div>
<div s-route="/page3">
<div class="page" style="background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<h1 style="color: #ffc107;">Page 3</h1>
<p>This is the third page with animated transitions.</p>
<div style="height: 200px; background: linear-gradient(135deg, #ffc107, #ff9800); border-radius: 8px;"></div>
</div>
</div>
</div>
`,
setTransition: (type) => {
const routerView = document.getElementById('routerView');
if (routerView) {
routerView.className = '';
routerView.classList.add(type + '-enter');
setTimeout(() => {
routerView.classList.remove(type + '-enter');
routerView.classList.add(type + '-enter-active');
}, 10);
}
}
};
});
createApp().mount('[s-app]');
</script>
</div>
Chapter 13 Summary
You've now mastered SPA routing in SimpliJS:
Basic routing with s-route and s-view
Navigation with s-link for smooth transitions
Route parameters for dynamic content
Nested routes for complex layouts
Route guards for authentication
Transitions and animations for polished UX
404 handling for unmatched routes
Routing transforms your application from a simple page into a full-featured SPA with seamless navigation and deep linking capabilities.
In the next chapter, we'll explore static site generation (SSG) for SEO optimization and performance.
End of Chapter 13
Chapter 14: Static Site Generation (SSG)
Welcome to Chapter 14, where we explore how SimpliJS can generate static sites for optimal performance and SEO. Static Site Generation (SSG) pre-renders your pages at build time, creating HTML files that load instantly and are fully indexable by search engines.
14.1 Understanding Static Site Generation
Before diving into implementation, let's understand why SSG matters and how it differs from client-side rendering.
The Rendering Spectrum
<div s-app>
<ssg-explanation></ssg-explanation>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'ssg-explanation\', () => {
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 30px;\">
<h2>Rendering Approaches Compared</h2>
<div style=\"display: grid; gap: 20px; margin: 30px 0;\">
*<!-- CSR -->*
<div style=\"padding: 20px; background: #fff3cd; border-radius:
8px;\">
<h3 style=\"color: #856404;\">1. Client-Side Rendering (CSR)</h3>
<div style=\"display: flex; align-items: center; gap: 20px;\">
<div style=\"flex:1;\">
<code>Empty HTML → JS Load → Render</code>
<ul style=\"margin-top: 10px;\">
<li>❌ Slow initial load</li>
<li>❌ Poor SEO</li>
<li>✅ Dynamic after load</li>
</ul>
</div>
<div style=\"flex:1; background: white; padding: 10px; border-radius:
4px;\">
<div style=\"background: #ddd; height: 20px; width: 100%; margin: 5px
0;\"></div>
<div style=\"background: #ddd; height: 20px; width: 80%; margin: 5px
0;\"></div>
<div style=\"background: #007bff; height: 20px; width: 0%; margin: 5px
0;\" id=\"loadingBar\"></div>
</div>
</div>
</div>
*<!-- SSR -->*
<div style=\"padding: 20px; background: #d4edda; border-radius:
8px;\">
<h3 style=\"color: #155724;\">2. Server-Side Rendering (SSR)</h3>
<div style=\"display: flex; align-items: center; gap: 20px;\">
<div style=\"flex:1;\">
<code>Server renders → HTML sent → Hydrate</code>
<ul style=\"margin-top: 10px;\">
<li>✅ Good SEO</Chapter 14: Static Site Generation (SSG)
Welcome to Chapter 14, where we explore how SimpliJS can generate static sites for optimal performance and SEO. Static Site Generation (SSG) pre-renders your pages at build time, creating HTML files that load instantly and are fully indexable by search engines.
14.1 Understanding Static Site Generation
Before diving into implementation, let's understand why SSG matters and how it differs from client-side rendering.
The Rendering Spectrum
<div s-app>
<ssg-explanation></ssg-explanation>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'ssg-explanation\', () => {
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 30px;\">
<h2>Rendering Approaches Compared</h2>
<div style=\"display: grid; gap: 20px; margin: 30px 0;\">
<!-- CSR -->
<div style=\"padding: 20px; background: #fff3cd; border-radius:
8px;\">
<h3 style=\"color: #856404;\">1. Client-Side Rendering (CSR)</h3>
<div style=\"display: flex; align-items: center; gap: 20px;\">
<div style=\"flex:1;\">
<code>Empty HTML → JS Load → Render</code>
<ul style=\"margin-top: 10px;\">
<li>❌ Slow initial load</li>
<li>❌ Poor SEO</li>
<li>✅ Dynamic after load</li>
</ul>
</div>
<div style=\"flex:1; background: white; padding: 10px; border-radius:
4px;\">
<div style=\"background: #ddd; height: 20px; width: 100%; margin: 5px
0;\"></div>
<div style=\"background: #ddd; height: 20px; width: 80%; margin: 5px
0;\"></div>
<div style=\"background: #007bff; height: 20px; width: 0%; margin: 5px
0;\" id=\"loadingBar\"></div>
</div>
</div>
</div>
<!-- SSR -->
<div style=\"padding: 20px; background: #d4edda; border-radius:
8px;\">
<h3 style=\"color: #155724;\">2. Server-Side Rendering (SSR)</h3>
<div style=\"display: flex; align-items: center; gap: 20px;\">
<div style=\"flex:1;\">
<code>Server renders → HTML sent → Hydrate</code>
<ul style=\"margin-top: 10px;\">
<li>✅ Good SEO</li>
<li>✅ Faster initial view</li>
<li>❌ Server load</li>
<li>❌ Slower navigation</li>
</ul>
</div>
<div style=\"flex:1; background: white; padding: 10px; border-radius:
4px;\">
<div style=\"background: #28a745; height: 20px; width: 100%; margin:
5px 0;\"></div>
<div style=\"background: #28a745; height: 20px; width: 80%; margin: 5px
0;\"></div>
<div style=\"background: #007bff; height: 20px; width: 60%; margin: 5px
0;\"></div>
</div>
</div>
</div>
<!-- SSG -->
<div style=\"padding: 20px; background: #cce5ff; border-radius: 8px;
border: 2px solid #007bff;\">
<h3 style=\"color: #004085;\">3. Static Site Generation (SSG)
⭐</h3>
<div style=\"display: flex; align-items: center; gap: 20px;\">
<div style=\"flex:1;\">
<code>Build time → Static HTML → Instant load</code>
<ul style=\"margin-top: 10px;\">
<li>✅ Excellent SEO</li>
<li>✅ Lightning fast</li>
<li>✅ Low server cost</li>
<li>✅ Works without JS</li>
<li>✅ CDN friendly</li>
</ul>
</div>
<div style=\"flex:1; background: white; padding: 10px; border-radius:
4px;\">
<div style=\"background: #007bff; height: 20px; width: 100%; margin:
5px 0;\"></div>
<div style=\"background: #007bff; height: 20px; width: 100%; margin:
5px 0;\"></div>
<div style=\"background: #007bff; height: 20px; width: 100%; margin:
5px 0;\"></div>
</div>
</div>
</div>
</div>
<div style=\"background: #e8f4fd; padding: 20px; border-radius:
8px;\">
<h3>How SSG Works in SimpliJS</h3>
<ol>
<li><strong>Build Time:</strong> SimpliJS crawls your routes and
renders each page to static HTML</li>
<li><strong>Generation:</strong> Creates individual HTML files,
sitemap, RSS feed, and assets</li>
<li><strong>Deployment:</strong> Upload static files to any
hosting service or CDN</li>
<li><strong>Runtime:</strong> Browser receives fully-formed HTML
instantly</li>
<li><strong>Hydration:</strong> JavaScript adds interactivity
after initial load</li>
</ol>
</div>
</div>
\`
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>14.2 Setting Up SSG in SimpliJS
SimpliJS includes a powerful built-in SSG engine that makes static site generation simple.
Basic SSG Configuration
First, let's create a basic SSG configuration file:
*// ssg.config.js - SSG Configuration File*
export default {
*// Basic settings*
baseUrl: \'https://my-simplijs-site.com\',
outDir: \'dist\',
minify: true,
*// Routes to generate*
routes: {
\'/\': \'HomePage\',
\'/about\': \'AboutPage\',
\'/blog\': \'BlogPage\',
\'/contact\': \'ContactPage\'
},
*// Assets to preload*
preload: \[
\'/src/index.js\',
\'/styles/main.css\',
\'/fonts/custom.woff2\'
\],
*// SEO settings*
seo: {
title: \'My SimpliJS Site\',
description: \'A blazing fast static site built with SimpliJS\',
image: \'/images/og-image.png\',
twitterHandle: \'@simplijs\'
},
*// Sitemap generation*
sitemap: true,
*// RSS feed*
rss: {
title: \'My Blog\',
description: \'Latest posts from my SimpliJS site\',
items: \[\] *// Will be populated from blog posts*
}
};Project Structure for SSG
Here's how to organize your project for SSG:
<!DOCTYPE html>
<html>
<head>
<title>SSG Project Structure</title>
</head>
<body>
<div s-app>
<project-structure></project-structure>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'project-structure\', () => {
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 30px;
background: #1e1e1e; color: #d4d4d4; font-family: monospace;
border-radius: 8px;\">
<h2 style=\"color: #fff;\">SSG Project Structure</h2>
<pre style=\"background: #2d2d2d; padding: 20px; border-radius: 8px;
overflow-x: auto;\">
my-simplijs-site/
├── src/
│ ├── components/
│ │ ├── Header.js
│ │ ├── Footer.js
│ │ └── BlogPost.js
│ ├── pages/
│ │ ├── Home.js
│ │ ├── About.js
│ │ ├── Blog.js
│ │ └── Contact.js
│ ├── styles/
│ │ └── main.css
│ └── index.js
├── public/
│ ├── images/
│ └── favicon.ico
├── ssg.config.js
├── package.json
└── index.html
</pre>
<div style=\"margin-top: 20px; padding: 15px; background: #2d2d2d;
border-radius: 8px;\">
<h4 style=\"color: #fff;\">Build Command:</h4>
<code style=\"color: #6a9955;\">node ssg.js ssg.config.js</code>
</div>
<div style=\"margin-top: 20px; padding: 15px; background: #2d2d2d;
border-radius: 8px;\">
<h4 style=\"color: #fff;\">Generated Output:</h4>
<pre style=\"color: #6a9955;\">
dist/
├── index.html
├── about/
│ └── index.html
├── blog/
│ ├── index.html
│ ├── post-1/
│ │ └── index.html
│ ├── post-2/
│ │ └── index.html
│ └── post-3/
│ └── index.html
├── contact/
│ └── index.html
├── sitemap.xml
├── rss.xml
├── robots.txt
└── assets/
├── js/
├── css/
└── images/
</pre>
</div>
</div>
\`
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>
</body>
</html>14.3 SEO Helpers and Metadata Management
SimpliJS provides built-in SEO helpers to manage metadata across your static site.
SEO Helper Functions
<!DOCTYPE html>
<html>
<head>
<title>SEO Helpers Demo</title>
</head>
<body>
<div s-app>
<seo-demo></seo-demo>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// Import SEO helpers (in a real app, these would be imported from
simplijs)*
*// For demo purposes, we\'ll simulate them*
component(\'seo-demo\', () => {
*// Simulated SEO state*
const seoState = {
title: \'Default Title\',
description: \'Default description\',
image: \'/default-og.jpg\',
url: \'https://example.com\',
twitterHandle: \'@simplijs\',
themeColor: \'#007bff\',
breadcrumbs: \[\],
jsonLd: null
};
*// Simulated SEO helper functions*
const setSEO = (data) => {
Object.assign(seoState, data);
console.log(\'SEO Updated:\', seoState);
};
const setThemeColor = (color) => {
seoState.themeColor = color;
document.querySelector(\'meta\[name=\"theme-color\"\]\')?.setAttribute(\'content\',
color);
};
const setBreadcrumbs = (crumbs) => {
seoState.breadcrumbs = crumbs;
};
const setJsonLd = (data) => {
seoState.jsonLd = data;
};
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 30px;\">
<h2>SEO Helpers Demo</h2>
<!-- Demo Controls -->
<div style=\"display: grid; gap: 20px; margin: 30px 0;\">
<!-- Basic SEO -->
<div style=\"padding: 20px; background: #f8f9fa; border-radius:
8px;\">
<h3>Basic SEO</h3>
<div style=\"margin: 10px 0;\">
<label>Page Title:</label>
<input type=\"text\"
value=\"\${seoState.title}\"
oninput=\"this.closest(\'seo-demo\').updateTitle(this.value)\"
style=\"width: 100%; padding: 8px; margin: 5px 0;\">
</div>
<div style=\"margin: 10px 0;\">
<label>Description:</label>
<textarea
oninput=\"this.closest(\'seo-demo\').updateDescription(this.value)\"
style=\"width: 100%; padding: 8px; margin: 5px
0;\">\${seoState.description}</textarea>
</div>
<div style=\"margin: 10px 0;\">
<label>OG Image URL:</label>
<input type=\"text\"
value=\"\${seoState.image}\"
oninput=\"this.closest(\'seo-demo\').updateImage(this.value)\"
style=\"width: 100%; padding: 8px; margin: 5px 0;\">
</div>
</div>
<!-- Theme Color -->
<div style=\"padding: 20px; background: #f8f9fa; border-radius:
8px;\">
<h3>Theme Color</h3>
<input type=\"color\"
value=\"\${seoState.themeColor}\"
onchange=\"this.closest(\'seo-demo\').updateThemeColor(this.value)\"
style=\"width: 100%; height: 50px;\">
</div>
<!-- Breadcrumbs -->
<div style=\"padding: 20px; background: #f8f9fa; border-radius:
8px;\">
<h3>Breadcrumbs</h3>
<button onclick=\"this.closest(\'seo-demo\').setBreadcrumbs()\"
style=\"padding: 10px; background: #007bff; color: white; border: none;
border-radius: 4px;\">
Set Breadcrumbs
</button>
<div style=\"margin-top: 10px; padding: 10px; background: white;
border-radius: 4px;\">
\${seoState.breadcrumbs.map(crumb =>
\`<span style=\"margin: 0 5px;\">\${crumb.name} →</span>\`
).join(\'\')}
</div>
</div>
<!-- JSON-LD -->
<div style=\"padding: 20px; background: #f8f9fa; border-radius:
8px;\">
<h3>JSON-LD Structured Data</h3>
<button onclick=\"this.closest(\'seo-demo\').setJsonLd()\"
style=\"padding: 10px; background: #28a745; color: white; border: none;
border-radius: 4px;\">Add Article Schema
</button>
<pre style="margin-top: 10px; padding: 10px; background: white; border-radius: 4px; overflow-x: auto;">
${JSON.stringify(seoState.jsonLd, null, 2) || '{}'}
</pre>
</div>
</div>
<!-- SEO Preview -->
<div style="margin-top: 30px; padding: 20px; background: #e8f4fd; border-radius: 8px;">
<h3>Google Search Preview</h3>
<div style="background: white; padding: 20px; border-radius: 8px; border: 1px solid #ddd;">
<div style="color: #1a0dab; font-size: 20px;">${seoState.title}</div>
<div style="color: #006621; font-size: 14px;">${seoState.url}</div>
<div style="color: #545454; font-size: 14px; margin-top: 5px;">${seoState.description}</div>
</div>
</div>
<!-- Generated Meta Tags -->
<div style="margin-top: 30px; padding: 20px; background: #333; color: #fff; border-radius: 8px; font-family: monospace;">
<h4 style="color: #fff;">Generated Meta Tags:</h4>
<pre style="color: #6a9955;">
<title>${seoState.title}</title>
<meta name="description" content="${seoState.description}">
<meta property="og:title" content="${seoState.title}">
<meta property="og:description" content="${seoState.description}">
<meta property="og:image" content="${seoState.image}">
<meta property="og:url" content="${seoState.url}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="${seoState.twitterHandle}">
<meta name="theme-color" content="${seoState.themeColor}">
</pre>
</div>
</div>
`,
updateTitle: (value) => {
setSEO({ title: value });
},
updateDescription: (value) => {
setSEO({ description: value });
},
updateImage: (value) => {
setSEO({ image: value });
},
updateThemeColor: (value) => {
setThemeColor(value);
},
setBreadcrumbs: () => {
setBreadcrumbs([
{ name: 'Home', url: '/' },
{ name: 'Blog', url: '/blog' },
{ name: 'Post Title', url: '/blog/post' }
]);
},
setJsonLd: () => {
setJsonLd({
"@context": "https://schema.org",
"@type": "Article",
"headline": seoState.title,
"description": seoState.description,
"image": seoState.image,
"author": {
"@type": "Person",
"name": "John Doe"
},
"datePublished": "2024-01-15",
"dateModified": "2024-01-15"
});
}
};
});
createApp().mount('[s-app]');
</script>
</div>
</body>
</html>
14.4 Building a Blog with SSG
Let's build a complete blog using SimpliJS SSG features.
Blog Project Structure
*// ssg.config.js - Blog Configuration*
export default {
baseUrl: \'https://my-blog.com\',
outDir: \'dist\',
minify: true,
*// Blog routes*
routes: {
\'/\': \'HomePage\',
\'/blog\': \'BlogPage\',
\'/blog/:slug\': \'BlogPostPage\',
\'/about\': \'AboutPage\',
\'/contact\': \'ContactPage\'
},
*// Blog posts data*
blogPosts: \[
{
slug: \'getting-started-with-simplijs\',
title: \'Getting Started with SimpliJS\',
date: \'2024-01-15\',
author: \'John Doe\',
excerpt: \'Learn how to build amazing apps with SimpliJS\...\',
content: \'\...\',
tags: \[\'tutorial\', \'beginners\'\],
image: \'/images/blog/getting-started.jpg\'
},
{
slug: \'advanced-ssg-techniques\',
title: \'Advanced SSG Techniques\',
date: \'2024-01-14\',
author: \'Jane Smith\',
excerpt: \'Take your static sites to the next level\...\',
content: \'\...\',
tags: \[\'advanced\', \'performance\'\],
image: \'/images/blog/advanced-ssg.jpg\'
}
\],
*// Generate RSS feed from blog posts*
rss: {
title: \'My SimpliJS Blog\',
description: \'Latest articles about SimpliJS and web development\',
items: (config) => config.blogPosts.map(post => ({
title: post.title,
url: \`/blog/\${post.slug}\`,
description: post.excerpt,
date: post.date
}))
},
*// Generate sitemap*
sitemap: true,
*// Preload critical assets*
preload: \[
\'/src/index.js\',
\'/styles/blog.css\',
\'/fonts/inter.woff2\'
\],
*// SEO defaults*
seo: {
title: \'My SimpliJS Blog\',
description: \'A blog about SimpliJS and modern web development\',
image: \'/images/og-default.jpg\',
twitterHandle: \'@simplijs\'
}
};Blog Components
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width,
initial-scale=1.0\">
<title>SimpliJS Blog</title>
<style>
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto,
sans-serif;
margin: 0;
padding: 0;
background: #f8f9fa;
color: #333;
line-height: 1.6;
}
.container { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
.blog-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 60px 0;
text-align: center;
}
.blog-title { font-size: 48px; margin: 0; }
.blog-subtitle { font-size: 18px; opacity: 0.9; margin-top: 10px; }
.nav-bar {
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 15px 0;
}
.nav-links {
display: flex;
gap: 30px;
justify-content: center;
}
.nav-links a {
color: #333;
text-decoration: none;
font-weight: 500;
}
.nav-links a:hover { color: #667eea; }
.post-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 30px;
margin: 40px 0;
}
.post-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
transition: transform 0.3s, box-shadow 0.3s;
}
.post-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.15);
}
.post-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.post-content { padding: 20px; }
.post-title {
margin: 0 0 10px;
font-size: 24px;
color: #333;
}
.post-meta {
color: #666;
font-size: 14px;
margin-bottom: 15px;
}
.post-excerpt {
color: #666;
margin-bottom: 20px;
}
.read-more {
color: #667eea;
text-decoration: none;
font-weight: 600;
}
.post-tags {
display: flex;
gap: 10px;
margin: 15px 0;
}
.tag {
background: #e3f2fd;
color: #1976d2;
padding: 3px 10px;
border-radius: 20px;
font-size: 12px;
}
.blog-footer {
background: #333;
color: white;
text-align: center;
padding: 40px 0;
margin-top: 60px;
}
.pagination {
display: flex;
justify-content: center;
gap: 10px;
margin: 40px 0;
}
.pagination button {
padding: 10px 15px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 4px;
}
.pagination button.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.post-full {
background: white;
padding: 40px;
border-radius: 12px;
margin: 40px 0;
}
.post-full img {
max-width: 100%;
border-radius: 8px;
}
.post-full h1 { font-size: 42px; margin-top: 0; }
</style>
</head>
<body>
<div s-app>
*<!-- Blog Header -->*
<header class=\"blog-header\">
<div class=\"container\">
<h1 class=\"blog-title\">📝 SimpliJS Blog</h1>
<p class=\"blog-subtitle\">Thoughts, tutorials, and updates about
SimpliJS</p>
</div>
</header>
*<!-- Navigation -->*
<nav class=\"nav-bar\">
<div class=\"nav-links\">
<a href=\"#\" s-link=\"/\">Home</a>
<a href=\"#\" s-link=\"/blog\">Blog</a>
<a href=\"#\" s-link=\"/about\">About</a>
<a href=\"#\" s-link=\"/contact\">Contact</a>
</div>
</nav>
*<!-- Router Outlet -->*
<main class=\"container\">
<div s-view></div>
</main>
*<!-- Footer -->*
<footer class=\"blog-footer\">
<div class=\"container\">
<p>© 2024 SimpliJS Blog. Built with ❤️ using SimpliJS SSG.</p>
<p style=\"margin-top: 10px; opacity: 0.7;\">
<a href=\"/sitemap.xml\" style=\"color: white;\">Sitemap</a> \|
<a href=\"/rss.xml\" style=\"color: white;\">RSS Feed</a> \|
<a href=\"/privacy\" style=\"color: white;\">Privacy</a>
</p>
</div>
</footer>
*<!-- Routes -->*
*<!-- Home Page -->*
<div s-route=\"/\">
<home-page></home-page>
</div>
*<!-- Blog Listing Page -->*
<div s-route=\"/blog\">
<blog-listing></blog-listing>
</div>
*<!-- Blog Post Page (with slug parameter) -->*
<div s-route=\"/blog/:slug\">
<blog-post></blog-post>
</div>
*<!-- About Page -->*
<div s-route=\"/about\">
<div style=\"background: white; padding: 40px; border-radius: 12px;
margin: 40px 0;\">
<h1>About This Blog</h1>
<p>Welcome to the SimpliJS blog! Here we share tutorials, best
practices, and updates about the SimpliJS framework.</p>
<p>Our goal is to make web development simple and enjoyable for
everyone.</p>
<div style=\"display: grid; grid-template-columns: repeat(3, 1fr); gap:
30px; margin-top: 40px;\">
<div style=\"text-align: center;\">
<div style=\"font-size: 48px;\">📚</div>
<h3>Tutorials</h3>
<p>Step-by-step guides to master SimpliJS</p>
</div>
<div style=\"text-align: center;\">
<div style=\"font-size: 48px;\">🚀</div>
<h3>Best Practices</h3>
<p>Learn how to build scalable apps</p>
</div>
<div style=\"text-align: center;\">
<div style=\"font-size: 48px;\">🎉</div>
<h3>Updates</h3>
<p>Latest features and improvements</p>
</div>
</div>
</div>
</div>
*<!-- Contact Page -->*
<div s-route=\"/contact\">
<div style=\"background: white; padding: 40px; border-radius: 12px;
margin: 40px 0;\">
<h1>Contact Us</h1>
<form onsubmit=\"event.preventDefault(); alert(\'Message sent!\');\">
<div style=\"margin: 20px 0;\">
<label style=\"display: block; margin-bottom: 5px;\">Name:</label>
<input type=\"text\" style=\"width: 100%; padding: 10px; border: 2px
solid #ddd; border-radius: 4px;\">
</div>
<div style=\"margin: 20px 0;\">
<label style=\"display: block; margin-bottom: 5px;\">Email:</label>
<input type=\"email\" style=\"width: 100%; padding: 10px; border: 2px
solid #ddd; border-radius: 4px;\">
</div>
<div style=\"margin: 20px 0;\">
<label style=\"display: block; margin-bottom:
5px;\">Message:</label>
<textarea rows=\"5\" style=\"width: 100%; padding: 10px; border: 2px
solid #ddd; border-radius: 4px;\"></textarea>
</div>
<button type=\"submit\" style=\"padding: 12px 30px; background:#667eea; color: white; border: none; border-radius: 4px; font-size: 16px;">
Send Message
</button>
</form>
</div>
</div>
</div>
<script type="module">
import { createApp, component, reactive } from 'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js';
// Blog data (in real app, this would come from a CMS or markdown files)
const blogPosts = [
{
slug: 'getting-started-with-simplijs',
title: 'Getting Started with SimpliJS',
date: '2024-01-15',
author: 'John Doe',
excerpt: 'Learn how to build amazing apps with SimpliJS, the revolutionary framework that makes web development simple and enjoyable.',
content: `
<p>SimpliJS is a revolutionary framework that changes how we think about web development. In this post, we'll explore the basics and build your first app.</p>
<h2>Why SimpliJS?</h2>
<p>Traditional frameworks require complex build setups and configuration. SimpliJS works directly in the browser with zero build steps.</p>
<h2>Getting Started</h2>
<p>Just include SimpliJS via CDN and start writing components. Here's a simple example:</p>
<pre><code><div s-app s-state="{ count: 0 }">
<h1>Count: {count}</h1>
<button s-click="count++">Increment</button>
</div></code></pre>
<h2>Key Features</h2>
<ul>
<li>Zero configuration</li>
<li>HTML-First approach</li>
<li>Proxy-based reactivity</li>
<li>Built-in SSG</li>
</ul>
`,
image: 'https://picsum.photos/800/400?random=1',
tags: ['tutorial', 'beginners']
},
{
slug: 'advanced-ssg-techniques',
title: 'Advanced SSG Techniques',
date: '2024-01-14',
author: 'Jane Smith',
excerpt: 'Take your static sites to the next level with these advanced SSG techniques in SimpliJS.',
content: `
<p>Static Site Generation is powerful, but there's more than meets the eye. Let's explore advanced techniques.</p>
<h2>Dynamic Routes with SSG</h2>
<p>You can generate thousands of pages from dynamic data sources like CMS or markdown files.</p>
<h2>Incremental Static Regeneration</h2>
<p>Update your static content without rebuilding the entire site.</p>
<h2>Optimizing Images</h2>
<p>Automatically optimize and generate responsive images during build.</p>
`,
image: 'https://picsum.photos/800/400?random=2',
tags: ['advanced', 'performance']
},
{
slug: 'state-management-patterns',
title: 'State Management Patterns',
date: '2024-01-13',
author: 'Bob Johnson',
excerpt: 'Discover different approaches to managing state in large SimpliJS applications.',
content: `
<p>As your app grows, state management becomes crucial. Here are proven patterns for SimpliJS.</p>
<h2>Reactive State</h2>
<p>Use the reactive() function for local component state.</p>
<h2>Global Stores</h2>
<p>Share state across components using the global event bus.</p>
<h2>Time Travel</h2>
<p>The Time Vault plugin enables undo/redo and debugging.</p>
`,
image: 'https://picsum.photos/800/400?random=3',
tags: ['state', 'architecture']
},
{
slug: 'building-reusable-components',
title: 'Building Reusable Components',
date: '2024-01-12',
author: 'Alice Williams',
excerpt: 'Learn how to create flexible, reusable components that can be shared across projects.',
content: `
<p>Components are the building blocks of SimpliJS apps. Here's how to make them reusable.</p>
<h2>Props and Slots</h2>
<p>Use props for data and slots for content injection.</p>
<h2>Component Composition</h2>
<p>Build complex UIs by composing simpler components.</p>
<h2>Publishing Components</h2>
<p>Share your components via npm or CDN.</p>
`,
image: 'https://picsum.photos/800/400?random=4',
tags: ['components', 'best-practices']
}
];
// Home Page Component
component('home-page', () => {
const featured = blogPosts.slice(0, 3);
return {
render: () => `
<div style="margin: 40px 0;">
<section style="text-align: center; padding: 60px 20px; background: white; border-radius: 12px;">
<h1 style="font-size: 48px; margin: 0 0 20px;">Welcome to SimpliJS Blog</h1>
<p style="font-size: 18px; color: #666; max-width: 600px; margin: 0 auto;">
Discover tutorials, best practices, and updates about the SimpliJS framework.
</p>
</section>
<section style="margin: 60px 0;">
<h2 style="text-align: center;">Featured Posts</h2>
<div class="post-grid">
${featured.map(post => `
<article class="post-card">
<img src="${post.image}" alt="${post.title}" class="post-image">
<div class="post-content">
<h3 class="post-title">${post.title}</h3>
<div class="post-meta">
By ${post.author} • ${post.date}
</div>
<p class="post-excerpt">${post.excerpt}</p>
<div class="post-tags">
${post.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
<a href="#" s-link="/blog/${post.slug}" class="read-more">Read More →</a>
</div>
</article>
`).join('')}
</div>
</section>
</div>
`
};
});
// Blog Listing Component
component('blog-listing', () => {
return {
render: () => `
<div style="margin: 40px 0;">
<h1 style="text-align: center;">All Posts</h1>
<div class="post-grid">
${blogPosts.map(post => `
<article class="post-card">
<img src="${post.image}" alt="${post.title}" class="post-image">
<div class="post-content">
<h3 class="post-title">${post.title}</h3>
<div class="post-meta">
By ${post.author} • ${post.date}
</div>
<p class="post-excerpt">${post.excerpt}</p>
<div class="post-tags">
${post.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
<a href="#" s-link="/blog/${post.slug}" class="read-more">Read More →</a>
</div>
</article>
`).join('')}
</div>
</div>
`
};
});
// Blog Post Component
component('blog-post', (element, props) => {
// Get slug from URL
const slug = window.location.pathname.split('/').pop();
const post = blogPosts.find(p => p.slug === slug) || {
title: 'Post Not Found',
content: '<p>The requested post could not be found.</p>',
author: '',
date: '',
tags: []
};
return {
render: () => `
<article class="post-full">
<a href="#" s-link="/blog" style="color: #667eea; text-decoration: none; margin-bottom: 20px; display: inline-block;">
← Back to Blog
</a>
<h1>${post.title}</h1>
<div style="display: flex; gap: 20px; color: #666; margin: 20px 0;">
<span>By ${post.author}</span>
<span>📅 ${post.date}</span>
</div>
<img src="${post.image}" alt="${post.title}" style="width: 100%; border-radius: 12px; margin: 20px 0;">
<div class="post-tags">
${post.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
<div style="line-height: 1.8; margin: 30px 0;">
${post.content}
</div>
<hr style="margin: 40px 0;">
<div style="text-align: center;">
<h3>Share this post</h3>
<div style="display: flex; gap: 20px; justify-content: center; margin-top: 20px;">
<a href="#" style="color: #1da1f2; text-decoration: none;">Twitter</a>
<a href="#" style="color: #4267b2; text-decoration: none;">Facebook</a>
<a href="#" style="color: #0077b5; text-decoration: none;">LinkedIn</a>
<a href="#" style="color: #e60023; text-decoration: none;">Pinterest</a>
</div>
</div>
</article>
`
};
});
createApp().mount('[s-app]');
</script>
</body>
</html>
14.5 Generating Sitemaps and RSS Feeds
SimpliJS automatically generates sitemaps and RSS feeds during the build process.
Sitemap Generation
xml
<!-- Generated sitemap.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://my-blog.com/</loc>
<lastmod>2024-01-15</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://my-blog.com/blog</loc>
<lastmod>2024-01-15</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://my-blog.com/blog/getting-started-with-simplijs</loc>
<lastmod>2024-01-15</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://my-blog.com/blog/advanced-ssg-techniques</loc>
<lastmod>2024-01-14</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://my-blog.com/about</loc>
<lastmod>2024-01-10</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://my-blog.com/contact</loc>
<lastmod>2024-01-10</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
</urlset>
RSS Feed Generation
xml
<!-- Generated rss.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>My SimpliJS Blog</title>
<link>https://my-blog.com</link>
<description>Latest articles about SimpliJS and web development</description>
<language>en-us</language>
<lastBuildDate>Mon, 15 Jan 2024 00:00:00 GMT</lastBuildDate>
<atom:link href="https://my-blog.com/rss.xml" rel="self" type="application/rss+xml"/>
<item>
<title>Getting Started with SimpliJS</title>
<link>https://my-blog.com/blog/getting-started-with-simplijs</link>
<description>Learn how to build amazing apps with SimpliJS...</description>
<pubDate>Mon, 15 Jan 2024 00:00:00 GMT</pubDate>
<guid>https://my-blog.com/blog/getting-started-with-simplijs</guid>
</item>
<item>
<title>Advanced SSG Techniques</title>
<link>https://my-blog.com/blog/advanced-ssg-techniques</link>
<description>Take your static sites to the next level...</description>
<pubDate>Sun, 14 Jan 2024 00:00:00 GMT</pubDate>
<guid>https://my-blog.com/blog/advanced-ssg-techniques</guid>
</item>
<item>
<title>State Management Patterns</title>
<link>https://my-blog.com/blog/state-management-patterns</link>
<description>Discover different approaches to managing state...</description>
<pubDate>Sat, 13 Jan 2024 00:00:00 GMT</pubDate>
<guid>https://my-blog.com/blog/state-management-patterns</guid>
</item>
</channel>
</rss>
Robots.txt Generation
txt
# Generated robots.txt
User-agent: *
Allow: /
Sitemap: https://my-blog.com/sitemap.xml
# Disallow admin pages
Disallow: /admin/
Disallow: /private/
# Crawl delay for polite bots
Crawl-delay: 10
14.6 Performance Optimization with SSG
SSG provides numerous performance benefits out of the box.
Performance Features
<div s-app>
<performance-showcase></performance-showcase>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'performance-showcase\', () => {
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 30px;\">
<h2>SSG Performance Benefits</h2>
<div style=\"display: grid; gap: 20px; margin: 30px 0;\">
<!-- Preloading -->
<div style=\"padding: 20px; background: #f8f9fa; border-radius:
8px;\">
<h3>🚀 Asset Preloading</h3>
<pre style=\"background: #333; color: #6a9955; padding: 15px;
border-radius: 4px;\">
<link rel=\"preload\" href=\"/js/app.js\" as=\"script\">
<link rel=\"preload\" href=\"/css/style.css\" as=\"style\">
<link rel=\"preload\" href=\"/fonts/inter.woff2\" as=\"font\"
crossorigin>
<link rel=\"modulepreload\" href=\"/js/components.js\">
</pre>
</div>
<!-- Critical CSS -->
<div style=\"padding: 20px; background: #f8f9fa; border-radius:
8px;\">
<h3>🎨 Critical CSS Inlining</h3>
<pre style=\"background: #333; color: #6a9955; padding: 15px;
border-radius: 4px;\">
<style>
/* Critical CSS for above-the-fold content */
body { margin: 0; font-family: system-ui; }
header { background: #007bff; color: white; }
.hero { min-height: 50vh; }
</style>
<link rel=\"stylesheet\" href=\"/css/main.css\" media=\"print\"
onload=\"this.media=\'all\'\">
</pre>
</div>
<!-- Image Optimization -->
<div style=\"padding: 20px; background: #f8f9fa; border-radius:
8px;\">
<h3>🖼️ Responsive Images</h3>
<pre style=\"background: #333; color: #6a9955; padding: 15px;
border-radius: 4px;\">
<img src=\"/images/hero.jpg\"
srcset=\"/images/hero-400.jpg 400w,
/images/hero-800.jpg 800w,
/images/hero-1200.jpg 1200w\"
sizes=\"(max-width: 600px) 400px,
(max-width: 1200px) 800px,
1200px\"
loading=\"lazy\"
alt=\"Hero image\">
</pre>
</div>
<!-- Link Prefetching -->
<div style=\"padding: 20px; background: #f8f9fa; border-radius:
8px;\">
<h3>🔗 Smart Prefetching</h3>
<pre style=\"background: #333; color: #6a9955; padding: 15px;
border-radius: 4px;\">
<link rel=\"prefetch\" href=\"/blog/page2.html\">
<link rel=\"prerender\" href=\"/about.html\">
// Or use s-link with prefetch
<a href=\"#\" s-link=\"/blog\" s-prefetch>Blog</a>
</pre>
</div>
</div>
<!-- Lighthouse Scores -->
<div style=\"margin-top: 40px; padding: 20px; background: #d4edda;
border-radius: 8px;\">
<h3 style=\"color: #155724;\">📊 Typical Lighthouse Scores with
SSG</h3>
<div style=\"display: grid; grid-template-columns: repeat(4, 1fr); gap:
20px; margin-top: 20px;\">
<div style=\"text-align: center;\">
<div style=\"font-size: 48px; color: #28a745;\">98</div>
<div>Performance</div>
</div>
<div style=\"text-align: center;\">
<div style=\"font-size: 48px; color: #28a745;\">100</div>
<div>Accessibility</div>
</div>
<div style=\"text-align: center;\">
<div style=\"font-size: 48px; color: #28a745;\">100</div>
<div>Best Practices</div>
</div>
<div style=\"text-align: center;\">
<div style=\"font-size: 48px; color: #28a745;\">100</div>
<div>SEO</div>
</div>
</div>
</div>
</div>
\`
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>14.7 Deploying Your Static Site
Once generated, your static site can be deployed anywhere.
Deployment Options
<div s-app>
<deployment-guide></deployment-guide>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'deployment-guide\', () => {
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 30px;\">
<h2>Deployment Options</h2>
<div style=\"display: grid; gap: 20px; margin: 30px 0;\">
<!-- GitHub Pages -->
<div style=\"padding: 20px; background: #f8f9fa; border-radius:
8px;\">
<h3 style=\"display: flex; align-items: center; gap: 10px;\">
<span style=\"font-size: 24px;\">🐙</span> GitHub Pages
</h3>
<pre style=\"background: #333; color: #6a9955; padding: 15px;
border-radius: 4px;\">
\# Deploy to GitHub Pages
npm run build
git add dist -f
git commit -m \"Deploy\"
git subtree push --prefix dist origin gh-pages
</pre>
</div>
<!-- Netlify -->
<div style=\"padding: 20px; background: #f8f9fa; border-radius:
8px;\">
<h3 style=\"display: flex; align-items: center; gap: 10px;\">
<span style=\"font-size: 24px;\">🌐</span> Netlify
</h3>
<pre style=\"background: #333; color: #6a9955; padding: 15px;
border-radius: 4px;\">
\# netlify.toml
\[build\]
command = \"npm run build\"
publish = \"dist\"
\# Drag and drop dist folder to Netlify Drop
\# Or connect Git repository for automatic deploys
</pre>
</div>
<!-- Vercel -->
<div style=\"padding: 20px; background: #f8f9fa; border-radius:
8px;\">
<h3 style=\"display: flex; align-items: center; gap: 10px;\">
<span style=\"font-size: 24px;\">▲</span> Vercel
</h3>
<pre style=\"background: #333; color: #6a9955; padding: 15px;
border-radius: 4px;\">
\# vercel.json
{
\"buildCommand\": \"npm run build\",
\"outputDirectory\": \"dist\",
\"framework\": null
}
\# Deploy with Vercel CLI
vercel --prod
</pre>
</div>
<!-- AWS S3 -->
<div style=\"padding: 20px; background: #f8f9fa; border-radius:
8px;\">
<h3 style=\"display: flex; align-items: center; gap: 10px;\">
<span style=\"font-size: 24px;\">☁️</span> AWS S3
</h3>
<pre style=\"background: #333; color: #6a9955; padding: 15px;
border-radius: 4px;\">
\# Sync with AWS CLI
aws s3 sync dist/ s3://my-bucket/ --delete
\# Enable static website hosting
\# Configure CloudFront for CDN
</pre>
</div>
<!-- Cloudflare Pages -->
<div style=\"padding: 20px; background: #f8f9fa; border-radius:
8px;\">
<h3 style=\"display: flex; align-items: center; gap: 10px;\">
<span style=\"font-size: 24px;\">💨</span> Cloudflare Pages
</h3>
<pre style=\"background: #333; color: #6a9955; padding: 15px;
border-radius: 4px;\">
\# Connect Git repository
\# Build command: npm run build
\# Build directory: dist
\# Automatic HTTPS and CDN
</pre>
</div>
</div>
<!-- Deployment Checklist -->
<div style=\"margin-top: 40px; padding: 20px; background: #e8f4fd;
border-radius: 8px;\">
<h3>✅ Deployment Checklist</h3>
<ul style=\"list-style: none; padding: 0;\">
<li style=\"margin: 10px 0;\">☐ Run build command: <code>node ssg.js
ssg.config.js</code></li>
<li style=\"margin: 10px 0;\">☐ Test built site locally: <code>npx
serve dist</code></li>
<li style=\"margin: 10px 0;\">☐ Verify all links work</li>
<li style=\"margin: 10px 0;\">☐ Check sitemap.xml and
robots.txt</li>
<li style=\"margin: 10px 0;\">☐ Validate RSS feed</li>
<li style=\"margin: 10px 0;\">☐ Run Lighthouse audit</li>
<li style=\"margin: 10px 0;\">☐ Configure custom domain</li>
<li style=\"margin: 10px 0;\">☐ Set up HTTPS</li>
<li style=\"margin: 10px 0;\">☐ Enable CDN</li>
<li style=\"margin: 10px 0;\">☐ Submit sitemap to search
engines</li>
</ul>
</div>
</div>
\`
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>Chapter 14 Summary
You've now mastered Static Site Generation in SimpliJS:
Understanding SSG and its benefits over CSR and SSR
Configuration for routes, SEO, and assets
SEO helpers for metadata management
Building a blog with dynamic routes
Sitemap and RSS feed generation
Performance optimization techniques
Deployment options for static sites
SSG transforms your SimpliJS applications into lightning-fast, SEO-friendly static sites that can be deployed anywhere. Your content is pre-rendered at build time, providing the best possible user experience and search engine visibility.
In the next chapter, we'll explore the powerful plugin ecosystem that extends SimpliJS even further.
End of Chapter 14
Chapter 15: The Plugin Ecosystem
Welcome to Chapter 15, where we explore the powerful plugin ecosystem that extends SimpliJS's capabilities. Plugins add professional-grade features like authentication, advanced state management, routing, form handling, and developer tools—all while maintaining SimpliJS's signature simplicity.
15.1 Introduction to the Plugin System
SimpliJS plugins are modular extensions that integrate seamlessly with the core framework. They follow the same HTML-First philosophy and can be used both declaratively and programmatically.
Plugin Architecture
<div s-app>
<plugin-intro></plugin-intro>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'plugin-intro\', () => {
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 30px;\">
<h2>SimpliJS Plugin Ecosystem</h2>
<div style=\"display: grid; grid-template-columns: repeat(auto-fit,
minmax(250px, 1fr)); gap: 20px; margin: 30px 0;\">
<div style=\"padding: 20px; background: #e3f2fd; border-radius:
8px;\">
<h3 style=\"color: #1976d2;\">🔐 \@simplijs/auth</h3>
<p>Professional authentication and session management</p>
</div>
<div style=\"padding: 20px; background: #d4edda; border-radius:
8px;\">
<h3 style=\"color: #28a745;\">💾 \@simplijs/vault-pro</h3>
<p>Advanced state with time-travel and persistence</p>
</div>
<div style=\"padding: 20px; background: #fff3cd; border-radius:
8px;\">
<h3 style=\"color: #ffc107;\">🔄 \@simplijs/router</h3>
<p>Declarative SPA routing with transitions</p>
</div>
<div style=\"padding: 20px; background: #f8d7da; border-radius:
8px;\">
<h3 style=\"color: #dc3545;\">🌉 \@simplijs/bridge-adapters</h3>
<p>Import React, Vue, Svelte components</p>
</div>
<div style=\"padding: 20px; background: #e8f4fd; border-radius:
8px;\">
<h3 style=\"color: #17a2b8;\">🛠️ \@simplijs/devtools</h3>
<p>Real-time component and state inspection</p>
</div>
<div style=\"padding: 20px; background: #d1c4e9; border-radius:
8px;\">
<h3 style=\"color: #6f42c1;\">📝 \@simplijs/forms</h3>
<p>Professional form validation and wizards</p>
</div>
<div style=\"padding: 20px; background: #c8e6c9; border-radius:
8px;\">
<h3 style=\"color: #2e7d32;\">⚡ \@simplijs/ssg</h3>
<p>Static Site Generation for SEO excellence</p>
</div>
</div>
<div style=\"background: #f8f9fa; padding: 20px; border-radius:
8px;\">
<h3>Plugin Installation</h3>
<pre style=\"background: #333; color: #6a9955; padding: 15px;
border-radius: 4px;\">
\# NPM
npm install \@simplijs/auth \@simplijs/router \@simplijs/forms
\# CDN
import { createAuth } from
\'https://cdn.jsdelivr.net/npm/@simplijs/auth@latest\'
import { createRouter } from
\'https://cdn.jsdelivr.net/npm/@simplijs/router@latest\'
\# Local
import { createAuth } from \'./plugins/auth/index.js\'Chapter 15: The Plugin Ecosystem
Welcome to Chapter 15, where we explore the powerful plugin ecosystem that extends SimpliJS's capabilities. Plugins add professional-grade features like authentication, advanced state management, routing, form handling, and developer tools—all while maintaining SimpliJS's signature simplicity.
15.1 Introduction to the Plugin System
SimpliJS plugins are modular extensions that integrate seamlessly with the core framework. They follow the same HTML-First philosophy and can be used both declaratively and programmatically.
Plugin Architecture
<div s-app>
<plugin-intro></plugin-intro>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'plugin-intro\', () => {
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 30px;\">
<h2>SimpliJS Plugin Ecosystem</h2>
<div style=\"display: grid; grid-template-columns: repeat(auto-fit,
minmax(250px, 1fr)); gap: 20px; margin: 30px 0;\">
<div style=\"padding: 20px; background: #e3f2fd; border-radius:
8px;\">
<h3 style=\"color: #1976d2;\">🔐 \@simplijs/auth</h3>
<p>Professional authentication and session management</p>
</div>
<div style=\"padding: 20px; background: #d4edda; border-radius:
8px;\">
<h3 style=\"color: #28a745;\">💾 \@simplijs/vault-pro</h3>
<p>Advanced state with time-travel and persistence</p>
</div>
<div style=\"padding: 20px; background: #fff3cd; border-radius:
8px;\">
<h3 style=\"color: #ffc107;\">🔄 \@simplijs/router</h3>
<p>Declarative SPA routing with transitions</p>
</div>
<div style=\"padding: 20px; background: #f8d7da; border-radius:
8px;\">
<h3 style=\"color: #dc3545;\">🌉 \@simplijs/bridge-adapters</h3>
<p>Import React, Vue, Svelte components</p>
</div>
<div style=\"padding: 20px; background: #e8f4fd; border-radius:
8px;\">
<h3 style=\"color: #17a2b8;\">🛠️ \@simplijs/devtools</h3>
<p>Real-time component and state inspection</p>
</div>
<div style=\"padding: 20px; background: #d1c4e9; border-radius:
8px;\">
<h3 style=\"color: #6f42c1;\">📝 \@simplijs/forms</h3>
<p>Professional form validation and wizards</p>
</div>
<div style=\"padding: 20px; background: #c8e6c9; border-radius:
8px;\">
<h3 style=\"color: #2e7d32;\">⚡ \@simplijs/ssg</h3>
<p>Static Site Generation for SEO excellence</p>
</div>
</div>
<div style=\"background: #f8f9fa; padding: 20px; border-radius:
8px;\">
<h3>Plugin Installation</h3>
<pre style=\"background: #333; color: #6a9955; padding: 15px;
border-radius: 4px;\">
\# NPM
npm install \@simplijs/auth \@simplijs/router \@simplijs/forms
\# CDN
import { createAuth } from
\'https://cdn.jsdelivr.net/npm/@simplijs/auth@latest\'
import { createRouter } from
\'https://cdn.jsdelivr.net/npm/@simplijs/router@latest\'
\# Local
import { createAuth } from \'./plugins/auth/index.js\'
import { createRouter } from \'./plugins/router/index.js\'
</pre>
</div>
<div style=\"margin-top: 30px; padding: 20px; background: #e8f4fd;
border-radius: 8px;\">
<h3>Plugin Philosophy</h3>
<p>Each plugin is designed to be:</p>
<ul>
<li><strong>Modular:</strong> Use only what you need</li>
<li><strong>Composable:</strong> Plugins work together
seamlessly</li>
<li><strong>HTML-First:</strong> Use with s-* directives when
possible</li>
<li><strong>Lightweight:</strong> Minimal overhead</li>
<li><strong>Professional:</strong> Production-ready
features</li>
</ul>
</div>
</div>
\`
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>15.2 @simplijs/auth - Authentication Plugin
The auth plugin provides professional authentication and session management with a reactive API.
Basic Authentication Setup
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width,
initial-scale=1.0\">
<title>Auth Plugin Demo</title>
<style>
* { box-sizing: border-box; }
body { font-family: Arial, sans-serif; background: #f0f2f5; margin: 0;
padding: 20px; }
.container { max-width: 800px; margin: 0 auto; }
.card { background: white; border-radius: 12px; padding: 30px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1); margin: 20px 0; }
.form-group { margin: 15px 0; }
.form-group label { display: block; margin-bottom: 5px; font-weight:
600; }
.form-group input { width: 100%; padding: 10px; border: 2px solid #ddd;
border-radius: 4px; }
.btn { padding: 10px 20px; border: none; border-radius: 4px; cursor:
pointer; font-size: 16px; }
.btn-primary { background: #007bff; color: white; }
.btn-success { background: #28a745; color: white; }
.btn-danger { background: #dc3545; color: white; }
.alert { padding: 15px; border-radius: 4px; margin: 15px 0; }
.alert-success { background: #d4edda; color: #155724; border: 1px solid#c3e6cb; }
.alert-error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.user-info { display: flex; align-items: center; gap: 20px; padding: 15px; background: #e3f2fd; border-radius: 8px; }
</style>
</head>
<body>
<div s-app>
<auth-demo></auth-demo>
<script type="module">
import { createApp, component, reactive } from 'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js';
// Simulated auth plugin (in real app, import from @simplijs/auth)
function createAuth(options = {}) {
const state = reactive({
user: null,
isAuthenticated: false,
loading: false,
error: null
});
// Simulated API
const users = [
{ id: 1, email: 'demo@example.com', password: 'password', name: 'Demo User', role: 'user' },
{ id: 2, email: 'admin@example.com', password: 'admin', name: 'Admin User', role: 'admin' }
];
const login = async (email, password) => {
state.loading = true;
state.error = null;
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
const user = users.find(u => u.email === email && u.password === password);
if (user) {
const { password, ...userWithoutPassword } = user;
state.user = userWithoutPassword;
state.isAuthenticated = true;
// Store in localStorage if persist enabled
if (options.persist) {
localStorage.setItem('auth_user', JSON.stringify(userWithoutPassword));
}
if (options.onLogin) options.onLogin(userWithoutPassword);
} else {
state.error = 'Invalid email or password';
}
state.loading = false;
};
const logout = () => {
state.user = null;
state.isAuthenticated = false;
if (options.persist) {
localStorage.removeItem('auth_user');
}
if (options.onLogout) options.onLogout();
};
const checkSession = () => {
if (options.persist) {
const saved = localStorage.getItem('auth_user');
if (saved) {
state.user = JSON.parse(saved);
state.isAuthenticated = true;
}
}
};
// Check for existing session
checkSession();
return {
state,
login,
logout,
checkSession
};
}
// UI Helpers (simulated)
const AuthUI = {
loginForm: (submitHandler) => `
<form onsubmit="event.preventDefault(); ${submitHandler}(document.getElementById('email').value, document.getElementById('password').value)">
<div class="form-group">
<label>Email:</label>
<input type="email" id="email" value="demo@example.com" required>
</div>
<div class="form-group">
<label>Password:</label>
<input type="password" id="password" value="password" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
`,
registerForm: (submitHandler) => `
<form onsubmit="event.preventDefault(); ${submitHandler}(document.getElementById('reg-name').value, document.getElementById('reg-email').value, document.getElementById('reg-password').value)">
<div class="form-group">
<label>Name:</label>
<input type="text" id="reg-name" required>
</div>
<div class="form-group">
<label>Email:</label>
<input type="email" id="reg-email" required>
</div>
<div class="form-group">
<label>Password:</label>
<input type="password" id="reg-password" required>
</div>
<button type="submit" class="btn btn-success">Register</button>
</form>
`
};
component('auth-demo', () => {
const auth = createAuth({ persist: true });
const handleLogin = (email, password) => {
auth.login(email, password);
};
const handleLogout = () => {
auth.logout();
};
return {
render: () => `
<div class="container">
<h1>🔐 Auth Plugin Demo</h1>
${auth.state.loading ? `
<div class="alert alert-success">Loading...</div>
` : ''}
${auth.state.error ? `
<div class="alert alert-error">${auth.state.error}</div>
` : ''}
${!auth.state.isAuthenticated ? `
<div class="card">
<h2>Login</h2>
${AuthUI.loginForm('this.closest(\'auth-demo\').handleLogin')}
<div style="margin-top: 20px; text-align: center;">
<p>Demo credentials: demo@example.com / password</p>
<p>Admin: admin@example.com / admin</p>
</div>
</div>
<div class="card">
<h2>Register</h2>
${AuthUI.registerForm('this.closest(\'auth-demo\').handleRegister')}
</div>
` : `
<div class="card">
<h2>Welcome!</h2>
<div class="user-info">
<div style="width: 50px; height: 50px; background: #007bff; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px;">
${auth.state.user?.name[0]}
</div>
<div>
<h3 style="margin: 0;">${auth.state.user?.name}</h3>
<p style="margin: 5px 0 0; color: #666;">${auth.state.user?.email}</p>
<span style="display: inline-block; padding: 3px 10px; background: #e3f2fd; color: #1976d2; border-radius: 20px; margin-top: 5px;">
Role: ${auth.state.user?.role}
</span>
</div>
</div>
<button onclick="this.closest('auth-demo').handleLogout()" class="btn btn-danger" style="margin-top: 20px;">
Logout
</button>
</div>
`}
<div class="card">
<h3>Auth State</h3>
<pre style="background: #f8f9fa; padding: 15px; border-radius: 4px;">
${JSON.stringify({
isAuthenticated: auth.state.isAuthenticated,
user: auth.state.user,
loading: auth.state.loading,
error: auth.state.error
}, null, 2)}
</pre>
</div>
</div>
`,
handleLogin,
handleLogout,
handleRegister: (name, email, password) => {
alert(`Registration would happen here: ${name}, ${email}`);
}
};
});
createApp().mount('[s-app]');
</script>
</div>
</body>
</html>
Protected Routes with Auth Guard
<div s-app>
<protected-routes></protected-routes>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// Auth plugin (simplified)*
const auth = reactive({
user: null,
isAuthenticated: false,
login(email, password) {
if (email === \'user@example.com\' && password === \'password\') {
this.user = { id: 1, name: \'John Doe\', email, role: \'user\' };
this.isAuthenticated = true;
return true;
}
return false;
},
logout() {
this.user = null;
this.isAuthenticated = false;
}
});
*// Route guard function*
function requireAuth(route) {
if (!auth.isAuthenticated) {
window.location.hash = \'/login\';
return false;
}
return true;
}
component(\'protected-routes\', () => {
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 20px;\">
<h2>Protected Routes Demo</h2>
<!-- Navigation -->
<nav style=\"display: flex; gap: 20px; margin: 20px 0; padding: 15px;
background: #f8f9fa; border-radius: 8px;\">
<a href=\"#\" s-link=\"/\">Home</a>
<a href=\"#\" s-link=\"/public\">Public</a>
<a href=\"#\" s-link=\"/dashboard\">Dashboard (Protected)</a>
<a href=\"#\" s-link=\"/profile\">Profile (Protected)</a>
\${!auth.isAuthenticated ? \`
<a href=\"#\" s-link=\"/login\" style=\"margin-left:
auto;\">Login</a>
\` : \`
<span style=\"margin-left: auto;\">
Welcome, \${auth.user.name}!
<button onclick=\"auth.logout(); window.location.hash=\'/\'\"
style=\"margin-left: 10px; padding: 5px 10px; background: #dc3545;
color: white; border: none; border-radius: 4px;\">
Logout
</button>
</span>
\`}
</nav>
<!-- Router Outlet -->
<div s-view></div>
<!-- Public Routes -->
<div s-route=\"/\">
<div style=\"background: white; padding: 30px; border-radius: 8px;\">
<h1>Home Page</h1>
<p>This is a public page. Anyone can see this.</p>
</div>
</div>
<div s-route=\"/public\">
<div style=\"background: white; padding: 30px; border-radius: 8px;\">
<h1>Public Page</h1>
<p>This content is accessible to everyone.</p>
</div>
</div>
<div s-route=\"/login\">
<div style=\"background: white; padding: 30px; border-radius: 8px;
max-width: 400px; margin: 0 auto;\">
<h2>Login</h2>
\${auth.isAuthenticated ? \`
<div class=\"alert alert-success\">You are already logged in!</div>
<a href=\"#\" s-link=\"/dashboard\">Go to Dashboard</a>
\` : \`
<form onsubmit=\"event.preventDefault();
const email = document.getElementById(\'email\').value;
const password = document.getElementById(\'password\').value;
if(auth.login(email, password)) {
window.location.hash = \'/dashboard\';
} else {
alert(\'Invalid credentials\');
}\">
<div style=\"margin: 15px 0;\">
<label>Email:</label>
<input type=\"email\" id=\"email\" value=\"user@example.com\"
style=\"width: 100%; padding: 8px;\">
</div>
<div style=\"margin: 15px 0;\">
<label>Password:</label>
<input type=\"password\" id=\"password\" value=\"password\"
style=\"width: 100%; padding: 8px;\">
</div>
<button type=\"submit\" style=\"padding: 10px; width: 100%; background:#007bff; color: white; border: none; border-radius: 4px;">
Login
</button>
</form>
<p style="margin-top: 20px; font-size: 14px; color: #666;">
Demo credentials: user@example.com / password
</p>
`}
</div>
</div>
<!-- Protected Routes with Guard -->
<div s-route="/dashboard" s-guard="requireAuth">
${auth.isAuthenticated ? `
<div style="background: white; padding: 30px; border-radius: 8px;">
<h1>Dashboard</h1>
<p>Welcome to your protected dashboard, ${auth.user.name}!</p>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-top: 30px;">
<div style="padding: 20px; background: #e3f2fd; border-radius: 8px;">
<h3>Stats</h3>
<p>Total users: 1,234</p>
</div>
<div style="padding: 20px; background: #d4edda; border-radius: 8px;">
<h3>Activity</h3>
<p>Last login: Today</p>
</div>
</div>
</div>
` : ''}
</div>
<div s-route="/profile" s-guard="requireAuth">
${auth.isAuthenticated ? `
<div style="background: white; padding: 30px; border-radius: 8px;">
<h1>Profile</h1>
<div style="display: flex; gap: 30px; align-items: center;">
<div style="width: 100px; height: 100px; background: #007bff; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 36px;">
${auth.user.name[0]}
</div>
<div>
<h2>${auth.user.name}</h2>
<p>${auth.user.email}</p>
<p>Role: ${auth.user.role}</p>
</div>
</div>
</div>
` : ''}
</div>
<!-- 404 Route -->
<div s-route="/:404">
<div style="background: white; padding: 30px; border-radius: 8px; text-align: center;">
<h1 style="font-size: 72px; color: #dc3545;">404</h1>
<h2>Page Not Found</h2>
<a href="#" s-link="/">Go Home</a>
</div>
</div>
</div>
`
};
});
createApp().mount('[s-app]');
</script>
</div>
15.3 @simplijs/vault-pro - Advanced State Management
The Vault Pro plugin adds time-travel debugging, state persistence, and checkpointing to your applications.
Time Travel with Vault
<div s-app>
<vault-demo></vault-demo>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// Vault Pro plugin (simplified)*
function createVault(initialState = {}, options = {}) {
const history = \[JSON.parse(JSON.stringify(initialState))\];
let currentIndex = 0;
const state = reactive(JSON.parse(JSON.stringify(initialState)));
*// Proxy to track changes*
const handler = {
set(target, property, value) {
target\[property\] = value;
*// Save to history*
if (currentIndex < history.length - 1) {
*// We\'re in the middle of history, truncate forward history*
history.splice(currentIndex + 1);
}
history.push(JSON.parse(JSON.stringify(target)));
currentIndex = history.length - 1;
if (options.persist) {
localStorage.setItem(\'vault_state\', JSON.stringify(target));
}
return true;
}
};
const proxy = new Proxy(state, handler);
*// Load persisted state*
if (options.persist) {
const saved = localStorage.getItem(\'vault_state\');
if (saved) {
const parsed = JSON.parse(saved);
Object.assign(state, parsed);
history.push(parsed);
currentIndex = history.length - 1;
}
}
return {
state: proxy,
vault: {
*// Time travel methods*
undo() {
if (currentIndex > 0) {
currentIndex--;
Object.assign(state, history\[currentIndex\]);
}
},
redo() {
if (currentIndex < history.length - 1) {
currentIndex++;
Object.assign(state, history\[currentIndex\]);
}
},
*// Checkpointing*
checkpoint(name) {
const checkpoint = {
name,
state: JSON.parse(JSON.stringify(state)),
timestamp: new Date().toISOString()
};
*// In real plugin, would store checkpoints*
console.log(\'Checkpoint created:\', checkpoint);
return checkpoint;
},
restore(name) {
console.log(\'Restoring checkpoint:\', name);
*// Would restore from checkpoint*
},
*// Share state*
share() {
const stateStr = JSON.stringify(state);
const encoded = btoa(stateStr);
return \`\${window.location.origin}?state=\${encoded}\`;
},
*// History info*
get history() {
return {
past: history.slice(0, currentIndex),
present: history\[currentIndex\],
future: history.slice(currentIndex + 1)
};
},
get canUndo() {
return currentIndex > 0;
},
get canRedo() {
return currentIndex < history.length - 1;
}
}
};
}
component(\'vault-demo\', () => {
const vault = createVault({
counter: 0,
todos: \[\],
user: {
name: \'Alice\',
preferences: {
theme: \'light\'
}
}
}, { persist: true });
const addTodo = () => {
const text = document.getElementById(\'todoInput\').value;
if (text) {
vault.state.todos.push({
id: Date.now(),
text,
completed: false
});
document.getElementById(\'todoInput\').value = \'\';
}
};
const toggleTodo = (id) => {
const todo = vault.state.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
};
const deleteTodo = (id) => {
vault.state.todos = vault.state.todos.filter(t => t.id !== id);
};
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 30px;
background: white; border-radius: 12px; box-shadow: 0 4px 6px
rgba(0,0,0,0.1);\">
<h2>🕰️ Vault Pro - Time Travel Demo</h2>
<!-- Time Travel Controls -->
<div style=\"display: flex; gap: 10px; margin: 20px 0; padding: 20px;
background: #f8f9fa; border-radius: 8px;\">
<button onclick=\"this.closest(\'vault-demo\').undo()\"
style=\"padding: 10px 20px; background: \${vault.vault.canUndo ?
\'#007bff\' : \'#ccc\'}; color: white; border: none; border-radius:
4px;\"
\${!vault.vault.canUndo ? \'disabled\' : \'\'}>
⏪ Undo
</button>
<button onclick=\"this.closest(\'vault-demo\').redo()\"
style=\"padding: 10px 20px; background: \${vault.vault.canRedo ?
\'#007bff\' : \'#ccc\'}; color: white; border: none; border-radius:
4px;\"
\${!vault.vault.canRedo ? \'disabled\' : \'\'}>
⏩ Redo
</button>
<button onclick=\"this.closest(\'vault-demo\').checkpoint()\"
style=\"padding: 10px 20px; background: #28a745; color: white; border:
none; border-radius: 4px;\">
📍 Create Checkpoint
</button>
<button onclick=\"this.closest(\'vault-demo\').share()\"
style=\"padding: 10px 20px; background: #ffc107; color: #333; border:
none; border-radius: 4px;\">
🔗 Share State
</button>
</div>
<!-- Counter Demo -->
<div style=\"margin: 30px 0; padding: 20px; background: #e3f2fd;
border-radius: 8px;\">
<h3>Counter: \${vault.state.counter}</h3>
<div style=\"display: flex; gap: 10px;\">
<button onclick=\"vault.state.counter++\"
style=\"padding: 10px 20px; background: #28a745; color: white; border:
none; border-radius: 4px;\">
+1
</button>
<button onclick=\"vault.state.counter += 5\"
style=\"padding: 10px 20px; background: #17a2b8; color: white; border:
none; border-radius: 4px;\">
+5
</button>
<button onclick=\"vault.state.counter = 0\"
style=\"padding: 10px 20px; background: #dc3545; color: white; border:
none; border-radius: 4px;\">
Reset
</button>
</div>
</div>
<!-- Todo Demo -->
<div style=\"margin: 30px 0;\">
<h3>Todos</h3>
<div style=\"display: flex; gap: 10px; margin: 20px 0;\">
<input type=\"text\" id=\"todoInput\" placeholder=\"Add a todo\...\"
style=\"flex:1; padding: 10px; border: 2px solid #ddd; border-radius:
4px;\">
<button onclick=\"this.closest(\'vault-demo\').addTodo()\"
style=\"padding: 10px 20px; background: #007bff; color: white; border:
none; border-radius: 4px;\">
Add
</button>
</div>
<div style=\"display: grid; gap: 10px;\">
\${vault.state.todos.map(todo => \`
<div style=\"display: flex; align-items: center; gap: 10px; padding:
10px; background: #f8f9fa; border-radius: 4px;\">
<input type=\"checkbox\"
\${todo.completed ? \'checked\' : \'\'}
onchange=\"this.closest(\'vault-demo\').toggleTodo(\${todo.id})\">
<span style=\"flex:1; \${todo.completed ? \'text-decoration:
line-through; color: #999;\' : \'\'}\">
\${todo.text}
</span>
<button
onclick=\"this.closest(\'vault-demo\').deleteTodo(\${todo.id})\"
style=\"padding: 5px 10px; background: #dc3545; color: white; border:
none; border-radius: 4px;\">
×
</button>
</div>
\`).join(\'\')}
\${vault.state.todos.length === 0 ? \`
<p style=\"text-align: center; color: #999; padding: 40px;\">No todos yet. Add one above!
</p>
` : ''}
</div>
</div>
<!-- User Preferences -->
<div style="margin: 30px 0; padding: 20px; background: #f8f9fa; border-radius: 8px;">
<h3>User Preferences</h3>
<div style="display: flex; align-items: center; gap: 20px;">
<label>
Theme:
<select onchange="vault.state.user.preferences.theme = this.value">
<option value="light" ${vault.state.user.preferences.theme === 'light' ? 'selected' : ''}>Light</option>
<option value="dark" ${vault.state.user.preferences.theme === 'dark' ? 'selected' : ''}>Dark</option>
</select>
</label>
<span>Current: ${vault.state.user.preferences.theme}</span>
</div>
</div>
<!-- History Viewer -->
<div style="margin-top: 40px; padding: 20px; background: #333; color: #fff; border-radius: 8px;">
<h3 style="color: #fff;">State History</h3>
<pre style="color: #6a9955; overflow-x: auto;">
${JSON.stringify(vault.vault.history, null, 2)}
</pre>
</div>
</div>
`,
undo: () => vault.vault.undo(),
redo: () => vault.vault.redo(),
checkpoint: () => {
const name = prompt('Enter checkpoint name:');
if (name) vault.vault.checkpoint(name);
},
share: () => {
const link = vault.vault.share();
prompt('Share this link:', link);
},
addTodo,
toggleTodo,
deleteTodo
};
});
createApp().mount('[s-app]');
</script>
</div>
15.4 @simplijs/router - Professional SPA Routing
The router plugin provides advanced routing capabilities with nested routes, route guards, and transitions.
Advanced Router Features
<div s-app>
<advanced-router></advanced-router>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// Router plugin (simplified)*
function createRouter(routes, options = {}) {
const state = reactive({
currentRoute: window.location.hash.slice(1) \|\| \'/\',
params: {},
query: {}
});
const matchRoute = (path) => {
*// Simple route matching (in real router, would be more sophisticated)*
const \[pathWithoutQuery\] = path.split(\'?\');
for (const route of Object.keys(routes)) {
if (route.includes(\':\')) {
*// Parameterized route*
const routeParts = route.split(\'/\');
const pathParts = pathWithoutQuery.split(\'/\');
if (routeParts.length === pathParts.length) {
const params = {};
let match = true;
for (let i = 0; i < routeParts.length; i++) {
if (routeParts\[i\].startsWith(\':\')) {
params\[routeParts\[i\].slice(1)\] = pathParts\[i\];
} else if (routeParts\[i\] !== pathParts\[i\]) {
match = false;
break;
}
}
if (match) {
state.params = params;
return route;
}
}
} else if (route === pathWithoutQuery) {
return route;
}
}
return Object.keys(routes).find(r => r === \'/:404\') \|\| \'/\';
};
const navigate = (path) => {
window.location.hash = path;
state.currentRoute = path.split(\'?\')\[0\];
*// Parse query params*
const queryString = path.split(\'?\')\[1\];
if (queryString) {
const params = new URLSearchParams(queryString);
state.query = Object.fromEntries(params);
} else {
state.query = {};
}
*// Check guards*
const route = matchRoute(state.currentRoute);
const guard = routes\[route\]?.guard;
if (guard && !guard(state)) {
window.location.hash = \'/\';
state.currentRoute = \'/\';
}
};
*// Listen for hash changes*
window.addEventListener(\'hashchange\', () => {
navigate(window.location.hash.slice(1));
});
*// Initial navigation*
navigate(state.currentRoute);
return {
state,
navigate,
matchRoute,
link: (path) => {
navigate(path);
}
};
}
component(\'advanced-router\', () => {
*// Define routes with components and guards*
const routes = {
\'/\': {
component: \'home-page\',
title: \'Home\'
},
\'/products\': {
component: \'products-page\',
title: \'Products\'
},
\'/products/:id\': {
component: \'product-detail\',
title: \'Product Details\'
},
\'/cart\': {
component: \'cart-page\',
title: \'Shopping Cart\',
guard: () => {
*// Check if cart has items*
return cart.items.length > 0;
}
},
\'/checkout\': {
component: \'checkout-page\',
title: \'Checkout\',
guard: () => {
return auth.isAuthenticated;
}
},
\'/profile\': {
component: \'profile-page\',
title: \'Profile\',
guard: () => {
return auth.isAuthenticated;
}
},
\'/:404\': {
component: \'not-found\',
title: \'404 - Not Found\'
}
};
*// Auth state*
const auth = reactive({
isAuthenticated: false,
user: null,
login() {
this.isAuthenticated = true;
this.user = { name: \'John Doe\', email: \'john@example.com\' };
router.navigate(\'/profile\');
},
logout() {
this.isAuthenticated = false;
this.user = null;
router.navigate(\'/\');
}
});
*// Cart state*
const cart = reactive({
items: \[\],
addItem(product) {
const existing = this.items.find(i => i.id === product.id);
if (existing) {
existing.quantity++;
} else {
this.items.push({ \...product, quantity: 1 });
}
},
removeItem(id) {
this.items = this.items.filter(i => i.id !== id);
},
clear() {
this.items = \[\];
}
});
*// Sample products*
const products = \[
{ id: 1, name: \'Laptop\', price: 999, image: \'💻\' },
{ id: 2, name: \'Mouse\', price: 49, image: \'🖱️\' },
{ id: 3, name: \'Keyboard\', price: 129, image: \'⌨️\' },
{ id: 4, name: \'Monitor\', price: 299, image: \'🖥️\' }
\];
const router = createRouter(routes);
return {
render: () => \`
<div style=\"max-width: 1000px; margin: 20px auto; padding: 20px;\">
<h2>🔄 Advanced Router Demo</h2>
<!-- Navigation -->
<nav style=\"display: flex; gap: 20px; margin: 20px 0; padding: 15px;
background: #f8f9fa; border-radius: 8px; align-items: center;\">
<a href=\"#\" s-link=\"/\">Home</a>
<a href=\"#\" s-link=\"/products\">Products</a>
\${cart.items.length > 0 ? \`
<a href=\"#\" s-link=\"/cart\">
Cart (\${cart.items.reduce((sum, i) => sum + i.quantity, 0)})
</a>
\` : \'\'}
\${auth.isAuthenticated ? \`
<a href=\"#\" s-link=\"/profile\">Profile</a>
<a href=\"#\" s-link=\"/checkout\">Checkout</a>
<span style=\"margin-left: auto;\">
Welcome, \${auth.user.name}!
<button onclick=\"auth.logout()\" style=\"margin-left: 10px; padding:
5px 10px; background: #dc3545; color: white; border: none;
border-radius: 4px;\">
Logout
</button>
</span>
\` : \`
<button onclick=\"auth.login()\" style=\"margin-left: auto; padding:
5px 10px; background: #28a745; color: white; border: none;
border-radius: 4px;\">
Login
</button>
\`}
</nav>
<!-- Current Route Info -->
<div style=\"margin: 20px 0; padding: 10px; background: #e3f2fd;
border-radius: 4px; font-size: 14px;\">
Current Route: \${router.state.currentRoute}
\${Object.keys(router.state.params).length ? \`
\| Params: \${JSON.stringify(router.state.params)}
\` : \'\'}
\${Object.keys(router.state.query).length ? \`
\| Query: \${JSON.stringify(router.state.query)}
\` : \'\'}
</div>
<!-- Router Outlet -->
<div s-view></div>
<!-- Routes -->
<!-- Home Page -->
<div s-route=\"/\">
<div style=\"background: white; padding: 30px; border-radius: 8px;\">
<h1>Welcome to the Shop</h1>
<p>Browse our products and add them to your cart.</p>
<a href=\"#\" s-link=\"/products\" style=\"display: inline-block;
padding: 10px 20px; background: #007bff; color: white; text-decoration:
none; border-radius: 4px;\">
View Products
</a>
</div>
</div>
<!-- Products Page -->
<div s-route=\"/products\">
<div style=\"background: white; padding: 30px; border-radius: 8px;\">
<h1>Products</h1>
<div style=\"display: grid; grid-template-columns: repeat(2, 1fr); gap:
20px; margin: 30px 0;\">
\${products.map(product => \`
<div style=\"padding: 20px; border: 1px solid #ddd; border-radius: 8px;
display: flex; justify-content: space-between; align-items: center;\">
<div>
<span style=\"font-size: 32px;\">\${product.image}</span>
<h3 style=\"margin: 10px 0 5px;\">\${product.name}</h3>
<p style=\"color: #28a745; font-weight:
bold;\">\$\${product.price}</p>
</div>
<div>
<a href=\"#\" s-link=\"/products/\${product.id}\" style=\"display:
block; margin-bottom: 10px; color: #007bff;\">View</a>
<button
onclick=\"cart.addItem(\${JSON.stringify(product).replace(/\"/g,
\'"\')})\"
style=\"padding: 5px 10px; background: #28a745; color: white; border:
none; border-radius: 4px;\">Add to Cart
</button>
</div>
</div>
`).join('')}
</div>
</div>
</div>
<!-- Product Detail Page -->
<div s-route="/products/:id">
<product-detail></product-detail>
</div>
<!-- Cart Page -->
<div s-route="/cart">
<div style="background: white; padding: 30px; border-radius: 8px;">
<h1>Shopping Cart</h1>
${cart.items.length === 0 ? `
<p style="text-align: center; padding: 40px; color: #999;">
Your cart is empty
</p>
` : `
<div style="margin: 30px 0;">
${cart.items.map(item => `
<div style="display: flex; align-items: center; gap: 20px; padding: 15px; border-bottom: 1px solid #eee;">
<span style="font-size: 24px;">${item.image}</span>
<div style="flex:2;">
<h4 style="margin: 0;">${item.name}</h4>
<p style="color: #666;">$${item.price} each</p>
</div>
<div style="flex:1;">Quantity: ${item.quantity}</div>
<div style="flex:1; font-weight: bold;">
$${(item.price * item.quantity).toFixed(2)}
</div>
<button onclick="cart.removeItem(${item.id})"
style="padding: 5px 10px; background: #dc3545; color: white; border: none; border-radius: 4px;">
Remove
</button>
</div>
`).join('')}
<div style="margin-top: 30px; text-align: right;">
<h3>Total: $${cart.items.reduce((sum, i) => sum + (i.price * i.quantity), 0).toFixed(2)}</h3>
<a href="#" s-link="/checkout" style="display: inline-block; padding: 10px 30px; background: #28a745; color: white; text-decoration: none; border-radius: 4px;">
Proceed to Checkout
</a>
</div>
</div>
`}
</div>
</div>
<!-- Checkout Page (Protected) -->
<div s-route="/checkout">
<div style="background: white; padding: 30px; border-radius: 8px;">
<h1>Checkout</h1>
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 30px;">
<div>
<h3>Shipping Information</h3>
<form onsubmit="event.preventDefault(); alert('Order placed!'); cart.clear(); router.navigate('/');">
<div style="margin: 15px 0;">
<label>Name:</label>
<input type="text" value="${auth.user?.name}" style="width: 100%; padding: 8px;">
</div>
<div style="margin: 15px 0;">
<label>Email:</label>
<input type="email" value="${auth.user?.email}" style="width: 100%; padding: 8px;">
</div>
<div style="margin: 15px 0;">
<label>Address:</label>
<input type="text" placeholder="Street address" style="width: 100%; padding: 8px;">
</div>
<button type="submit" style="padding: 10px 20px; background: #28a745; color: white; border: none; border-radius: 4px;">
Place Order
</button>
</form>
</div>
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px;">
<h3>Order Summary</h3>
${cart.items.map(item => `
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
<span>${item.name} x${item.quantity}</span>
<span>$${(item.price * item.quantity).toFixed(2)}</span>
</div>
`).join('')}
<hr>
<div style="display: flex; justify-content: space-between; font-weight: bold;">
<span>Total:</span>
<span>$${cart.items.reduce((sum, i) => sum + (i.price * i.quantity), 0).toFixed(2)}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Profile Page (Protected) -->
<div s-route="/profile">
<div style="background: white; padding: 30px; border-radius: 8px;">
<h1>Profile</h1>
<div style="display: flex; gap: 30px; align-items: center;">
<div style="width: 100px; height: 100px; background: #007bff; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 36px;">
${auth.user?.name[0]}
</div>
<div>
<h2>${auth.user?.name}</h2>
<p>${auth.user?.email}</p>
</div>
</div>
</div>
</div>
<!-- 404 Page -->
<div s-route="/:404">
<div style="background: white; padding: 30px; border-radius: 8px; text-align: center;">
<h1 style="font-size: 72px; color: #dc3545;">404</h1>
<h2>Page Not Found</h2>
<p>The page you're looking for doesn't exist.</p>
<a href="#" s-link="/" style="display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 4px;">
Go Home
</a>
</div>
</div>
</div>
`,
// Expose for event handlers
auth,
cart
};
});
// Product Detail Component
component('product-detail', (element, props) => {
// In real app, would fetch based on route params
const products = [
{ id: 1, name: 'Laptop', price: 999, image: '💻', description: 'High-performance laptop for work and play.', specs: ['16GB RAM', '512GB SSD', 'Intel i7'] },
{ id: 2, name: 'Mouse', price: 49, image: '🖱️', description: 'Wireless ergonomic mouse.', specs: ['Wireless', 'Ergonomic', 'Adjustable DPI'] },
{ id: 3, name: 'Keyboard', price: 129, image: '⌨️', description: 'Mechanical keyboard with RGB.', specs: ['Mechanical', 'RGB', 'Tenkeyless'] },
{ id: 4, name: 'Monitor', price: 299, image: '🖥️', description: '4K UHD monitor for professionals.', specs: ['4K', '27 inch', 'IPS Panel'] }
];
// Get id from URL
const id = parseInt(window.location.pathname.split('/').pop());
const product = products.find(p => p.id === id);
return {
render: () => `
<div style="background: white; padding: 30px; border-radius: 8px;">
<a href="#" s-link="/products" style="color: #007bff; text-decoration: none; margin-bottom: 20px; display: inline-block;">
← Back to Products
</a>
${product ? `
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 40px;">
<div style="font-size: 200px; text-align: center; background: #f8f9fa; padding: 40px; border-radius: 12px;">
${product.image}
</div>
<div>
<h1>${product.name}</h1>
<p style="font-size: 24px; color: #28a745;">$${product.price}</p>
<p style="line-height: 1.6; color: #666;">${product.description}</p>
<h3>Specifications:</h3>
<ul>
${product.specs.map(spec => `<li>${spec}</li>`).join('')}
</ul>
<button onclick="cart.addItem(${JSON.stringify(product).replace(/"/g, '"')})"
style="padding: 15px 30px; background: #28a745; color: white; border: none; border-radius: 4px; font-size: 16px;">
Add to Cart
</button>
</div>
</div>
` : `
<div style="text-align: center; padding: 60px;">
<h2>Product Not Found</h2>
<p>The product you're looking for doesn't exist.</p>
</div>
`}
</div>
`
};
});
createApp().mount('[s-app]');
</script>
</div>
15.5 @simplijs/forms - Professional Form Handling
The forms plugin provides advanced form validation, wizard handling, and auto-save capabilities.
Advanced Form Features
<div s-app>
<forms-plugin-demo></forms-plugin-demo>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// Forms plugin (simplified)*
function createForm(initialData = {}, options = {}) {
const state = reactive({
data: JSON.parse(JSON.stringify(initialData)),
errors: {},
touched: {},
dirty: {},
isValid: true,
isSubmitting: false,
submitCount: 0
});
const validateField = (name, value) => {
if (!options.validation \|\| !options.validation\[name\]) return null;
const rules = options.validation\[name\];
if (rules.required && !value) {
return \`\${name} is required\`;
}
if (rules.email && value && !value.includes(\'@\')) {
return \'Invalid email format\';
}
if (rules.min && value && value.length < rules.min) {
return \`Minimum length is \${rules.min}\`;
}
if (rules.max && value && value.length > rules.max) {
return \`Maximum length is \${rules.max}\`;
}
if (rules.pattern && value && !rules.pattern.test(value)) {
return rules.message \|\| \'Invalid format\';
}
if (rules.validate) {
return rules.validate(value);
}
return null;
};
const validateForm = () => {
const errors = {};
let isValid = true;
for (const \[name, value\] of Object.entries(state.data)) {
const error = validateField(name, value);
if (error) {
errors\[name\] = error;
isValid = false;
}
}
state.errors = errors;
state.isValid = isValid;
return isValid;
};
const handleChange = (name, value) => {
state.data\[name\] = value;
state.dirty\[name\] = true;
const error = validateField(name, value);
if (error) {
state.errors\[name\] = error;
} else {
delete state.errors\[name\];
}
state.isValid = Object.keys(state.errors).length === 0;
if (options.autoSave) {
clearTimeout(state.saveTimeout);
state.saveTimeout = setTimeout(() => {
localStorage.setItem(\'form_draft\', JSON.stringify(state.data));
}, 1000);
}
};
const handleBlur = (name) => {
state.touched\[name\] = true;
const error = validateField(name, state.data\[name\]);
if (error) {
state.errors\[name\] = error;
}
};
const handleSubmit = async (submitHandler) => {
state.submitCount++;
state.isSubmitting = true;
if (validateForm()) {
try {
await submitHandler(state.data);
if (options.autoSave) {
localStorage.removeItem(\'form_draft\');
}
if (options.onSuccess) options.onSuccess(state.data);
} catch (error) {
if (options.onError) options.onError(error);
}
}
state.isSubmitting = false;
};
const reset = () => {
state.data = JSON.parse(JSON.stringify(initialData));
state.errors = {};
state.touched = {};
state.dirty = {};
state.isValid = true;
};
*// Load draft if autoSave enabled*
if (options.autoSave) {
const draft = localStorage.getItem(\'form_draft\');
if (draft) {
try {
const parsed = JSON.parse(draft);
state.data = parsed;
} catch (e) {}
}
}
return {
state,
handleChange,
handleBlur,
handleSubmit,
reset,
validateForm
};
}
component(\'forms-plugin-demo\', () => {
const registrationForm = createForm({
username: \'\',
email: \'\',
password: \'\',
confirmPassword: \'\',
age: \'\',
country: \'\',
terms: false
}, {
validation: {
username: {
required: true,
min: 3,
max: 20,
pattern: /\^\[a-zA-Z0-9_\]+\$/,
message: \'Username can only contain letters, numbers, and underscores\'
},
email: {
required: true,
email: true
},
password: {
required: true,
min: 8,
validate: (value) => {
if (!/\[A-Z\]/.test(value)) return \'Must contain uppercase letter\';
if (!/\[a-z\]/.test(value)) return \'Must contain lowercase letter\';
if (!/\[0-9\]/.test(value)) return \'Must contain number\';
return null;
}
},
confirmPassword: {
required: true,
validate: (value) => {
if (value !== registrationForm.state.data.password) {
return \'Passwords do not match\';
}
return null;
}
},
age: {
required: true,
validate: (value) => {
const age = parseInt(value);
if (isNaN(age) \|\| age < 18 \|\| age > 120) {
return \'Age must be between 18 and 120\';
}
return null;
}
},
country: {
required: true
},
terms: {
validate: (value) => {
if (!value) return \'You must accept the terms\';
return null;
}
}
},
autoSave: true,
onSuccess: (data) => {
alert(\'Registration successful!\');
console.log(\'Form data:\', data);
},
onError: (error) => {
alert(\'Error: \' + error.message);
}
});
return {
render: () => \`
<div style=\"max-width: 600px; margin: 20px auto; padding: 30px;
background: white; border-radius: 12px; box-shadow: 0 4px 6px
rgba(0,0,0,0.1);\">
<h2>📝 Professional Form Handling</h2>
<!-- Form Status -->
<div style=\"display: flex; gap: 10px; margin: 20px 0; padding: 10px;
background: #f8f9fa; border-radius: 4px; font-size: 12px;\">
<span>Valid: \${registrationForm.state.isValid ? \'✅\' :
\'❌\'}</span>
<span>Dirty: \${Object.keys(registrationForm.state.dirty).length}
fields</span>
<span>Touched: \${Object.keys(registrationForm.state.touched).length}
fields</span>
<span>Errors:
\${Object.keys(registrationForm.state.errors).length}</span>
<span>Submit Count: \${registrationForm.state.submitCount}</span>
</div>
<form onsubmit=\"event.preventDefault();
this.closest(\'forms-plugin-demo\').handleSubmit()\">
<!-- Username -->
<div style=\"margin: 20px 0;\">
<label style=\"display: block; margin-bottom: 5px; font-weight:
600;\">
Username *
</label>
<input type=\"text\"
value=\"\${registrationForm.state.data.username}\"
oninput=\"this.closest(\'forms-plugin-demo\').handleChange(\'username\',
this.value)\"
onblur=\"this.closest(\'forms-plugin-demo\').handleBlur(\'username\')\"
placeholder=\"Enter username\"
style=\"width: 100%; padding: 10px; border: 2px solid
\${registrationForm.state.errors.username ? \'#dc3545\' : \'#ddd\'};
border-radius: 4px;\">
\${registrationForm.state.errors.username ? \`
<span style=\"color: #dc3545; font-size:
12px;\">\${registrationForm.state.errors.username}</span>
\` : \'\'}
<small style=\"color: #666;\">3-20 characters, letters, numbers,
underscore</small>
</div>
<!-- Email -->
<div style=\"margin: 20px 0;\">
<label style=\"display: block; margin-bottom: 5px; font-weight:
600;\">
Email *
</label>
<input type=\"email\"
value=\"\${registrationForm.state.data.email}\"
oninput=\"this.closest(\'forms-plugin-demo\').handleChange(\'email\',
this.value)\"
onblur=\"this.closest(\'forms-plugin-demo\').handleBlur(\'email\')\"
placeholder=\"Enter email\"
style=\"width: 100%; padding: 10px; border: 2px solid
\${registrationForm.state.errors.email ? \'#dc3545\' : \'#ddd\'};
border-radius: 4px;\">
\${registrationForm.state.errors.email ? \`
<span style=\"color: #dc3545; font-size:
12px;\">\${registrationForm.state.errors.email}</span>
\` : \'\'}
</div>
<!-- Password -->
<div style=\"margin: 20px 0;\">
<label style=\"display: block; margin-bottom: 5px; font-weight:
600;\">
Password *
</label>
<input type=\"password\"
value=\"\${registrationForm.state.data.password}\"
oninput=\"this.closest(\'forms-plugin-demo\').handleChange(\'password\',
this.value)\"
onblur=\"this.closest(\'forms-plugin-demo\').handleBlur(\'password\')\"
placeholder=\"Enter password\"
style=\"width: 100%; padding: 10px; border: 2px solid
\${registrationForm.state.errors.password ? \'#dc3545\' : \'#ddd\'};
border-radius: 4px;\">
\${registrationForm.state.errors.password ? \`
<span style=\"color: #dc3545; font-size:
12px;\">\${registrationForm.state.errors.password}</span>
\` : \'\'}
<small style=\"color: #666;\">Min 8 chars, with uppercase, lowercase,
and number</small>
</div>
<!-- Confirm Password -->
<div style=\"margin: 20px 0;\">
<label style=\"display: block; margin-bottom: 5px; font-weight:
600;\">Confirm Password *
</label>
<input type="password"
value="${registrationForm.state.data.confirmPassword}"
oninput="this.closest('forms-plugin-demo').handleChange('confirmPassword', this.value)"
onblur="this.closest('forms-plugin-demo').handleBlur('confirmPassword')"
placeholder="Confirm password"
style="width: 100%; padding: 10px; border: 2px solid ${registrationForm.state.errors.confirmPassword ? '#dc3545' : '#ddd'}; border-radius: 4px;">
${registrationForm.state.errors.confirmPassword ? `
<span style="color: #dc3545; font-size: 12px;">${registrationForm.state.errors.confirmPassword}</span>
` : ''}
</div>
<!-- Age -->
<div style="margin: 20px 0;">
<label style="display: block; margin-bottom: 5px; font-weight: 600;">
Age *
</label>
<input type="number"
value="${registrationForm.state.data.age}"
oninput="this.closest('forms-plugin-demo').handleChange('age', this.value)"
onblur="this.closest('forms-plugin-demo').handleBlur('age')"
placeholder="Enter age"
style="width: 100%; padding: 10px; border: 2px solid ${registrationForm.state.errors.age ? '#dc3545' : '#ddd'}; border-radius: 4px;">
${registrationForm.state.errors.age ? `
<span style="color: #dc3545; font-size: 12px;">${registrationForm.state.errors.age}</span>
` : ''}
</div>
<!-- Country -->
<div style="margin: 20px 0;">
<label style="display: block; margin-bottom: 5px; font-weight: 600;">
Country *
</label>
<select onchange="this.closest('forms-plugin-demo').handleChange('country', this.value)"
onblur="this.closest('forms-plugin-demo').handleBlur('country')"
style="width: 100%; padding: 10px; border: 2px solid ${registrationForm.state.errors.country ? '#dc3545' : '#ddd'}; border-radius: 4px;">
<option value="">Select a country</option>
<option value="us" ${registrationForm.state.data.country === 'us' ? 'selected' : ''}>United States</option>
<option value="ca" ${registrationForm.state.data.country === 'ca' ? 'selected' : ''}>Canada</option>
<option value="uk" ${registrationForm.state.data.country === 'uk' ? 'selected' : ''}>United Kingdom</option>
</select>
${registrationForm.state.errors.country ? `
<span style="color: #dc3545; font-size: 12px;">${registrationForm.state.errors.country}</span>
` : ''}
</div>
<!-- Terms -->
<div style="margin: 20px 0;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox"
${registrationForm.state.data.terms ? 'checked' : ''}
onchange="this.closest('forms-plugin-demo').handleChange('terms', this.checked)">
I accept the terms and conditions *
</label>
${registrationForm.state.errors.terms ? `
<span style="color: #dc3545; font-size: 12px;">${registrationForm.state.errors.terms}</span>
` : ''}
</div>
<!-- Form Actions -->
<div style="display: flex; gap: 20px; margin-top: 30px;">
<button type="submit"
style="flex:1; padding: 12px; background: ${registrationForm.state.isValid && !registrationForm.state.isSubmitting ? '#28a745' : '#ccc'}; color: white; border: none; border-radius: 4px; font-size: 16px; cursor: pointer;"
${!registrationForm.state.isValid || registrationForm.state.isSubmitting ? 'disabled' : ''}>
${registrationForm.state.isSubmitting ? 'Submitting...' : 'Register'}
</button>
<button type="button"
onclick="this.closest('forms-plugin-demo').resetForm()"
style="flex:1; padding: 12px; background: #ffc107; color: #333; border: none; border-radius: 4px; font-size: 16px; cursor: pointer;">
Reset
</button>
</div>
</form>
<!-- Form State Debug -->
<div style="margin-top: 40px; padding: 20px; background: #333; color: #fff; border-radius: 8px;">
<h4 style="color: #fff;">Form State</h4>
<pre style="color: #6a9955; overflow-x: auto;">
Data: ${JSON.stringify(registrationForm.state.data, null, 2)}
Errors: ${JSON.stringify(registrationForm.state.errors, null, 2)}
Touched: ${JSON.stringify(registrationForm.state.touched)}
Dirty: ${JSON.stringify(registrationForm.state.dirty)}
</pre>
</div>
<!-- Auto-save indicator -->
<div style="margin-top: 20px; padding: 10px; background: #e3f2fd; border-radius: 4px; text-align: center;">
💾 Auto-save enabled - form data is saved to localStorage
</div>
</div>
`,
handleChange: (name, value) => registrationForm.handleChange(name, value),
handleBlur: (name) => registrationForm.handleBlur(name),
handleSubmit: () => registrationForm.handleSubmit((data) => {
console.log('Submitting:', data);
return new Promise(resolve => setTimeout(resolve, 1000));
}),
resetForm: () => registrationForm.reset()
};
});
createApp().mount('[s-app]');
</script>
</div>
15.6 @simplijs/devtools - Development Tools
The devtools plugin provides real-time component and state inspection during development.
DevTools Integration
<div s-app>
<devtools-demo></devtools-demo>
<script type=\"module\">
import { createApp, component, reactive } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
*// DevTools plugin (simplified)*
function initDevTools() {
if (window.SimpliDevTools) return;
window.SimpliDevTools = {
components: new Map(),
stateSnapshots: \[\],
registerComponent(name, instance) {
this.components.set(name, instance);
},
inspectComponent(name) {
return this.components.get(name);
},
takeSnapshot() {
const snapshot = {
timestamp: new Date().toISOString(),
components: Array.from(this.components.entries()).map((\[name,
instance\]) => ({
name,
state: instance.state ? JSON.parse(JSON.stringify(instance.state)) :
null,
props: instance.props \|\| {}
}))
};
this.stateSnapshots.push(snapshot);
return snapshot;
},
showPanel() {
const panel = document.getElementById(\'simplijs-devtools\');
if (panel) {
panel.style.display = panel.style.display === \'none\' ? \'block\' :
\'none\';
}
}
};
*// Create devtools panel*
const panel = document.createElement(\'div\');
panel.id = \'simplijs-devtools\';
panel.style.cssText = \`
position: fixed;
bottom: 20px;
right: 20px;
width: 400px;
height: 500px;
background: #1e1e1e;
color: #d4d4d4;
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
z-index: 10000;
display: none;
flex-direction: column;
font-family: monospace;
\`;
panel.innerHTML = \`
<div style=\"padding: 10px; background: #333; border-radius: 8px 8px 0
0; display: flex; justify-content: space-between;\">
<span>🔧 SimpliJS DevTools</span>
<button onclick=\"SimpliDevTools.showPanel()\" style=\"background:
none; border: none; color: white; cursor: pointer;\">✕</button>
</div>
<div style=\"flex:1; overflow-y: auto; padding: 10px;\"
id=\"devtools-content\">
<div style=\"margin: 10px 0;\">
<button onclick=\"SimpliDevTools.takeSnapshot()\" style=\"width: 100%;
padding: 5px;\">Take Snapshot</button>
</div>
<div id=\"snapshot-list\"></div>
</div>
\`;
document.body.appendChild(panel);
*// Add keyboard shortcut (Ctrl+Shift+D)*
window.addEventListener(\'keydown\', (e) => {
if (e.ctrlKey && e.shiftKey && e.key === \'D\') {
window.SimpliDevTools.showPanel();
}
});
console.log(\'🔧 SimpliJS DevTools initialized (Ctrl+Shift+D to
open)\');
}
component(\'devtools-demo\', () => {
*// Initialize devtools*
initDevTools();
const counterState = reactive({ count: 0, step: 1 });
const todoState = reactive({
todos: \[\],
filter: \'all\'
});
*// Register components with devtools*
setTimeout(() => {
if (window.SimpliDevTools) {
window.SimpliDevTools.registerComponent(\'counter\', {
state: counterState,
props: { title: \'Counter Component\' }
});
window.SimpliDevTools.registerComponent(\'todos\', {
state: todoState,
props: { maxItems: 10 }
});
}
}, 100);
return {
render: () => \`
<div style=\"max-width: 800px; margin: 20px auto; padding: 20px;\">
<h2>🛠️ DevTools Demo</h2>
<div style=\"background: #f8f9fa; padding: 15px; border-radius: 8px;
margin: 20px 0;\">
<p>Press <kbd>Ctrl+Shift+D</kbd> to open DevTools panel</p>
<button onclick=\"SimpliDevTools?.showPanel()\" style=\"padding: 8px
16px; background: #007bff; color: white; border: none; border-radius:
4px;\">
Open DevTools
</button>
</div>
<!-- Counter Component -->
<div style=\"background: white; padding: 20px; border-radius: 8px;
margin: 20px 0;\">
<h3>Counter Component</h3>
<p>Count: \${counterState.count}</p>
<div style=\"display: flex; gap: 10px;\">
<button onclick=\"counterState.count += counterState.step\"
style=\"padding: 8px 16px; background: #28a745; color: white; border:
none; border-radius: 4px;\">
+\${counterState.step}
</button>
<button onclick=\"counterState.count = 0\"
style=\"padding: 8px 16px; background: #dc3545; color: white; border:
none; border-radius: 4px;\">
Reset
</button>
</div>
<div style=\"margin-top: 10px;\">
<label>Step:</label>
<input type=\"number\"
value=\"\${counterState.step}\"
onchange=\"counterState.step = parseInt(this.value) \|\| 1\"
style=\"width: 60px; padding: 5px; margin-left: 10px;\">
</div>
</div>
<!-- Todo Component -->
<div style=\"background: white; padding: 20px; border-radius: 8px;\">
<h3>Todos Component</h3>
<div style=\"display: flex; gap: 10px; margin: 10px 0;\">
<input type=\"text\" id=\"devTodoInput\" placeholder=\"Add a todo\...\"
style=\"flex:1; padding: 8px;\">
<button onclick=\"todoState.todos.push({ id: Date.now(), text:
document.getElementById(\'devTodoInput\').value, completed: false });
document.getElementById(\'devTodoInput\').value = \'\';\"
style=\"padding: 8px 16px; background: #007bff; color: white; border:
none; border-radius: 4px;\">
Add
</button>
</div>
<div style=\"margin: 10px 0;\">
<select onchange=\"todoState.filter = this.value\">
<option value=\"all\">All</option>
<option value=\"active\">Active</option>
<option value=\"completed\">Completed</option>
</select>
</div>
<div style=\"display: grid; gap: 5px;\">
\${todoState.todos
.filter(t => {
if (todoState.filter === \'active\') return !t.completed;
if (todoState.filter === \'completed\') return t.completed;
return true;
})
.map(todo => \`
<div style=\"display: flex; align-items: center; gap: 10px; padding:
5px;\">
<input type=\"checkbox\"
\${todo.completed ? \'checked\' : \'\'}
onchange=\"todo.completed = !todo.completed\">
<span style=\"\${todo.completed ? \'text-decoration: line-through;
color: #999;\' : \'\'}\">
\${todo.text}
</span>
</div>
\`).join(\'\')}
</div>
</div>
<div style=\"margin-top: 20px; padding: 15px; background: #e8f4fd;
border-radius: 8px;\">
<h4>DevTools Features:</h4>
<ul>
<li>Component inspection</li>
<li>State snapshots</li>
<li>Real-time updates</li>
<li>Keyboard shortcut (Ctrl+Shift+D)</li>
</ul>
</div>
</div>
\`
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>Chapter 15 Summary
You've now explored the powerful SimpliJS plugin ecosystem:
@simplijs/auth - Professional authentication with session management
@simplijs/vault-pro - Advanced state with time-travel and persistence
@simplijs/router - Declarative SPA routing with guards and transitions
@simplijs/forms - Professional form handling with validation and wizards
@simplijs/devtools - Real-time component and state inspection
Each plugin extends SimpliJS with production-ready features while maintaining the framework's signature simplicity and HTML-First philosophy. Plugins can be used independently or together to build sophisticated applications.
In the next chapter, we'll explore real-world project examples that combine everything you've learned.
End of Chapter 15
Chapter 16: Real-World Project: Building an E-Commerce Platform
Welcome to Chapter 16, where we'll build a complete, production-ready e-commerce platform using everything we've learned about SimpliJS. This project combines components, routing, state management, forms, authentication, and plugins into a cohesive, real-world application.
16.1 Project Overview
Let's start by understanding what we're building and planning the architecture.
Project Requirements
<div s-app>
<project-overview></project-overview>
<script type=\"module\">
import { createApp, component } from
\'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js\';
component(\'project-overview\', () => {
return {
render: () => \`
<div style=\"max-width: 1000px; margin: 20px auto; padding: 30px;\">
<h1>🛍️ E-Commerce Platform - Project Overview</h1>
<div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 30px;
margin: 40px 0;\">
<!-- Features -->
<div style=\"background: #e3f2fd; padding: 25px; border-radius:
12px;\">
<h2 style=\"color: #1976d2;\">Features</h2>
<ul style=\"list-style: none; padding: 0;\">
<li style=\"margin: 15px 0;\">✅ Product catalog with
categories</li>
<li style=\"margin: 15px 0;\">✅ Shopping cart functionality</li>
<li style=\"margin: 15px 0;\">✅ User authentication</li>
<li style=\"margin: 15px 0;\">✅ Checkout process</li>
<li style=\"margin: 15px 0;\">✅ Order management</li>
<li style=\"margin: 15px 0;\">✅ Product search & filters</li>
<li style=\"margin: 15px 0;\">✅ Wishlist</li>
<li style=\"margin: 15px 0;\">✅ Reviews & ratings</li>
</ul>
</div>
<!-- Tech Stack -->
<div style=\"background: #d4edda; padding: 25px; border-radius:
12px;\">
<h2 style=\"color: #28a745;\">Tech Stack</h2>
<ul style=\"list-style: none; padding: 0;\">
<li style=\"margin: 15px 0;\">🔷 SimpliJS Core</li>
<li style=\"margin: 15px 0;\">🔷 \@simplijs/router</li>
<li style=\"margin: 15px 0;\">🔷 \@simplijs/auth</li>
<li style=\"margin: 15px 0;\">🔷 \@simplijs/forms</li>
<li style=\"margin: 15px 0;\">🔷 \@simplijs/vault-pro</li>
<li style=\"margin: 15px 0;\">🔷 LocalStorage for persistence</li>
<li style=\"margin: 15px 0;\">🔷 CSS Grid/Flexbox</li>
</ul>
</div>
</div>
<!-- Architecture Diagram -->
<div style=\"background: white; padding: 30px; border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);\">
<h2>Application Architecture</h2>
<pre style=\"background: #f8f9fa; padding: 20px; border-radius:
8px;\">
┌─────────────────────────────────────────────────────────────┐
│ App Component │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Header │ │ Navigation │ │ User Status │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Router View │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ │ │ │
│ │ ├─ Home Page │ │
│ │ ├─ Products Page │ │
│ │ ├─ Product Details │ │
│ │ ├─ Cart Page │ │
│ │ ├─ Checkout │ │
│ │ ├─ Login/Register │ │
│ │ └─ User Dashboard │ │
│ └───────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Footer │
└─────────────────────────────────────────────────────────────┘
State Management
┌─────────────────────────────────────────────────────────────┐
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Cart │ │ Auth │ │ Products │ │
│ │ (Vault) │ │ (Auth) │ │ (Reactive) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
</pre>
</div>
</div>
\`
};
});
createApp().mount(\'\[s-app\]\');
</script>
</div>16.2 Complete E-Commerce Implementation
Now let's build the complete e-commerce platform step by step.
Full Application Code
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width,
initial-scale=1.0\">
<title>🛍️ SimpliShop - E-Commerce Platform</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto,
sans-serif;
background: #f8f9fa;
color: #333;
line-height: 1.6;
}
/* Layout */
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
/* Header */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px 0;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 24px;
font-weight: bold;
text-decoration: none;
color: white;
}
.logo span {
font-size: 32px;
margin-right: 5px;
}
/* Navigation */
.nav {
display: flex;
gap: 30px;
align-items: center;
}
.nav a {
color: white;
text-decoration: none;
font-weight: 500;
transition: opacity 0.3s;
}
.nav a:hover {
opacity: 0.8;
}
.cart-icon {
position: relative;
font-size: 20px;
}
.cart-count {
position: absolute;
top: -8px;
right: -8px;
background: #dc3545;
color: white;
font-size: 12px;
padding: 2px 6px;
border-radius: 50%;
}
.user-menu {
display: flex;
align-items: center;
gap: 15px;
}
.avatar {
width: 35px;
height: 35px;
background: rgba(255,255,255,0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
/* Main Content */
.main-content {
flex: 1;
padding: 40px 0;
}
/* Footer */
.footer {
background: #333;
color: white;
padding: 40px 0;
margin-top: auto;
}
.footer-content {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 30px;
}
.footer-section h3 {
margin-bottom: 20px;
color: #667eea;
}
.footer-section ul {
list-style: none;
}
.footer-section li {
margin: 10px 0;
}
.footer-section a {
color: #999;
text-decoration: none;
transition: color 0.3s;
}
.footer-section a:hover {
color: white;
}
/* Cards */
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 30px;
margin: 30px 0;
}
.product-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
transition: transform 0.3s, box-shadow 0.3s;
cursor: pointer;
}
.product-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.15);
}
.product-image {
height: 200px;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
font-size: 80px;
border-bottom: 1px solid #eee;
}
.product-info {
padding: 20px;
}
.product-title {
font-size: 18px;
margin-bottom: 10px;
color: #333;
}
.product-price {
font-size: 24px;
color: #28a745;
font-weight: bold;
margin-bottom: 10px;
}
.product-rating {
color: #ffc107;
margin-bottom: 10px;
}
.product-description {
color: #666;
font-size: 14px;
margin-bottom: 15px;
line-height: 1.5;
}
.product-actions {
display: flex;
gap: 10px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5a67d8;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover {
background: #218838;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.btn-outline {
background: transparent;
border: 2px solid #667eea;
color: #667eea;
}
.btn-outline:hover {
background: #667eea;
color: white;
}
/* Cart */
.cart-item {
display: flex;
align-items: center;
gap: 20px;
padding: 20px;
background: white;
border-radius: 8px;
margin: 10px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.cart-item-image {
width: 80px;
height: 80px;
background: #f8f9fa;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
}
.cart-item-details {
flex: 1;
}
.cart-item-title {
font-weight: 600;
margin-bottom: 5px;
}
.cart-item-price {
color: #28a745;
font-weight: bold;
}
.cart-item-quantity {
display: flex;
align-items: center;
gap: 10px;
}
.quantity-btn {
width: 30px;
height: 30px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
}
.quantity-input {
width: 50px;
text-align: center;
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
.cart-summary {
background: white;
padding: 30px;
border-radius: 8px;
margin-top: 30px;
}
.cart-total {
display: flex;
justify-content: space-between;
font-size: 24px;
font-weight: bold;
margin: 20px 0;
padding-top: 20px;
border-top: 2px solid #eee;
}
/* Forms */
.form-container {
max-width: 400px;
margin: 40px auto;
padding: 30px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.form-group {
margin: 20px 0;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.form-group input {
width: 100%;
padding: 10px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 16px;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.form-error {
color: #dc3545;
font-size: 14px;
margin-top: 5px;
}
/* Alerts */
.alert {
padding: 15px;
border-radius: 6px;
margin: 20px 0;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-danger {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
/* Filters */
.filters {
background: white;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.filter-group {
flex: 1;
min-width: 200px;
}
.filter-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.filter-group select,
.filter-group input {
width: 100%;
padding: 8px;
border: 2px solid #ddd;
border-radius: 4px;
}
/* Loading Spinner */
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 40px auto;
}
\@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div s-app class=\"app\">
*<!-- App State -->*
<div s-state=\"{
// Products data
products: \[
{
id: 1,
name: \'Wireless Headphones\',
price: 99.99,
category: \'electronics\',
rating: 4.5,
image: \'🎧\',
description: \'Premium wireless headphones with noise cancellation and
30-hour battery life.\',
inStock: true
},
{
id: 2,
name: \'Smart Watch\',
price: 199.99,
category: \'electronics\',
rating: 4.8,
image: \'⌚\',
description: \'Track your fitness, receive notifications, and more with
this stylish smart watch.\',
inStock: true
},
{
id: 3,
name: \'Laptop Backpack\',
price: 49.99,
category: \'accessories\',
rating: 4.3,
image: \'🎒\',
description: \'Water-resistant backpack with padded laptop compartment
and USB charging port.\',
inStock: false
},
{
id: 4,
name: \'Mechanical Keyboard\',
price: 129.99,
category: \'electronics\',
rating: 4.7,
image: \'⌨️\',
description: \'RGB mechanical keyboard with Cherry MX switches and
programmable keys.\',
inStock: true
},
{
id: 5,
name: \'Coffee Mug\',
price: 14.99,
category: \'home\',
rating: 4.2,
image: \'☕\',
description: \'Ceramic coffee mug with heat-changing design. Perfect for
your morning coffee.\',
inStock: true
},
{
id: 6,
name: \'Desk Lamp\',
price: 39.99,
category: \'home\',
rating: 4.4,
image: \'💡\',
description: \'LED desk lamp with adjustable brightness and color
temperature.\',
inStock: true
}
\],
// Cart state (using Vault pattern)
cart: {
items: JSON.parse(localStorage.getItem(\'cart\')) \|\| \[\],
total: 0
},
// Auth state
auth: {
user: JSON.parse(localStorage.getItem(\'user\')) \|\| null,
isAuthenticated: !!localStorage.getItem(\'user\')
},
// UI state
filters: {
category: \'all\',
minPrice: 0,
maxPrice: 1000,
search: \'\'
},
// Wishlist
wishlist: JSON.parse(localStorage.getItem(\'wishlist\')) \|\| \[\],
// Current page for pagination
currentPage: 1,
itemsPerPage: 6
}\">
*<!-- Header -->*
<header class=\"header\">
<div class=\"container\">
<div class=\"header-content\">
<a href=\"#\" s-link=\"/\" class=\"logo\">
<span>🛍️</span> SimpliShop
</a>
<nav class=\"nav\">
<a href=\"#\" s-link=\"/\">Home</a>
<a href=\"#\" s-link=\"/products\">Products</a>
<a href=\"#\" s-link=\"/categories\">Categories</a>
<a href=\"#\" s-link=\"/deals\">Deals</a>
<div class=\"user-menu\">
<a href=\"#\" s-link=\"/cart\" class=\"cart-icon\">
🛒
<span class=\"cart-count\" s-if=\"cart.items.length > 0\">
{cart.items.reduce((sum, item) => sum + item.quantity, 0)}
</span>
</a>
<span s-if=\"!auth.isAuthenticated\">
<a href=\"#\" s-link=\"/login\">Login</a> \|
<a href=\"#\" s-link=\"/register\">Register</a>
</span>
<span s-if=\"auth.isAuthenticated\" class=\"user-menu\">
<a href=\"#\" s-link=\"/dashboard\">
<div class=\"avatar\">
{auth.user?.name?.\[0\] \|\| \'U\'}
</div>
</a>
<a href=\"#\" s-link=\"/wishlist\">❤️</a>
<button s-click=\"logout()\" class=\"btn btn-outline\" style=\"padding:
5px 10px;\">
Logout
</button>
</span>
</div>
</nav>
</div>
</div>
</header>
*<!-- Main Content with Router View -->*
<main class=\"main-content\">
<div class=\"container\">
<div s-view></div>
</div>
</main>
*<!-- Footer -->*
<footer class=\"footer\">
<div class=\"container\">
<div class=\"footer-content\">
<div class=\"footer-section\">
<h3>About Us</h3>
<p>SimpliShop is your one-stop destination for quality products at
great prices.</p>
</div>
<div class=\"footer-section\">
<h3>Quick Links</h3>
<ul>
<li><a href=\"#\" s-link=\"/about\">About</a></li>
<li><a href=\"#\" s-link=\"/contact\">Contact</a></li>
<li><a href=\"#\" s-link=\"/faq\">FAQ</a></li>
<li><a href=\"#\" s-link=\"/shipping\">Shipping</a></li>
</ul>
</div>
<div class=\"footer-section\">
<h3>Categories</h3>
<ul>
<li><a href=\"#\"
s-link=\"/products?category=electronics\">Electronics</a></li>
<li><a href=\"#\"
s-link=\"/products?category=accessories\">Accessories</a></li>
<li><a href=\"#\" s-link=\"/products?category=home\">Home &
Living</a></li>
</ul>
</div>
<div class=\"footer-section\">
<h3>Contact</h3>
<ul>
<li>📧 support@simplishop.com</li>
<li>📞 (555) 123-4567</li>
<li>📍 123 Main St, City, State</li>
</ul>
</div>
</div>
<div style=\"text-align: center; margin-top: 40px; color: #999;\">
© 2024 SimpliShop. All rights reserved.
</div>
</div>
</footer>
*<!-- Routes -->*
*<!-- Home Page -->*
<div s-route=\"/\">
<div>
*<!-- Hero Section -->*
<div style=\"background: linear-gradient(135deg, #667eea 0%, #764ba2
100%); color: white; padding: 60px 0; text-align: center; border-radius:
12px; margin: 20px 0;\">
<h1 style=\"font-size: 48px; margin-bottom: 20px;\">Welcome to
SimpliShop</h1>
<p style=\"font-size: 18px; margin-bottom: 30px;\">Discover amazing
products at unbeatable prices</p>
<a href=\"#\" s-link=\"/products\" class=\"btn btn-success\"
style=\"padding: 15px 40px; font-size: 18px;\">
Shop Now
</a>
</div>
*<!-- Featured Products -->*
<h2 style=\"margin: 40px 0 20px;\">Featured Products</h2>
<div class=\"product-grid\">
<div s-for=\"product in products.slice(0, 3)\" s-key=\"product.id\"
class=\"product-card\">
<div class=\"product-image\">{product.image}</div>
<div class=\"product-info\">
<h3 class=\"product-title\">{product.name}</h3>
<div class=\"product-price\">\${product.price}</div>
<div class=\"product-rating\">
{\'★\'.repeat(Math.floor(product.rating))}{\'☆\'.repeat(5 -
Math.floor(product.rating))}
</div>
<p class=\"product-description\">{product.description}</p>
<div class=\"product-actions\">
<button s-click=\"addToCart(product)\" class=\"btn btn-primary\"
s-if=\"product.inStock\">Add to Cart
</button>
<button s-click="addToWishlist(product)" class="btn btn-outline">
❤️
</button>
</div>
</div>
</div>
</div>
<!-- Categories -->
<h2 style="margin: 40px 0 20px;">Shop by Category</h2>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 30px;">
<div class="product-card" style="text-align: center;">
<div class="product-image" style="font-size: 100px;">📱</div>
<div class="product-info">
<h3>Electronics</h3>
<a href="#" s-link="/products?category=electronics" class="btn btn-primary">Browse</a>
</div>
</div>
<div class="product-card" style="text-align: center;">
<div class="product-image" style="font-size: 100px;">🎒</div>
<div class="product-info">
<h3>Accessories</h3>
<a href="#" s-link="/products?category=accessories" class="btn btn-primary">Browse</a>
</div>
</div>
<div class="product-card" style="text-align: center;">
<div class="product-image" style="font-size: 100px;">🏠</div>
<div class="product-info">
<h3>Home & Living</h3>
<a href="#" s-link="/products?category=home" class="btn btn-primary">Browse</a>
</div>
</div>
</div>
</div>
</div>
<!-- Products Page -->
<div s-route="/products">
<div>
<h1>All Products</h1>
<!-- Filters -->
<div class="filters">
<div class="filter-group">
<label>Category:</label>
<select s-model="filters.category">
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="accessories">Accessories</option>
<option value="home">Home & Living</option>
</select>
</div>
<div class="filter-group">
<label>Price Range:</label>
<div style="display: flex; gap: 10px;">
<input type="number" s-bind="filters.minPrice" placeholder="Min" min="0">
<input type="number" s-bind="filters.maxPrice" placeholder="Max" min="0">
</div>
</div>
<div class="filter-group">
<label>Search:</label>
<input type="text" s-bind="filters.search" placeholder="Search products...">
</div>
</div>
<!-- Filtered Products -->
<div s-state="{
filteredProducts: products
.filter(p => filters.category === 'all' || p.category === filters.category)
.filter(p => p.price >= (filters.minPrice || 0) && p.price <= (filters.maxPrice || 1000))
.filter(p => !filters.search || p.name.toLowerCase().includes(filters.search.toLowerCase()))
}">
<div class="product-grid">
<div s-for="product in filteredProducts" s-key="product.id" class="product-card">
<a href="#" s-link="/products/{product.id}" style="text-decoration: none; color: inherit;">
<div class="product-image">{product.image}</div>
<div class="product-info">
<h3 class="product-title">{product.name}</h3>
<div class="product-price">${product.price}</div>
<div class="product-rating">
{'★'.repeat(Math.floor(product.rating))}{'☆'.repeat(5 - Math.floor(product.rating))}
</div>
</div>
</a>
<div class="product-info" style="padding-top: 0;">
<div class="product-actions">
<button s-click="addToCart(product)" class="btn btn-primary" s-if="product.inStock">
Add to Cart
</button>
<button s-click="addToWishlist(product)" class="btn btn-outline">
❤️
</button>
</div>
</div>
</div>
</div>
<!-- Pagination -->
<div s-if="filteredProducts.length > itemsPerPage" style="display: flex; gap: 10px; justify-content: center; margin: 40px 0;">
<button s-click="currentPage = Math.max(1, currentPage - 1)"
class="btn btn-outline"
s-if="currentPage > 1">
Previous
</button>
<span style="padding: 10px;">Page {currentPage} of {Math.ceil(filteredProducts.length / itemsPerPage)}</span>
<button s-click="currentPage = Math.min(Math.ceil(filteredProducts.length / itemsPerPage), currentPage + 1)"
class="btn btn-outline"
s-if="currentPage < Math.ceil(filteredProducts.length / itemsPerPage)">
Next
</button>
</div>
</div>
</div>
</div>
<!-- Product Detail Page -->
<div s-route="/products/:id">
<div s-state="{
productId: parseInt(window.location.pathname.split('/').pop()),
product: products.find(p => p.id === productId)
}">
<div s-if="product" style="background: white; padding: 40px; border-radius: 12px;">
<a href="#" s-link="/products" style="color: #667eea; text-decoration: none; margin-bottom: 20px; display: inline-block;">
← Back to Products
</a>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 50px;">
<div style="font-size: 200px; text-align: center; background: #f8f9fa; padding: 40px; border-radius: 12px;">
{product.image}
</div>
<div>
<h1>{product.name}</h1>
<div class="product-rating" style="font-size: 24px; margin: 10px 0;">
{'★'.repeat(Math.floor(product.rating))}{'☆'.repeat(5 - Math.floor(product.rating))}
<span style="color: #666; font-size: 16px;">({product.rating} stars)</span>
</div>
<div class="product-price" style="font-size: 36px; margin: 20px 0;">
${product.price}
</div>
<p style="line-height: 1.8; margin: 20px 0;">{product.description}</p>
<div style="margin: 30px 0;">
<span style="padding: 5px 15px; background: {product.inStock ? '#d4edda' : '#f8d7da'}; color: {product.inStock ? '#155724' : '#721c24'}; border-radius: 20px;">
{product.inStock ? 'In Stock' : 'Out of Stock'}
</span>
</div>
<div style="display: flex; gap: 20px;">
<button s-click="addToCart(product)"
class="btn btn-primary"
style="padding: 15px 40px; font-size: 16px;"
s-if="product.inStock">
Add to Cart
</button>
<button s-click="addToWishlist(product)"
class="btn btn-outline"
style="padding: 15px 40px; font-size: 16px;">
Add to Wishlist
</button>
</div>
<!-- Product Details -->
<div style="margin-top: 40px; padding-top: 40px; border-top: 1px solid #eee;">
<h3>Product Details</h3>
<ul style="margin-top: 20px;">
<li>Category: {product.category}</li>
<li>SKU: PROD-{product.id}</li>
<li>Free shipping on orders over $50</li>
<li>30-day return policy</li>
</ul>
</div>
</div>
</div>
<!-- Related Products -->
<div style="margin-top: 60px;">
<h2>Related Products</h2>
<div class="product-grid">
<div s-for="p in products.filter(p => p.category === product.category && p.id !== product.id).slice(0, 3)"
s-key="p.id"
class="product-card">
<a href="#" s-link="/products/{p.id}" style="text-decoration: none; color: inherit;">
<div class="product-image">{p.image}</div>
<div class="product-info">
<h3 class="product-title">{p.name}</h3>
<div class="product-price">${p.price}</div>
</div>
</a>
</div>
</div>
</div>
</div>
<div s-else style="text-align: center; padding: 60px;">
<h2>Product Not Found</h2>
<p>The product you're looking for doesn't exist.</p>
<a href="#" s-link="/products" class="btn btn-primary">Browse Products</a>
</div>
</div>
</div>
<!-- Cart Page -->
<div s-route="/cart">
<div>
<h1>Shopping Cart</h1>
<div s-if="cart.items.length === 0" style="text-align: center; padding: 60px;">
<p style="font-size: 18px; color: #666;">Your cart is empty</p>
<a href="#" s-link="/products" class="btn btn-primary" style="margin-top: 20px;">
Continue Shopping
</a>
</div>
<div s-else>
<!-- Cart Items -->
<div s-for="item, index in cart.items" s-key="item.id" class="cart-item">
<div class="cart-item-image">{item.image}</div>
<div class="cart-item-details">
<div class="cart-item-title">{item.name}</div>
<div class="cart-item-price">${item.price}</div>
</div>
<div class="cart-item-quantity">
<button class="quantity-btn" s-click="updateQuantity(item.id, item.quantity - 1)">-</button>
<input type="number"
class="quantity-input"
value="{item.quantity}"
min="1"
s-change="updateQuantity(item.id, parseInt(event.target.value))">
<button class="quantity-btn" s-click="updateQuantity(item.id, item.quantity + 1)">+</button>
</div>
<div style="font-weight: bold; min-width: 100px;">
${(item.price * item.quantity).toFixed(2)}
</div>
<button class="btn btn-danger" s-click="removeFromCart(item.id)">
Remove
</button>
</div>
<!-- Cart Summary -->
<div class="cart-summary">
<h2>Order Summary</h2>
<div style="display: flex; justify-content: space-between; margin: 15px 0;">
<span>Subtotal ({cart.items.reduce((sum, i) => sum + i.quantity, 0)} items):</span>
<span>${cart.items.reduce((sum, i) => sum + (i.price * i.quantity), 0).toFixed(2)}</span>
</div>
<div style="display: flex; justify-content: space-between; margin: 15px 0;">
<span>Shipping:</span>
<span>${cart.items.reduce((sum, i) => sum + (i.price * i.quantity), 0) > 50 ? 'FREE' : '$5.00'}</span>
</div>
<div style="display: flex; justify-content: space-between; margin: 15px 0;">
<span>Tax (8%):</span>
<span>${(cart.items.reduce((sum, i) => sum + (i.price * i.quantity), 0) * 0.08).toFixed(2)}</span>
</div>
<div class="cart-total">
<span>Total:</span>
<span>
${(
cart.items.reduce((sum, i) => sum + (i.price * i.quantity), 0) +
(cart.items.reduce((sum, i) => sum + (i.price * i.quantity), 0) > 50 ? 0 : 5) +
(cart.items.reduce((sum, i) => sum + (i.price * i.quantity), 0) * 0.08)
).toFixed(2)}
</span>
</div>
<div style="display: flex; gap: 20px;">
<a href="#" s-link="/checkout" class="btn btn-success" style="flex:1; padding: 15px; text-align: center; text-decoration: none;">
Proceed to Checkout
</a>
<button class="btn btn-danger" s-click="clearCart()" style="flex:1;">
Clear Cart
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Checkout Page -->
<div s-route="/checkout">
<div>
<h1>Checkout</h1>
<div s-if="!auth.isAuthenticated" class="alert alert-warning" style="margin: 20px 0;">
Please <a href="#" s-link="/login">login</a> or <a href="#" s-link="/register">register</a> to complete your purchase.
</div>
<div s-else style="display: grid; grid-template-columns: 2fr 1fr; gap: 30px;">
<!-- Shipping Form -->
<div style="background: white; padding: 30px; border-radius: 12px;">
<h2>Shipping Information</h2>
<form s-submit="placeOrder()">
<div class="form-group">
<label>Full Name:</label>
<input type="text" s-bind="checkoutInfo.fullName" value="{auth.user?.name}" required>
</div>
<div class="form-group">
<label>Email:</label>
<input type="email" s-bind="checkoutInfo.email" value="{auth.user?.email}" required>
</div>
<div class="form-group">
<label>Address:</label>
<input type="text" s-bind="checkoutInfo.address" placeholder="Street address" required>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
<div class="form-group">
<label>City:</label>
<input type="text" s-bind="checkoutInfo.city" required>
</div>
<div class="form-group">
<label>State:</label>
<input type="text" s-bind="checkoutInfo.state" required>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
<div class="form-group">
<label>ZIP Code:</label>
<input type="text" s-bind="checkoutInfo.zip" required>
</div>
<div class="form-group">
<label>Country:</label>
<select s-model="checkoutInfo.country">
<option value="US">United States</option>
<option value="CA">Canada</option>
<option value="UK">United Kingdom</option>
</select>
</div>
</div>
<div class="form-group">
<label>Payment Method:</label>
<div style="display: flex; gap: 20px;">
<label>
<input type="radio" name="payment" value="credit" s-model="checkoutInfo.payment"> Credit Card
</label>
<label>
<input type="radio" name="payment" value="paypal" s-model="checkoutInfo.payment"> PayPal
</label>
</div>
</div>
<button type="submit" class="btn btn-success" style="width: 100%; padding: 15px;">
Place Order
</button>
</form>
</div>
<!-- Order Summary -->
<div style="background: white; padding: 30px; border-radius: 12px; height: fit-content;">
<h2>Order Summary</h2>
<div s-for="item in cart.items" s-key="item.id" style="display: flex; justify-content: space-between; margin: 15px 0;">
<span>{item.name} x{item.quantity}</span>
<span>${(item.price * item.quantity).toFixed(2)}</span>
</div>
<hr style="margin: 20px 0;">
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
<span>Subtotal:</span>
<span>${cart.items.reduce((sum, i) => sum + (i.price * i.quantity), 0).toFixed(2)}</span>
</div>
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
<span>Shipping:</span>
<span>${cart.items.reduce((sum, i) => sum + (i.price * i.quantity), 0) > 50 ? '$0.00' : '$5.00'}</span>
</div>
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
<span>Tax:</span>
<span>${(cart.items.reduce((sum, i) => sum + (i.price * i.quantity), 0) * 0.08).toFixed(2)}</span>
</div>
<hr style="margin: 20px 0;">
<div style="display: flex; justify-content: space-between; font-size: 20px; font-weight: bold;">
<span>Total:</span>
<span>${(
cart.items.reduce((sum, i) => sum + (i.price * i.quantity), 0) +
(cart.items.reduce((sum, i) => sum + (i.price * i.quantity), 0) > 50 ? 0 : 5) +
(cart.items.reduce((sum, i) => sum + (i.price * i.quantity), 0) * 0.08)
).toFixed(2)}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Login Page -->
<div s-route="/login">
<div class="form-container">
<h2 style="text-align: center;">Login</h2>
<form s-submit="login()">
<div class="form-group">
<label>Email:</label>
<input type="email" s-bind="loginForm.email" value="demo@example.com" required>
</div>
<div class="form-group">
<label>Password:</label>
<input type="password" s-bind="loginForm.password" value="password" required>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">Login</button>
</form>
<p style="text-align: center; margin-top: 20px;">
Don't have an account? <a href="#" s-link="/register">Register</a>
</p>
<p style="text-align: center; font-size: 14px; color: #666;">
Demo credentials: demo@example.com / password
</p>
</div>
</div>
<!-- Register Page -->
<div s-route="/register">
<div class="form-container">
<h2 style="text-align: center;">Register</h2>
<form s-submit="register()">
<div class="form-group">
<label>Name:</label>
<input type="text" s-bind="registerForm.name" required>
</div>
<div class="form-group">
<label>Email:</label>
<input type="email" s-bind="registerForm.email" required>
</div>
<div class="form-group">
<label>Password:</label>
<input type="password" s-bind="registerForm.password" required>
</div>
<div class="form-group">
<label>Confirm Password:</label>
<input type="password" s-bind="registerForm.confirmPassword" required>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">Register</button>
</form>
<p style="text-align: center; margin-top: 20px;">
Already have an account? <a href="#" s-link="/login">Login</a>
</p>
</div>
</div>
<!-- User Dashboard -->
<div s-route="/dashboard">
<div s-if="!auth.isAuthenticated" class="alert alert-danger" style="text-align: center; padding: 40px;">
Please <a href="#" s-link="/login">login</a> to view your dashboard.
</div>
<div s-else style="background: white; padding: 30px; border-radius: 12px;">
<h1>Welcome, {auth.user?.name}!</h1>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 30px; margin: 40px 0;">
<div style="text-align: center; padding: 30px; background: #e3f2fd; border-radius: 8px;">
<div style="font-size: 48px;">📦</div>
<h3>Orders</h3>
<p style="font-size: 24px;">5</p>
</div>
<div style="text-align: center; padding: 30px; background: #d4edda; border-radius: 8px;">
<div style="font-size: 48px;">❤️</div>
<h3>Wishlist</h3>
<p style="font-size: 24px;">{wishlist.length}</p>
</div>
<div style="text-align: center; padding: 30px; background: #fff3cd; border-radius: 8px;">
<div style="font-size: 48px;">⭐</div>
<h3>Reviews</h3>
<p style="font-size: 24px;">3</p>
</div>
</div>
<h2>Recent Orders</h2>
<div style="margin-top: 20px;">
<div style="padding: 20px; border: 1px solid #eee; border-radius: 8px; margin: 10px 0;">
<div style="display: flex; justify-content: space-between;">
<div>
<strong>Order #12345</strong>
<p style="color: #666;">Placed on Jan 15, 2024</p>
</div>
<div>
<span style="background: #d4edda; color: #155724; padding: 5px 10px; border-radius: 20px;">Delivered</span>
</div>
</div>
<div style="margin-top: 10px;">3 items • Total: $157.99</div>
</div>
<div style="padding: 20px; border: 1px solid #eee; border-radius: 8px; margin: 10px 0;">
<div style="display: flex; justify-content: space-between;">
<div>
<strong>Order #12344</strong>
<p style="color: #666;">Placed on Jan 10, 2024</p>
</div>
<div>
<span style="background: #fff3cd; color: #856404; padding: 5px 10px; border-radius: 20px;">Shipped</span>
</div>
</div>
<div style="margin-top: 10px;">2 items • Total: $89.99</div>
</div>
</div>
</div>
</div>
<!-- Wishlist Page -->
<div s-route="/wishlist">
<div>
<h1>My Wishlist</h1>
<div s-if="wishlist.length === 0" style="text-align: center; padding: 60px;">
<p style="font-size: 18px; color: #666;">Your wishlist is empty</p>
<a href="#" s-link="/products" class="btn btn-primary" style="margin-top: 20px;">
Browse Products
</a>
</div>
<div s-else class="product-grid">
<div s-for="item in wishlist" s-key="item.id" class="product-card">
<a href="#" s-link="/products/{item.id}" style="text-decoration: none; color: inherit;">
<div class="product-image">{item.image}</div>
<div class="product-info">
<h3 class="product-title">{item.name}</h3>
<div class="product-price">${item.price}</div>
</div>
</a>
<div class="product-info" style="padding-top: 0;">
<div class="product-actions">
<button s-click="addToCart(item)" class="btn btn-primary">
Add to Cart
</button>
<button s-click="removeFromWishlist(item.id)" class="btn btn-danger">
Remove
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 404 Page -->
<div s-route="/:404">
<div style="text-align: center; padding: 100px;">
<h1 style="font-size: 72px; color: #dc3545;">404</h1>
<h2 style="margin: 20px 0;">Page Not Found</h2>
<p style="color: #666; margin-bottom: 30px;">The page you're looking for doesn't exist.</p>
<a href="#" s-link="/" class="btn btn-primary">Go Home</a>
</div>
</div>
</div>
</div>
<script type="module">
import { createApp } from 'https://cdn.jsdelivr.net/gh/Pappa1945-tech/simplijs@v3.2.1/dist/simplijs.min.js';
const app = createApp();
// Global methods
window.addToCart = (product) => {
const cart = app.state.cart;
const existing = cart.items.find(item => item.id === product.id);
if (existing) {
existing.quantity++;
} else {
cart.items.push({ ...product, quantity: 1 });
}
// Save to localStorage
localStorage.setItem('cart', JSON.stringify(cart.items));
// Show notification
alert(`${product.name} added to cart!`);
};
window.removeFromCart = (productId) => {
const cart = app.state.cart;
cart.items = cart.items.filter(item => item.id !== productId);
localStorage.setItem('cart', JSON.stringify(cart.items));
};
window.updateQuantity = (productId, newQuantity) => {
if (newQuantity < 1) return;
const cart = app.state.cart;
const item = cart.items.find(item => item.id === productId);
if (item) {
item.quantity = newQuantity;
localStorage.setItem('cart', JSON.stringify(cart.items));
}
};
window.clearCart = () => {
if (confirm('Are you sure you want to clear your cart?')) {
app.state.cart.items = [];
localStorage.removeItem('cart');
}
};
window.addToWishlist = (product) => {
const wishlist = app.state.wishlist;
if (!wishlist.find(item => item.id === product.id)) {
wishlist.push(product);
localStorage.setItem('wishlist', JSON.stringify(wishlist));
alert(`${product.name} added to wishlist!`);
}
};
window.removeFromWishlist = (productId) => {
app.state.wishlist = app.state.wishlist.filter(item => item.id !== productId);
localStorage.setItem('wishlist', JSON.stringify(app.state.wishlist));
};
window.login = () => {
const email = app.state.loginForm?.email;
const password = app.state.loginForm?.password;
// Simple demo authentication
if (email === 'demo@example.com' && password === 'password') {
const user = { name: 'Demo User', email, id: 1 };
app.state.auth.user = user;
app.state.auth.isAuthenticated = true;
localStorage.setItem('user', JSON.stringify(user));
window.location.hash = '/dashboard';
} else {
alert('Invalid credentials');
}
};
window.logout = () => {
app.state.auth.user = null;
app.state.auth.isAuthenticated = false;
localStorage.removeItem('user');
window.location.hash = '/';
};
window.register = () => {
const form = app.state.registerForm;
if (form.password !== form.confirmPassword) {
alert('Passwords do not match');
return;
}
const user = { name: form.name, email: form.email, id: Date.now() };
app.state.auth.user = user;
app.state.auth.isAuthenticated = true;
localStorage.setItem('user', JSON.stringify(user));
window.location.hash = '/dashboard';
};
window.placeOrder = () => {
alert('Order placed successfully! Thank you for shopping with SimpliShop!');
app.state.cart.items = [];
localStorage.removeItem('cart');
window.location.hash = '/dashboard';
};
// Initialize form state
app.state.loginForm = { email: 'demo@example.com', password: 'password' };
app.state.registerForm = { name: '', email: '', password: '', confirmPassword: '' };
app.state.checkoutInfo = {
fullName: '',
email: '',
address: '',
city: '',
state: '',
zip: '',
country: 'US',
payment: 'credit'
};
app.mount('[s-app]');
</script>
</body>
</html>
Chapter 16 Summary
You've now built a complete, production-ready e-commerce platform with SimpliJS:
Full-featured shopping cart with quantity management
Product catalog with categories, filters, and search
User authentication system with login/register
Checkout process with shipping and payment
User dashboard with order history
Wishlist functionality
Persistent state with localStorage
Responsive design with CSS Grid/Flexbox
Professional UI with cards, forms, and alerts
This project demonstrates how all the pieces of SimpliJS come together to create a real-world application. You've used:
Reactive state for products, cart, and user data
Routing for multi-page experience
Forms for user input
Event handling for interactivity
Conditional rendering for dynamic UI
LocalStorage for persistence
Computed values for totals and filters
The e-commerce platform is fully functional and can be extended with additional features like product reviews, order tracking, admin panel, and more.
This concludes our journey through building a real-world application with SimpliJS. You now have the skills to build sophisticated web applications with simplicity and elegance.
End of Chapter 16
Appendix: SimpliJS Complete Reference
Welcome to the SimpliJS Complete Reference. This appendix serves as a comprehensive guide to all directives, APIs, plugins, and best practices covered throughout the book. Use this as a quick reference when building your SimpliJS applications.
A.1 Directive Quick Reference
Core Directives
| Directive | Description | Example |
|---|---|---|
| s-app | Marks the boundary for SimpliJS application | <div s-app>...</div> |
| s-state | Initializes local reactive state | <div s-state="{ count: 0 }"> |
| s-global | Shared data across multiple app instances | <div s-global="{ theme: 'dark' }"> |
Data Binding
| Directive | Description | Example |
|---|---|---|
| s-bind | Two-way binding for inputs | <input s-bind="username"> |
| s-text | Reactive text content | <span s-text="message"></span> |
| {expression} | Interpolation in text | <h1>Hello, {user}!</h1> |
| s-html | Raw HTML injection | <div s-html="content"></div> |
| s-value | One-way value sync | <input s-value="initial"> |
| s-attr | Dynamic attribute binding | <img s-attr:src="imageUrl"> |
| s-class | Dynamic CSS classes | <div s-class="{ active: isActive }"> |
| s-style | Dynamic inline styles | <div s-style="{ color: themeColor }"> |
Control Flow
| Directive | Description | Example |
|---|---|---|
| s-if | Conditional rendering | <div s-if="isLoggedIn">Welcome</div> |
| s-else | Else condition | <div s-else>Login</div> |
| s-else-if | Else-if condition | <div s-else-if="role === 'admin'">Admin</div> |
| s-show | CSS visibility toggle | <div s-show="isVisible">Content</div> |
| s-hide | Inverse visibility | <div s-hide="isLoading">Content</div> |
| s-for | List rendering | <li s-for="item in items">{item}</li> |
| s-key | Unique key for list items | <li s-for="item in items" s-key="item.id"> |
| s-index | Loop index access | <li s-for="item, i in items">Index: {i} |
Events
| Directive | Description | Example |
|---|---|---|
| s-click | Click event handler | <button s-click="count++">Click</button> |
| s-dblclick | Double click | <div s-dblclick="handleDoubleClick()"> |
| s-mousedown | Mouse down | <button s-mousedown="startDrag()"> |
| s-mouseup | Mouse up | <button s-mouseup="endDrag()"> |
| s-mouseenter | Mouse enter | <div s-mouseenter="hover = true"> |
| s-mouseleave | Mouse leave | <div s-mouseleave="hover = false"> |
| s-mousemove | Mouse move | <div s-mousemove="trackPosition(event)"> |
| s-keydown | Key down | <input s-keydown="handleKey(event)"> |
| s-keyup | Key up | <input s-keyup="validateInput()"> |
| s-keypress | Key press | <input s-keypress="typeahead()"> |
| s-key:[key] | Specific key | <input s-key:enter="submit()"> |
| s-input | Input event | <input s-input="live = $event.target.value"> |
| s-change | Change event | <select s-change="updateCategory()"> |
| s-submit | Form submit | <form s-submit="save()"> |
| s-focus | Focus event | <input s-focus="logFocus()"> |
| s-blur | Blur event | <input s-blur="validate()"> |
| s-scroll | Scroll event | <div s-scroll="handleScroll()"> |
Form Handling
| Directive | Description | Example |
|---|---|---|
| s-model | Advanced form binding | <input type="checkbox" s-model="agree"> |
| s-validate | Built-in validation | <input s-bind="email" s-validate="required|email"> |
| s-error | Error message display | <span s-error="email"></span> |
Async & Data Fetching
| Directive | Description | Example |
|---|---|---|
| s-fetch | Automated JSON fetching | <div s-fetch="'/api/users'"> |
| s-loading | Loading state UI | <div s-loading>Loading...</div> |
| s-error | Error state UI | <div s-error>Failed to load</div> |
Components & Slots
| Directive | Description | Example |
|---|---|---|
| s-component | Mount registered component | <div s-component="'my-button'"></div> |
| s-prop | Pass props to component | <my-user s-prop:name="user.name"></my-user> |
| s-slot | Named slot content | <h1 s-slot="title">Hello</h1> |
Routing
| Directive | Description | Example |
|---|---|---|
| s-route | Define route template | <div s-route="/home">Home</div> |
| s-view | Router outlet | <main s-view></main> |
| s-link | Navigation link | <a s-link="/about">About</a> |
| s-link-active | Active link class | <a s-link="/" s-link-active="active">Home</a> |
| s-guard | Route guard | <div s-route="/admin" s-guard="isAdmin"> |
Performance
| Directive | Description | Example |
|---|---|---|
| s-lazy | Lazy load images | <img s-lazy="'/images/photo.jpg'"> |
| s-memo | Memoize DOM updates | <div s-memo="items.length"> |
| s-ref | DOM element reference | <input s-ref="myInput"> |
| s-once | Render once only | <div s-once>{timestamp}</div> |
| s-ignore | Skip subtree | <div s-ignore>Static content</div> |
Event Modifiers
| Modifier | Description | Example |
|---|---|---|
| .prevent | Prevent default | <a s-click.prevent="handler"> |
| .stop | Stop propagation | <button s-click.stop="handler"> |
| .once | Trigger once | <button s-click.once="handler"> |
| .self | Only trigger on self | <div s-click.self="handler"> |
| .capture | Use capture phase | <div s-click.capture="handler"> |
Key Modifiers
| Modifier | Description | Example |
|---|---|---|
| .enter | Enter key | <input s-key.enter="submit"> |
| .tab | Tab key | <input s-key.tab="nextField"> |
| .delete | Delete key | <input s-key.delete="clear"> |
| .esc | Escape key | <div s-key.esc="closeModal"> |
| .space | Spacebar | <div s-key.space="toggle"> |
| .up | Up arrow | <input s-key.up="increment"> |
| .down | Down arrow | <input s-key.down="decrement"> |
| .left | Left arrow | <div s-key.left="prevSlide"> |
| .right | Right arrow | <div s-key.right="nextSlide"> |
| .ctrl | Ctrl modifier | <div s-key.ctrl.s="save"> |
| .shift | Shift modifier | <div s-key.shift.a="selectAll"> |
| .alt | Alt modifier | <div s-key.alt.f4="close"> |
| .meta | Meta/Windows key | <div s-key.meta.k="search"> |
A.2 JavaScript API Reference
Core Functions
*// Create a SimpliJS application*
createApp(options?: AppOptions): App
*// Define a custom component*
component(name: string, factory: ComponentFactory): void
*// Create reactive state*
reactive<T extends object>(initial: T): T
*// Create computed property*
computed<T>(fn: () => T): { value: T }
*// Watch for changes*
watch<T>(source: () => T, callback: WatchCallback<T>, options?:
WatchOptions): void
*// Create DOM reference*
ref<T = HTMLElement>(): Ref<T>Component Lifecycle Hooks
{
*// Called before component is mounted*
beforeMount?: () => void;
*// Called after component is mounted*
onMount?: () => void;
*// Called before component updates*
beforeUpdate?: () => void;
*// Called after component updates*
onUpdate?: () => void;
*// Called before component is destroyed*
beforeDestroy?: () => void;
*// Called after component is destroyed*
onDestroy?: () => void;
*// Called when error occurs*
onError?: (error: Error) => void;
}Event Bus API
*// Emit an event*
emit(event: string, data?: any): void;
*// Listen to an event*
on(event: string, handler: (data: any) => void): () => void;
*// Listen once*
once(event: string, handler: (data: any) => void): () => void;
*// Remove listener*
off(event: string, handler?: (data: any) => void): void;SEO Helpers
*// Set SEO meta tags*
setSEO({
title?: string,
description?: string,
image?: string,
url?: string,
twitterHandle?: string
}): void;
*// Set theme color*
setThemeColor(color: string): void;
*// Set breadcrumbs*
setBreadcrumbs(crumbs: Array<{ name: string, url: string }>): void;
*// Set JSON-LD structured data*
setJsonLd(data: object): void;A.3 Plugin API Reference
@simplijs/auth
*// Create auth instance*
const auth = createAuth({
persist?: boolean, *// Persist to localStorage*
onLogin?: (user) => void, *// Login callback*
onLogout?: () => void, *// Logout callback*
onRedirect?: (path) => void *// Redirect callback*
});
*// Properties*
auth.state.user; *// Current user*
auth.state.isAuthenticated; *// Auth status*
auth.state.loading; *// Loading state*
auth.state.error; *// Error message*
*// Methods*
auth.login(credentials); *// Login user*
auth.logout(); *// Logout user*
auth.register(userData); *// Register user*
auth.checkSession(); *// Check existing session*@simplijs/vault-pro
*// Create vault instance*
const vault = createVault(initialState, {
persist?: boolean, *// Persist to localStorage*
maxHistory?: number *// Max history entries*
});
*// State access*
vault.state; *// Reactive state*
*// Time travel*
vault.vault.undo(); *// Undo last change*
vault.vault.redo(); *// Redo last undone*
vault.vault.canUndo; *// Check if undo available*
vault.vault.canRedo; *// Check if redo available*
*// Checkpoints*
vault.vault.checkpoint(name); *// Create named checkpoint*
vault.vault.restore(name); *// Restore checkpoint*
*// Sharing*
vault.vault.share(); *// Get shareable link*
vault.vault.history; *// Get history*@simplijs/router
*// Create router instance*
const router = createRouter(routes, {
mode?: \'hash\' \| \'history\', *// Router mode*
transition?: string, *// Transition name*
scrollBehavior?: function *// Scroll behavior*
});
*// Route definition*
const routes = {
\'/\': {
component: \'home-page\',
title: \'Home\',
guard?: () => boolean
},
\'/users/:id\': {
component: \'user-page\',
title: \'User Profile\'
},
\'/:404\': {
component: \'not-found\'
}
};
*// Properties*
router.state.currentRoute; *// Current route*
router.state.params; *// Route parameters*
router.state.query; *// Query parameters*
*// Methods*
router.navigate(path); *// Navigate to path*
router.link(path); *// Create navigation link*
router.back(); *// Go back*
router.forward(); *// Go forward*@simplijs/forms
*// Create form instance*
const form = createForm(initialData, {
validation?: object, *// Validation rules*
autoSave?: boolean, *// Auto-save to localStorage*
onSuccess?: (data) => void, *// Success callback*
onError?: (error) => void *// Error callback*
});
*// Properties*
form.state.data; *// Form data*
form.state.errors; *// Validation errors*
form.state.touched; *// Touched fields*
form.state.dirty; *// Dirty fields*
form.state.isValid; *// Form validity*
form.state.isSubmitting; *// Submitting state*
*// Methods*
form.handleChange(name, value); *// Handle field change*
form.handleBlur(name); *// Handle field blur*
form.handleSubmit(handler); *// Handle form submit*
form.reset(); *// Reset form*
form.validate(); *// Validate form*
*// Validation rules*
{
required: true, *// Field is required*
email: true, *// Valid email*
min: 8, *// Minimum length*
max: 20, *// Maximum length*
pattern: /regex/, *// Regex pattern*
validate: (value) => string\|null *// Custom validator*
}@simplijs/devtools
*// Initialize devtools*
initDevTools();
*// DevTools features (available in console)*
window.SimpliDevTools.components; *// Registered components*
window.SimpliDevTools.stateSnapshots; *// State history*
window.SimpliDevTools.takeSnapshot(); *// Take state snapshot*
window.SimpliDevTools.showPanel(); *// Show devtools panel*A.4 Common Patterns and Best Practices
State Management Patterns
*// 1. Local component state*
component(\'my-component\', () => {
const state = reactive({ count: 0 });
return { render: () => \`<div>\${state.count}</div>\` };
});
*// 2. Shared state with reactive()*
const store = reactive({
user: null,
settings: { theme: \'light\' }
});
*// 3. Computed properties*
const fullName = computed(() => \`\${firstName.value}
\${lastName.value}\`);
*// 4. Watchers for side effects*
watch(() => state.user, (newUser) => {
localStorage.setItem(\'user\', JSON.stringify(newUser));
});Component Composition Patterns
*// 1. Container/Presentational pattern*
component(\'user-container\', () => {
const users = reactive(\[\]);
const fetchUsers = async () => { */* \... */* };
return {
render: () => \`
<user-list users=\"\${JSON.stringify(users)}\"></user-list>
\`,
onMount: fetchUsers
};
});
*// 2. Higher-order component pattern*
function withAuth(WrappedComponent) {
component(\`auth-\${WrappedComponent}\`, () => {
const auth = useAuth();
return {
render: () => auth.isAuthenticated
? \`<\${WrappedComponent}></\${WrappedComponent}>\`
: \`<login-prompt></login-prompt>\`
};
});
}
*// 3. Render props pattern*
component(\'data-provider\', (element, props) => {
const data = reactive(\[\]);
return {
render: () => props.render(data)
};
});Performance Optimization Patterns
*// 1. Use s-once for static content*
<div s-once>{expensiveComputation()}</div>
*// 2. Use s-memo for expensive sections*
<div s-memo=\"items.length\">
{items.map(renderExpensiveItem)}
</div>
*// 3. Use s-lazy for images*
<img s-lazy=\"imageUrl\" loading=\"lazy\">
*// 4. Virtual scrolling for long lists*
<div s-for=\"item in visibleItems\" s-key=\"item.id\">
{item.content}
</div>
*// 5. Debounce expensive operations*
let timeout;
watch(() => state.search, () => {
clearTimeout(timeout);
timeout = setTimeout(() => search(), 300);
});Security Best Practices
*// 1. Always sanitize user input*
const sanitizeHTML = (str) => {
const div = document.createElement(\'div\');
div.textContent = str;
return div.innerHTML;
};
*// 2. Use s-html only with trusted content*
<div s-html=\"sanitizeHTML(userContent)\"></div>
*// 3. Escape interpolation in attributes*
<img s-attr:src=\"\`/images/\${encodeURIComponent(filename)}\`\">
*// 4. Implement route guards for protected routes*
<div s-route=\"/admin\" s-guard=\"isAdmin\">
Admin panel
</div>
*// 5. Use Content Security Policy*
<meta http-equiv=\"Content-Security-Policy\"
content=\"default-src \'self\'; script-src \'self\'
\'unsafe-inline\'\">A.5 Troubleshooting Guide
Common Issues and Solutions
| Issue | Possible Cause | Solution |
|---|---|---|
| Directives not working | Missing s-app | Add s-app to root element |
| State not updating | Direct mutation | Use reactive() or array methods |
| s-for not updating | Missing s-key | Add unique s-key |
| Component not rendering | Name without hyphen | Use hyphen in component names |
| Events not firing | Wrong event name | Check event spelling (s-click not onclick) |
| Props undefined | Wrong attribute name | Use kebab-case in HTML |
| Router not working | Missing s-view | Add <div s-view> |
| Styles not applying | CSS specificity | Check selector priority |
| Memory leaks | Missing cleanup | Use onDestroy for cleanup |
| Performance issues | Too many watchers | Use computed and memo |
Debugging Tips
*// 1. Log state changes*
watch(() => state, (newVal) => {
console.log(\'State changed:\', newVal);
}, { deep: true });
*// 2. Inspect component*
console.log(element.component);
*// 3. Use devtools*
initDevTools();
*// 4. Debug rendering*
console.log(\'Rendering:\', new Date());
*// 5. Track events*
window.addEventListener(\'click\', (e) => {
console.log(\'Event:\', e.type, e.target);
});A.6 Migration Guide
From HTML-First to Components
*// HTML-First approach*
<div s-app s-state=\"{ count: 0 }\">
<p>{count}</p>
<button s-click=\"count++\">+</button>
</div>
*// Component approach*
component(\'my-counter\', () => {
const state = reactive({ count: 0 });
return {
render: () => \`
<p>\${state.count}</p>
<button
onclick=\"this.closest(\'my-counter\').increment()\">+</button>
\`,
increment: () => state.count++
};
});From Vanilla JS to SimpliJS
*// Vanilla JS*
let count = 0;
const btn = document.getElementById(\'btn\');
const display = document.getElementById(\'display\');
btn.addEventListener(\'click\', () => {
count++;
display.textContent = count;
});
*// SimpliJS*
<div s-app s-state=\"{ count: 0 }\">
<span>{count}</span>
<button s-click=\"count++\">+</button>
</div>From Other Frameworks
React:
jsx
// React
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// SimpliJS
component('counter', () => {
const state = reactive({ count: 0 });
return {
render: () => `<button onclick="this.closest('counter').increment()">${state.count}</button>`,
increment: () => state.count++
};
});
Vue:
*<!-- Vue -->*
<template>
<button \@click=\"count++\">{{ count }}</button>
</template>
<script>
export default {
data() { return { count: 0 } }
}
</script>
*<!-- SimpliJS -->*
<counter></counter>
<script>
component(\'counter\', () => {
const state = reactive({ count: 0 });
return {
render: () => \`<button
onclick=\"this.closest(\'counter\').increment()\">\${state.count}</button>\`,
increment: () => state.count++
};
});
</script>A.7 Glossary
| Term | Definition |
|---|---|
| Reactivity | Automatic update of UI when data changes |
| Proxy | JavaScript object that intercepts operations |
| Directive | HTML attribute with s- prefix |
| Component | Reusable custom element |
| State | Data that changes over time |
| Prop | Property passed to a component |
| Slot | Content projection area |
| Lifecycle Hook | Function called at component stages |
| Two-way Binding | Automatic sync between UI and state |
| Computed Property | Cached derived value |
| Watcher | Function that reacts to changes |
| Ref | Direct DOM element reference |
| SSG | Static Site Generation |
| SPA | Single Page Application |
| CSR | Client-Side Rendering |
| SSR | Server-Side Rendering |
| CDN | Content Delivery Network |
| ESM | ECMAScript Module |
| JSON-LD | JSON Linked Data for SEO |
Conclusion: The Future of Web Development with SimpliJS
Congratulations! You've completed your journey from absolute beginner to SimpliJS expert. Throughout this book, you've learned:
The philosophy of the Anti-Build Movement and why simplicity matters
HTML-First development with declarative directives
Reactive state management with proxies and computed properties
Component architecture for reusable, encapsulated UI
Routing for single-page applications
Form handling with validation
Plugin ecosystem for professional features
Real-world projects like e-commerce platforms
The SimpliJS Advantage
As you've seen throughout this book, SimpliJS offers unique advantages:
Zero Configuration: Start coding immediately, no build tools needed
HTML-First: Intuitive for beginners, powerful for experts
Progressive Enhancement: Start simple, add complexity as needed
Native Browser Technologies: Built on standards, not frameworks
Tiny Footprint: <20KB, faster than any meta-framework
Production Ready: SSG, SEO, security features built-in
Plugin Ecosystem: Professional features when you need them
Your Journey Continues
You now have the skills to build anything with SimpliJS. Here are some next steps:
Build your own projects - Put your skills to practice
Contribute to SimpliJS - Join the community on GitHub
Create plugins - Extend SimpliJS for others
Share your knowledge - Blog, speak, or teach others
Stay updated - Follow SimpliJS news and updates
The Anti-Build Movement
You're now part of a growing movement of developers who believe that web development should be simple, accessible, and enjoyable. The Anti-Build Movement isn't about rejecting tools—it's about using the right tools for the job and remembering that the browser is incredibly powerful on its own.
As you build your next project, remember:
Start simple - Add complexity only when needed
Think declaratively - Describe what, not how
Leverage the platform - The browser is your friend
Build for users - Performance and UX matter
Share and collaborate - The community is here to help
Final Words
Thank you for joining this journey through SimpliJS. You've invested time and effort to learn a new way of thinking about web development—one that prioritizes simplicity, clarity, and developer happiness.
Remember the words of the Anti-Build Manifesto:
"We believe development should happen in the browser, not in a terminal full of build errors. Every minute spent configuring Vite, Webpack, or Babel is a minute lost on your product."
Go forth and build amazing things with SimpliJS. The web is your canvas, and now you have the perfect tool to paint on it.
Happy coding! 🚀
End of Book
Thank you for taking this journey through SimpliJS. It's been a pleasure guiding you from the very basics all the way to building a complete e-commerce platform. You've shown great dedication by working through all 16 chapters and the comprehensive appendix.
What You've Accomplished
Think about how far you've come:
From: "I don't know anything about JavaScript"
To: Building production-ready applications with reactive state, routing, authentication, and plugins
That's an incredible transformation!
Your SimpliJS Journey Checklist
✅ Understood the Anti-Build philosophy
✅ Mastered HTML-First directives
✅ Built reactive applications with s-state
✅ Created reusable components
✅ Implemented routing and navigation
✅ Handled forms and validation
✅ Added authentication and state persistence
✅ Built a complete e-commerce platform
✅ Learned the plugin ecosystem
Stay Connected
⭐ Star the SimpliJS GitHub repository
🐛 Report issues or suggest features
💬 Join community discussions
🤝 Contribute to the project
📢 Share what you build with #SimpliJS
One Last Tip
Remember this simple truth about web development:
"Simplicity is the ultimate sophistication." — Leonardo da Vinci
SimpliJS embodies this philosophy. As you continue building, always ask yourself: "Can this be simpler?" Often, the answer will lead you to better code.
If you ever need a refresher, this book will be here for you. The appendix, in particular, makes a great quick reference for your daily work.
Now go build something amazing! The web is waiting for what you'll create. 🚀
Happy coding, and thank you for being such an engaged learner!