What is Structive?
Structive is a framework built on single-file Web Components that offers a structure-driven template syntax designed to eliminate as much boilerplate and state-hook overhead as possible, while still providing a fully declarative UI and reactive state management.
Learn more:
– https://github.com/mogera551/Structive
– https://github.com/mogera551/Structive-Example
Entry Point
Your entry point is an HTML file, and you’ll need to use an import map to alias your modules:
<script type="importmap">
{
"imports": {
"structive": "path/to/cdn/structive.js",
"main": "./components/main.st.html"
}
}
</script>
<app-main></app-main>
<script type="module">
import { config, defineComponents } from "structive";
config.enableMainWrapper = false;
config.enableShadowDom = false;
config.enableRouter = false;
defineComponents({ "app-main": "main" });
</script>
Script
Register your components and tweak framework settings inside a <script type="module">
block.
Component Registration & Root Tag
Use defineComponents
to map your custom element tag names to their .st.html
files. Then include the root component tag (here <app-main>
) in your <body>
.
Configuration Options
Currently Structive does not ship with a built-in router or wrapper, so set:
config.enableMainWrapper = false;
config.enableRouter = false;
config.enableShadowDom = false; // or true if you prefer Shadow DOM
Roadmap
Routing and automatic component-loading (“autoload”) support are coming soon.
Components
Each component lives in its own single file, with three sections:
<!-- 1. UI Template -->
<template>
</template>
<!-- 2. CSS -->
<style>
</style>
<!-- 3. State Class -->
<script type="module">
</script>
We’ll focus on the UI template and the state class.
Structural Paths
Both your template and your state class use structural paths to bind data. Paths use dot notation and support a wildcard *
to represent array elements:
-
Full path:
user.profile.name
-
Wildcard path:
products.*.name
In the template, *
refers to the current item inside a for
block. In your state class, you can also declare derived state getters using wildcards.
UI Template
Your <template>
can include:
-
for
blocks for iteration -
if
blocks for conditional rendering - Interpolation for embedding values
- Attribute binding for linking state to DOM properties, classes, attributes, and events
You can also chain filters onto paths (e.g. |locale
to format numbers).
<template>
<ul>
{{ for:products }}
<li>{{ products.*.name }} — {{ products.*.price|locale }}</li>
{{ endfor: }}
</ul>
{{ if:user.isLoggedIn }}
Welcome, {{ user.profile.nickName }}!
{{ else: }}
Please log in.
{{ endif: }}
Enter your name:
<input type="text" data-bind="value:user.profile.name">
<button data-bind="onclick.popup">Click me</button>
</template>
for
Block
Start with {{ for:LIST_PATH }}
, end with {{ endfor: }}
. The path must resolve to an array. You don’t declare your own loop variable—inside, use structural paths. An implicit index variable $1
is provided; deeper loops get $2
, $3
, etc.
{{ for:users }}
{{ users.*.profile.name }}
{{ endfor: }}
{{ for:makers }}
<div>
No: {{ $1|inc,1 }} Maker: {{ makers.*.name }}
</div>
{{ for:makers.*.products }}
<div>
Product No: {{ $2|inc,1 }} —
{{ makers.*.products.name }}
({{ makers.*.products.*.price|locale }})
</div>
{{ endfor: }}
{{ endfor: }}
if
Block
Use {{ if:CONDITION_PATH }}
… {{ endif: }}
, with optional {{ else: }}
. The path must return a boolean. You may apply filters, but not raw expressions.
{{ if:user.isLoggedIn }}
Hello, {{ user.profile.nickName }}!
{{ else: }}
Please log in.
{{ endif: }}
<!-- Raw JS expressions are not allowed -->
{{ if:user.age > 18 }} <!-- ❌ -->
{{ if:user.age|gt,18 }} <!-- ✅ -->
Interpolation
Embed state values directly in text with {{ PATH }}
. Filters can be chained.
{{ user.profile.nickName }}
{{ user.profile.nickName|uc }} <!-- upper-case -->
{{ for:states }}
<div>
{{ states.*.name }}, {{ states.*.population|locale }}
</div>
{{ endfor: }}
Attribute Binding
Link state paths to DOM element properties, classes, attributes, and events—all via data-bind
. Supports two-way binding for inputs.
Property Binding
Certain DOM props are auto two-way: value
, checked
, etc.
<input type="text" data-bind="value:user.profile.name">
{{ for:products }}
<div>
{{ products.*.name }}
<input type="text" data-bind="value:products.*.inventory">
</div>
{{ endfor: }}
Conditional Class Binding
Toggle a CSS class based on a boolean path.
.adult { color: red; }
<div data-bind="class.adult:user.isAdult">
<!-- adds “adult” class when user.isAdult === true -->
Event Binding
Map element events to methods on your state class. Use on
-prefixed method names for clarity.
<button data-bind="onclick:onAdd">Add</button>
Custom Attribute Binding
Use attr.
prefix when you need to set arbitrary HTML/SVG attributes.
<polygon data-bind="attr.points:points"></polygon>
State Class
Your state lives in a default-exported JS class. Define all your state as class properties.
<script type="module">
export default class {
fruits = [
{ name: "apple" },
{ name: "banana" },
{ name: "cherry" }
];
count = 0;
user = {
profile: {
name: "Alice",
age: 30
}
};
}
</script>
Event Handling
Define methods on your class to handle events. Prefix them with on
to distinguish them from utility methods.
{{ count }}
<button data-bind="onclick:onIncrement">Increment</button>
export default class {
count = 0;
onIncrement() {
this.count++;
}
}
Inside a for
loop, handlers receive the index as a second argument:
{{ for:users }}
{{ users.*.name }}
<button data-bind="onclick:onClick">Click</button>
{{ endfor: }}
export default class {
users = [
{ name: "Alice" },
{ name: "Bob" },
{ name: "Charlie" }
];
onClick(e, $1) {
alert("Clicked index = " + $1);
}
}
You can also update the looped item’s state by using a wildcard path:
{{ for:users }}
<div>
{{ users.*.name }}
<button data-bind="onclick:onToggle">Select</button>
</div>
{{ endfor: }}
export default class {
users = [
{ name: "Alice", selected: false },
{ name: "Bob", selected: false },
{ name: "Charlie", selected: false }
];
onToggle(e, $1) {
this["users.*.selected"] = !this["users.*.selected"];
}
}
Update Triggers
Any assignment to a class property via a structural path automatically re-renders the bound DOM. For arrays, use immutable methods (concat
, toSpliced
, etc.) rather than push
/pop
.
{{ count }}
<button data-bind="onclick:onIncrement">Increment</button>
{{ for:users }}
<div>
{{ users.*.name }}
<button data-bind="onclick:onDelete">Delete</button>
</div>
{{ endfor: }}
export default class {
count = 0;
onIncrement() {
this.count += 1;
}
users = [
{ name: "Alice", selected: false },
{ name: "Bob", selected: false },
{ name: "Charlie", selected: false }
];
onDelete(e, $1) {
this.users = this.users.toSpliced($1, 1);
}
}
Derived State Creation
Use getters named with structural paths to compute derived values. Whenever their dependencies change, the UI updates automatically via dependency tracking.
{{ user.profile.name }} <!-- e.g. “Alice” -->
{{ user.profile.ucName }}<!-- “ALICE” -->
<input type="text" data-bind="value:user.profile.name">
export default class {
user = {
profile: {
name: "Alice",
age: 30
}
};
get "user.profile.ucName"() {
return this["user.profile.name"].toUpperCase();
}
}
Derived Structural Paths with Wildcards
You can also create a wildcard-based derived state, as if each array element had a virtual property:
{{ for:users }}
{{ users.*.ucName }},
<input type="text" data-bind="value:users.*.name">
{{ endfor: }}
export default class {
users = [
{ name: "Alice", selected: false },
{ name: "Bob", selected: false },
{ name: "Charlie", selected: false }
];
get "users.*.ucName"() {
return this["users.*.name"].toUpperCase();
}
}
Summary of Development
- Use the same structural paths in your state class and UI template
- Perform all updates via structural paths
- Create derived state with getters named as paths
- Inside loops, use implicit indices (
$1
,$2
, …)
Finally
Any feedback or messages would be greatly appreciated!