DEV Community

Cover image for TargetJS: Code-Ordered Reactivity and Targets - A New Paradigm for UI Development
Ahmad Wasfi for TargetJS

Posted on • Edited on

TargetJS: Code-Ordered Reactivity and Targets - A New Paradigm for UI Development

Introduction

Reactive methods, where one method runs automatically when another completes, whether synchronous or asynchronous, is a powerful idea. TargetJS introduces a distinctly innovative approach to this concept: it enables methods to react exclusively to their immediately preceding counterparts, fostering a declarative and simple code flow.

TargetJS also brings in a second key concept: it unifies both variables and methods into a new construct called “Targets”. Targets also provide state, loops, timing, and more, whether it's a variable or a function.

When these two ideas are combined: code-ordered reactivity and Targets, they unlock a fundamentally new way of coding that simplifies everything from animations and UI updates to API calls and state management. The result is code that is not only more intuitive to write but also significantly more compact.

Understanding TargetJS Syntax: Reactive Postfixes

TargetJS uses the postfixes $ and $$ appended to target names for defining reactive behaviors. While initially appearing a bit cryptic, this convention provides a compact syntax.

$ Postfix (Immediate Reactivity):

A target name ending with a single $ (e.g., height$) indicates that this target will execute every time its immediately preceding target runs or emits a new value. If the preceding target involves an asynchronous operation like an API call, the reactive target activates when the response is received. If there are multiple API calls made, $ postfix ensures that the target reacts to the first API result when it becomes available, then the second, and so on, maintaining a strict, code-ordered sequence of operations.

$$ Postfix (Full Completion Reactivity):

A target name ending with a double $$ (e.g., fetch$$) will activate only after its immediately preceding targets have fully and comprehensively completed all of their operations. This includes:

  • The successful resolution of any timed sequences, such as animations.
  • The completion and return of results from all associated API calls.
  • The finalization of all tasks, animations, and API calls initiated by any dependent child targets that were themselves triggered by a preceding target.

Targets: The Building Blocks of TargetJS

Targets provide a unified interface for both variables and methods. Each Target comes equipped with a built-in set of capabilities:

  1. State Management: Targets are inherently stateful, enabling implicit state handling across your application.
  2. Iterations: They can iterate towards defined values, making them perfect for creating animations.
  3. Multiple or Conditional Execution: Targets can execute repeatedly or only under specific conditions.
  4. Execution timing: Targets enable fine-grained control over when they execute.
  5. Code-Ordered Execution: Targets execute sequentially and predictably in the order they are written within a JavaScript object, thanks to ES2015's guaranteed property order.

An All-in-One Solution

The elegance of TargetJS emerges when its two innovations: Reactivity and Targets, work in concert. The combination allows TargetJS to serve as a consistent, all-in-one solution for most front-end development needs:

  • Declarative UI Rendering: Define your user interface using Targets, and let TargetJS automatically manage rendering updates.
  • Animations: Leverage a target's built-in iteration, looping, and timing capabilities alongside reactivity to create complex animations with minimal effort.
  • API Integration: Execute API calls as targets and utilize their reactivity features to automatically process responses.
  • State Management: With targets inherently managing their own state, application state management becomes implicit.
  • Event Handling: Attach event listeners directly as targets to work consistently with the rest of the application

Examples in Action

To demonstrate the power and simplicity of TargetJS, let's explore its concepts through practical examples. We'll begin with a simple animation and incrementally expand it to demonstrate API integration, event handling, and dynamic UI updates.

Growing and Shrinking Box: Declarative Animation

import { App } from 'targetj';

App({
    background: 'mediumpurple',
    width: [{ list: [100, 250, 100] }, 50, 10], //  width animates through 100 → 250 → 100, over 50 steps with 10ms intervals.
    height$() { // `$` creates a reactive target: the `height` updates each time `width` executes
      return this.prevTargetValue / 2;
    } 
});
Enter fullscreen mode Exit fullscreen mode

Explanation

Targets execute precisely in the order they are defined:

  1. background: This target runs first, setting the element's background color to mediumpurple. Once the assignment is complete, its lifecycle ends.
  2. width: Next, the width target takes over. It's configured to animate through a list of values (100, 250, 100), performing 50 steps with a 10ms pause between each step, creating a grow-then-shrink effect.
  3. height$: Finally, the height$ target demonstrates TargetJS's reactivity. Because its name ends with a single $ postfix, height$ is explicitly declared to react whenever its immediately preceding target (width) executes on every step. As width animates and changes its value, height$ automatically re-runs, setting its value to half of width's value.

The example above can also be implemented directly in HTML, utilizing tg- attributes that mirror the object literal keys used in JavaScript:

<div
   tg-background="mediumpurple"
   tg-width="[{ list: [100, 250, 100] }, 50, 10]"
   tg-height$="return this.prevTargetValue / 2;">
</div>
Enter fullscreen mode Exit fullscreen mode

Adding an API Call

Let's extend our previous example to demonstrate how TargetJS handles asynchronous operations. We'll fetch user details from an API, but we also want this API call to initiate only after the box animation has fully completed.

import { App } from 'targetj';

App({
  background: 'mediumpurple',
  width: [{ list: [100, 250, 100] }, 50, 10],
  height$() {
    return this.prevTargetValue / 2;
  },
  fetch$$: 'https://targetjs.io/api/randomUser?id=user0', // `$$` ensures this runs only after width and height animation is complete
  html$() { // it runs when the API response is resolved
    return this.prevTargetValue.name; // `prevTargetValue` holds the result from the previous target (i.e., the API response)
  }
});
Enter fullscreen mode Exit fullscreen mode

