Specifications of Structive: A Framework That Makes State Management a Breeze

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!

Leave a Reply