DEV Community

mogera551
mogera551

Posted on

Structive vs. Vue: A Radar Chart Code Comparison

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

Structive

<label>{{ stats.*.label }}</label>
<span>{{ stats.*.value }}</span>
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

Structive

<script type="module">
export default class {
  newLabel = '';
  stats = [...];
}
</script>
Enter fullscreen mode Exit fullscreen mode

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

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(" ")
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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!");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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