DEV Community

mogera551
mogera551

Posted on

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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: }}
Enter fullscreen mode Exit fullscreen mode

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 }}     <!-- ✅ -->
Enter fullscreen mode Exit fullscreen mode

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: }}
Enter fullscreen mode Exit fullscreen mode

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: }}
Enter fullscreen mode Exit fullscreen mode

Conditional Class Binding

Toggle a CSS class based on a boolean path.

.adult { color: red; }
Enter fullscreen mode Exit fullscreen mode
<div data-bind="class.adult:user.isAdult">
  <!-- adds “adult” class when user.isAdult === true -->
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Custom Attribute Binding

Use attr. prefix when you need to set arbitrary HTML/SVG attributes.

<polygon data-bind="attr.points:points"></polygon>
Enter fullscreen mode Exit fullscreen mode

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>
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 }}
<button data-bind="onclick:onIncrement">Increment</button>
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 }}
  <button data-bind="onclick:onClick">Click</button>
{{ 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 }}
  <div>
    {{ users.*.name }}
    <button data-bind="onclick:onToggle">Select</button>
  </div>
{{ 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 }}
<button data-bind="onclick:onIncrement">Increment</button>

{{ for:users }}
  <div>
    {{ users.*.name }}
    <button data-bind="onclick:onDelete">Delete</button>
  </div>
{{ 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 }}  <!-- e.g. “Alice” -->
{{ user.profile.ucName }}<!-- “ALICE” -->
<input 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 }},
  <input 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!

Top comments (0)