Explanation

This example introduces two new targets:

  1. fetch$$: fetch target is a specialized target designed to retrieve data when given a URL string. Since its name ends with a $$ postfix, as previously discussed, this indicates that fetch$$ will activate only after preceding targets fully completed all of its operations. This guarantees the API call is initiated just after the animation finishes.

  2. html$: Following fetch$$, the html target is responsible for setting the text content of the div element to the fetched user's name. The $ postfix here signifies that html$ is a reactive target that executes each time its immediately preceding target (fetch$$) provides a result. In this context, this.prevTargetValue will hold the resolved data from the API call, allowing html$ to dynamically display the user's name as soon as it's available.

Together, these targets orchestrate the flow: animation completes, then the API call happens, then the UI updates with the fetched data, all managed declaratively and in code order.

Attaching a Click Handler

Let's expand our box further by adding a click handler. The goal is to change the box's background color to orange when clicked, pause for two seconds, and then revert the background back to its original mediumpurple.

import { App } from 'targetj';

App({
  background: 'mediumpurple',
  width: [{ list: [100, 250, 100] }, 50, 10],
  height$() {
    return this.prevTargetValue / 2;
  },
  fetch$$: 'https://targetjs.io/api/randomUser?id=user0',
  html$() {
    return this.prevTargetValue.name;
  },
  onClick() { // Special target that runs when the element is clicked
    this.setTarget('background', 'orange', 30, 10); // Animates background to orange over 30 steps
  },
  pause$$: { interval: 2000 }, // `$$` ensures this runs ONLY after the preceding 'onClick' animation is fully complete
  purpleAgain$$() { // `$$` ensures this runs ONLY after `pause$$` completes (2-second interval)
    this.setTarget('background', 'mediumpurple', 30, 10); // Animates background back to mediumpurple
  }
});
Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. onClick: This is a special TargetJS function that automatically runs whenever the associated element is clicked. Inside, this.setTarget('background', 'orange', 30, 10) imperatively triggers a new animation, changing the background color to orange over 30 steps.
  2. pause$$: Notice the $$ postfix. This pause$$ target is configured with an interval of 2000 milliseconds (2 seconds). Crucially, its $$ postfix means it will only begin its 2-second pause after its immediately preceding target (onClick) has fully completed its animation of changing the background to orange.
  3. purpleAgain$$: Also ending with $$, this target executes only after the pause$$ target has finished its 2-second execution. It then uses this.setTarget again to animate the background color back to mediumpurple.

This sequence demonstrates how TargetJS allows you to define complex, timed flow with a guaranteeing execution order.

Let's Make it More Complicated :)

Let’s expand the previous example by creating 10 boxes instead of just one. Each box will be added with a slight delay and undergo its own task from the previous example. Once all these individual box processes (creation, animation, and API calls) are complete, we'll trigger a final collective action: changing all their backgrounds to green.

import { App } from 'targetj';

App({
  children: { // A special target that generates a new list of child objects each time it executes
    cycles: 9, // Creates 10 children (from cycle 0 to 9)
    interval: 100, // Adds a new child every 100 milliseconds
    value(cycle) { 
      return {
        background: 'mediumpurple',
        width: [{ list: [100, 250, 100] }, 50, 10],
        height$() { return this.prevTargetValue / 2; },
        fetch$$: `https://targetjs.io/api/randomUser?id=user${cycle}`, // Each child fetches a unique user
        html$() { return this.prevTargetValue.name; },
        onClick() { this.setTarget('background', 'orange', 30, 10); },
        pause$$: { interval: 2000 },
        purpleAgain$$() { this.setTarget('background', 'mediumpurple', 30, 10); }
      };
    }
  },
  greenify$$() { // `$$` ensures this runs ONLY after ALL children have completed ALL their tasks
    this.getChildren().forEach(child => child.setTarget("background", "green", 30, 10)); // Iterates and animates each child's background
  }
});
Enter fullscreen mode Exit fullscreen mode

Explanation:

This advanced example demonstrates TargetJS's capability to manage complex, dynamic UI scenarios:

  1. children: A special target construct used to create a collection of child objects. Each time it executes, the returned object or objects are added to the current one. The cycles property specifies that the value function will run 10 times (from cycle 0 to 9), thus creating 10 individual box elements.
    The interval property ensures that each new box is created and added to the UI every 100 milliseconds.
    The value(cycle) function return the same object element from the previous example.

  2. greenify$$: This target demonstrates the $$ postfix's full completion reactivity at a higher level. The greenify$$ target will execute only after the entire children target has comprehensively completed all of its work. This includes:

  • The creation of all 10 child boxes.
  • The completion of each individual child's width animation.
  • The successful return of all 10 API calls (fetch) for each child.
  • The population of all user names (html) for each child.

Only when all tasks initiated by children are finished will greenify$$ runs. It then uses this.getChildren().forEach(child => child.setTarget("background", "green", 30, 10)) to iterate over all the created child boxes and animate their backgrounds to green.

What if greenify ends with only one $?

If greenify ended with only one $ (i.e., greenify$), it would exhibit immediate reactivity to its preceding target children. Instead of waiting for all 10 children to be fully animated, etc, greenify$ would run immediately each time a new child is added.

This example highlights TargetJS's ability to orchestrate complex, dependent operations across multiple elements in a compact, declarative, and code-ordered fashion.

Ready to learn more?

🔗 Visit: GitHub Repo

Top comments (0)