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:
<span class="na">type="importmap">
{
"imports": {
"structive": "path/to/cdn/structive.js",
"main": "./components/main.st.html"
}
}
<span class="na">type="module">
import { config, defineComponents } from "structive";
config.enableMainWrapper = false;
config.enableShadowDom = false;
config.enableRouter = false;
defineComponents({ "app-main": "main" });
Script
Register your components and tweak framework settings inside a block.
Component Registration & Root Tag
Use Currently Structive does not ship with a built-in router or wrapper, so set: Routing and automatic component-loading (“autoload”) support are coming soon. Each component lives in its own single file, with three sections: We’ll focus on the UI template and the state class. Both your template and your state class use structural paths to bind data. Paths use dot notation and support a wildcard In the template, Your You can also chain filters onto paths (e.g. Start with Use Embed state values directly in text with Link state paths to DOM element properties, classes, attributes, and events—all via Certain DOM props are auto two-way: Toggle a CSS class based on a boolean path.defineComponents
to map your custom element tag names to their .st.html
files. Then include the root component tag (here
) in your .
Configuration Options
config.enableMainWrapper = false;
config.enableRouter = false;
config.enableShadowDom = false; // or true if you prefer Shadow DOM
Roadmap
Components
<span class="na">type="module">
Structural Paths
*
to represent array elements:
user.profile.name
products.*.name
*
refers to the current item inside a for
block. In your state class, you can also declare derived state getters using wildcards.
UI Template
can include:
for
blocks for iterationif
blocks for conditional rendering|locale
to format numbers).
{{ for:products }}
{{ products.*.name }} — {{ products.*.price|locale }}
{{ endfor: }}
{{ if:user.isLoggedIn }}
Welcome, {{ user.profile.nickName }}!
{{ else: }}
Please log in.
{{ endif: }}
Enter your name:
type="text" data-bind="value:user.profile.name">
data-bind="onclick.popup">Click me
for
Block
{{ 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 }}
No: {{ $1|inc,1 }} Maker: {{ makers.*.name }}
{{ for:makers.*.products }}
Product No: {{ $2|inc,1 }} —
{{ makers.*.products.name }}
({{ makers.*.products.*.price|locale }})
{{ endfor: }}
{{ endfor: }}
if
Block
{{ 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: }}
{{ if:user.age > 18 }}
{{ if:user.age|gt,18 }}
Interpolation
{{ PATH }}
. Filters can be chained.
{{ user.profile.nickName }}
{{ user.profile.nickName|uc }}
{{ for:states }}
{{ states.*.name }}, {{ states.*.population|locale }}
{{ endfor: }}
Attribute Binding
data-bind
. Supports two-way binding for inputs.
Property Binding
value
, checked
, etc.
type="text" data-bind="value:user.profile.name">
{{ for:products }}
{{ products.*.name }}
type="text" data-bind="value:products.*.inventory">
{{ endfor: }}
Conditional Class Binding
.adult { color: red; }
data-bind="class.adult:user.isAdult">
Enter fullscreen mode
Exit fullscreen mode
Event Binding
Map element events to methods on your state class. Use on-prefixed method names for clarity.
data-bind="onclick:onAdd">Add
Enter fullscreen mode
Exit fullscreen mode
Custom Attribute Binding
Use attr. prefix when you need to set arbitrary HTML/SVG attributes.
data-bind="attr.points:points">
Enter fullscreen mode
Exit fullscreen mode
State Class
Your state lives in a default-exported JS class. Define all your state as class properties.
<span class="na">type="module">
export default class {
fruits = [
{ name: "apple" },
{ name: "banana" },
{ name: "cherry" }
];
count = 0;
user = {
profile: {
name: "Alice",
age: 30
}
};
}
Enter fullscreen mode
Exit fullscreen mode
Event Handling
Define methods on your class to handle events. Prefix them with on to distinguish them from utility methods.
{{ count }}
data-bind="onclick:onIncrement">Increment
Enter fullscreen mode
Exit fullscreen mode
export default class {
count = 0;
onIncrement() {
this.count++;
}
}
Enter fullscreen mode
Exit fullscreen mode
Inside a for loop, handlers receive the index as a second argument:
{{ for:users }}
{{ users.*.name }}
data-bind="onclick:onClick">Click
{{ endfor: }}
Enter fullscreen mode
Exit fullscreen mode
export default class {
users = [
{ name: "Alice" },
{ name: "Bob" },
{ name: "Charlie" }
];
onClick(e, $1) {
alert("Clicked index = " + $1);
}
}
Enter fullscreen mode
Exit fullscreen mode
You can also update the looped item’s state by using a wildcard path:
{{ for:users }}
{{ users.*.name }}
data-bind="onclick:onToggle">Select
{{ endfor: }}
Enter fullscreen mode
Exit fullscreen mode
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"];
}
}
Enter fullscreen mode
Exit fullscreen mode
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 }}
data-bind="onclick:onIncrement">Increment
{{ for:users }}
{{ users.*.name }}
data-bind="onclick:onDelete">Delete
{{ endfor: }}
Enter fullscreen mode
Exit fullscreen mode
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);
}
}
Enter fullscreen mode
Exit fullscreen mode
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 }}
{{ user.profile.ucName }}
type="text" data-bind="value:user.profile.name">
Enter fullscreen mode
Exit fullscreen mode
export default class {
user = {
profile: {
name: "Alice",
age: 30
}
};
get "user.profile.ucName"() {
return this["user.profile.name"].toUpperCase();
}
}
Enter fullscreen mode
Exit fullscreen mode
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 }},
type="text" data-bind="value:users.*.name">
{{ endfor: }}
Enter fullscreen mode
Exit fullscreen mode
export default class {
users = [
{ name: "Alice", selected: false },
{ name: "Bob", selected: false },
{ name: "Charlie", selected: false }
];
get "users.*.ucName"() {
return this["users.*.name"].toUpperCase();
}
}
Enter fullscreen mode
Exit fullscreen mode
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!