What is Structive?
Structive is a buildless framework that adopts single-file-based web components. While it may seem simple at first glance, it features structure-driven templates and simplified state management, enabling you to effortlessly build declarative and reactive UIs.
What is Structure-Driven?
It's a style for building applications using structural paths for UI, state, derived state, state updates, and parent-child communication. A structural path indicates the position of data within a hierarchical structure, using a wildcard *
for list elements.
For example, with data like users = [ { name:"Alice" }, { name:"Bob" } ]
, the structural path for the name
property of an element in the users
list would be users.*.name
.
https://github.com/mogera551/Structive
https://github.com/mogera551/Structive-Example
Vue's Radar Chart Example
Vue has a famous example: a radar chart.
You can adjust the values of each axis on the radar chart with sliders, and add or remove axes. The chart updates in real-time as values change, which is quite impactful. We'll implement the same functionality in Structive and compare the code to highlight Structive's unique characteristics. The Structive code we'll discuss is identical to what you can find here.
Let's Compare
File Structure
Both frameworks use single-file components. Vue's example is split into three components (main part, radar chart part, axis title part). Structive, on the other hand, handles state derivation for list elements easily, so it's all contained within a single file.
Template Overview
Both are HTML-based with a declarative syntax.
Vue's Case
The chart and label sections are componentized.
Structive's Case
It's written as a single component without further componentization. It also uses the same structural paths as the state for for
blocks, embedding, and attribute binding, aiming for a structure that minimizes logic in the template.
Differences in data paths in the UI:
Vue
<label>{{stat.label}}</label>
<span>{{stat.value}}</span>
Structive
<label>{{ stats.*.label }}</label>
<span>{{ stats.*.value }}</span>
for
Loop
Vue's Case
It uses the v-for
directive. Within the loop, data is accessed using a scoped variable. The axis title is sub-componentized and looped. Also, event handlers within the loop specify the list element as an argument.
Structive's Case
It uses a for
block. For list elements within the loop, it specifies a structural path with a wildcard (*
) instead of a scoped variable. Additionally, for event handlers within the loop, it only specifies the method name of the state class that performs the state update.
Vue
<axis-label
v-for="(stat, index) in stats"
:stat="stat"
:index="index"
:total="stats.length"
>
</axis-label>
<div v-for="stat in stats">
<label>{{stat.label}}</label>
<input type="range" v-model="stat.value" min="0" max="100">
<span>{{stat.value}}</span>
<button @click="remove(stat)" class="remove">X</button>
</div>
Structive
{{ for:stats }}
<text data-bind="
attr.x:stats.*.labelPoint.x;
attr.y:stats.*.labelPoint.y;
">{{ stats.*.label }}</text>
{{ endfor: }}
{{ for:stats }}
<div>
<label>{{ stats.*.label }}</label>
<input type="range" data-bind="value|number:stats.*.value" min="0" max="100">
<span>{{ stats.*.value }}</span>
<button data-bind="onclick:onRemove" class="remove">X</button>
</div>
{{ endfor: }}
Attribute Binding and Event Handlers
Let's examine how DOM attributes are bound.
Vue's Case
Binding uses :property-name
. For two-way binding, v-model
is used. Event handlers are prefixed with @
and can specify arguments. Similar binding is possible for SVG tags.
Structive's Case
All attribute bindings are written within the data-bind
attribute. The format is property-name:state-name
. For input tags, it automatically becomes two-way. You can specify a type conversion filter for input (data-bind="value|number:stats.*.value"
). Event handlers are also written in the data-bind
attribute. You can use the @preventDefault
modifier to omit calling e.preventDefault()
within the event handler. When binding to an attribute within a tag, you specify attr.attribute-name:state-name
. SVG tags can be bound similarly to HTML tags. Structural paths are specified for state names.
Vue
<polygon :points="points"></polygon>
<text :x="point.x" :y="point.y">{{stat.label}}</text>
<input type="range" v-model="stat.value" min="0" max="100">
<button @click="remove(stat)" class="remove">X</button>
<input name="newlabel" v-model="newLabel">
<button @click="add">Add a Stat</button>
Structive
<polygon data-bind="attr.points:points"></polygon>
<text data-bind="
attr.x:stats.*.labelPoint.x;
attr.y:stats.*.labelPoint.y;
">{{ stats.*.label }}</text>
<input type="range" data-bind="value|number:stats.*.value" min="0" max="100">
<button data-bind="onclick:onRemove" class="remove">X</button>
<input name="newlabel" data-bind="value:newLabel">
<button data-bind="onclick:onAdd@preventDefault">Add a Stat</button>
State Management Overview
Vue's Case
State is defined using variables within the <script setup>
in three separate SFC (Single File Component) files. In the parent component, reactive state is declared using ref
and reactive
. In child components, defineProps
is used to define the state to be used. Additionally, computed
is used to automatically calculate coordinates based on changes in the number of elements. Event handlers call functions specified in the UI. The arguments passed to these functions are the variables specified in the UI.
Structive's Case
State is defined using a class within the <script type='module'>
in a single SFC file. State is declared as properties, and derived state using getters achieves a similar effect to Vue's computed
, but it's characterized by defining them using structural paths (e.g., stats.*.point
). Event handlers that update the state are defined as methods. The arguments passed to these methods include the Event
object and, if called from within a loop, the loop index. preventDefault()
can be omitted by specifying the @preventDefault
data attribute modifier in the UI. Framework-provided APIs like $getAll
can be called via this
.
State Declaration
Vue's Case
Variables are declared using ref
and reactive
within the <script setup>
tag. In child components, state is defined using defineProps
.
Structive's Case
A state class is defined and export
ed within the <script type="module">
tag. State is declared as properties of the class. No ref
or reactive
is specifically needed.
Vue
<script setup>
const newLabel = ref('')
const stats = reactive([...]);
</script>
<script setup>
const props = defineProps({
stat: Object,
index: Number,
total: Number
})
</script>
<script setup>
const props = defineProps({
stats: Array
})
</script>
Structive
<script type="module">
export default class {
newLabel = '';
stats = [...];
}
</script>
State Derivation
Vue's Case
computed
is used to define automatically calculated properties. Recalculations for points
(polygon vertex coordinates) and point
(label display coordinates) are performed based on changes in stats
.
Structive's Case
JavaScript getters are used to define virtual properties for state derivation. These include the polygon vertex coordinate property stats.*.point
, the label display coordinate property stats.*.labelPoint
, the points
property for setting the <polygon/>
tag's points
attribute, and the stats.json
property for JSON representation of stats
.
Structive automatically tracks dependencies for state referenced by virtual properties. If a referenced state is updated, it's automatically recalculated. For points
, you can easily get all elements of stats.*.point
as an array by calling the $getAll
API. For stats.json
, updates to stats.*.value
are not automatically tracked, so you register the dependency using the $trackDependency
API. Within the definition of a virtual property in a loop context (using a wildcard structural path), you can reference the implicit index variable ($1
).
Structive's Virtual Properties
A key feature of virtual properties is the ability to define properties virtually for structural paths at any hierarchy level. Within the definition, you can reference structural paths as calculation targets, and by tracking the referenced structural paths as dependencies, automatic calculations are performed. These can then be used in the UI just like regular structural paths. Because it can handle abstract structural paths using the wildcard *
, the code's declarativeness is significantly enhanced.
Vue
// Calculates the polygon's coordinate array and formats it for the polygon tag's points attribute
const points = computed(() => {
const total = props.stats.length
return props.stats
.map((stat, i) => {
const { x, y } = valueToPoint(stat.value, i, total)
return `${x},${y}`
})
.join(' ')
})
// Calculates label coordinates
const point = computed(() =>
valueToPoint(+props.stat.value + 10, props.index, props.total)
)
Structive
export default class {
// JSON display
get "stats.json"() {
this.$trackDependency("stats.*.value"); // Track stats.*.value
return JSON.stringify(this.stats);
}
// Define label coordinates
get "stats.*.labelPoint"() {
// $1 is the index variable
return valueToPoint(100 + 10, this.$1, this.stats.length);
}
// Define polygon vertex coordinates
get "stats.*.point"() {
// $1 is the index variable
// Can reference path "stats.*.value", which becomes a dependency to track
return valueToPoint(this["stats.*.value"], this.$1, this.stats.length);
}
// Join and format the array of coordinates for the polygon tag's points attribute
get points() {
// Get all elements of path "stats.*.point" using $getAll
// Path "stats.*.point" is a tracked dependency
const points = this.$getAll("stats.*.point", []);
// Format for points attribute
return points.map(p => `${p.x},${p.y}`).join(" ")
}
}
State Updates
Let's look at the state update process for adding and deleting elements.
Vue's Case
Event handlers for state updates are defined as functions. You can use variables specified in the UI as arguments. Mutable array operations like push
and splice
are used.
Structive's Case
Event handlers for state updates are defined as methods of the state class. For event handlers within a loop, the loop index ($1
) is passed automatically as an argument, so you don't need to pass the index variable from the UI. Within the process, new arrays are created using concat
or toSpliced
and then assigned to the state property. Assignment to a state property triggers the update. preventDefault
is unnecessary because it's specified as a modifier (@preventDefault
) in the UI's attribute binding.
Vue
function add(e) {
e.preventDefault()
if (!newLabel.value) return
stats.push({
label: newLabel.value,
value: 100
})
newLabel.value = ''
}
function remove(stat) {
if (stats.length > 3) {
stats.splice(stats.indexOf(stat), 1)
} else {
alert("Can't delete more!")
}
}
Structive
export default class {
onAdd(e) {
if (!this.newLabel) return;
this.stats = this.stats.concat({ label: this.newLabel, value: 100});
this.newLabel = '';
}
onRemove(e, $1) {
if (this.stats.length > 3) {
this.stats = this.stats.toSpliced($1, 1);
} else {
alert("Can't delete more!");
}
}
}
Summary
By comparing the two, Structive's characteristics should now be clear.
Here are Structive's key features:
- In the UI, structural paths are specified everywhere.
- It uses
for
blocks. -
Attribute binding is defined in the
data-bind
attribute. -
Within
for
blocks, an asterisk*
is used in structural paths. - For events, only the handler's method name is specified.
- State management is done using classes.
- State is handled with properties, derived state with getters, and state updates with methods.
- Structural paths can be used within getters and methods.
- The index is automatically passed as an argument to methods within loops, which is convenient.
- State derivation getters can be used as virtual properties and can be specified for structural paths at any hierarchy level.
- It has less boilerplate code and no state hooks.
Finally
I would greatly appreciate any feedback or messages.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more