This content originally appeared on DEV Community and was authored by Ahmad Wasfi
Explore TargetJS on GitHub.
Introduction
Frameworks often promise simplicity, but frequently require extensive boilerplate and libraries as they inherit the same software approach rooted in early programming models and force it to fit building user interfaces by adding more complexity. User interfaces are dynamic and asynchronous and require a different paradigm.
TargetJS adopts a new approach. First, it unifies class methods and fields into a single construct called targets. Each target is given state, lifecycles, timing, iterations, and the autonomy to execute mimicking the behavior of living cells. Targets are essentially self-contained, intelligent blocks of code.
The second challenge is making these targets to fit and work together especially since UI operations are highly asynchronous. Instead of relying on traditional method calls and callbacks that don’t address asynchronous nature well, TargetJS allows targets to react to the execution or completion of preceding targets. A subsequent target can run independently, execute whenever the previous one does, or wait until the previous target completes. Targets stack together like snapping Lego pieces. It can address complex asynchronous workflow yet easy to understand.
For example, setting a value can implicitly define an animation, where the current value iteratively progresses until it reaches the new value. When the animation completes, the next target might initiate a fetch API call. Once the data is received, it can trigger another target that creates 10 nodes, each with its own animation and API call. A subsequent target can then be set to run only after all nodes have completed their tasks. Throughout this sequence, no direct method calls are made. Targets simply react and chain together based on how the code is written.
Targets unlock a fundamentally new way of coding that simplifies everything from animation, UI updates, API calls, and state management. It also makes the code significantly more compact.
TargetJS Syntax
To enable a target to react to a preceding one, we use postfixes appended directly to the target name. We could also have implemented targets as objects with special properties specifying their dependencies and reaction timing. However, since one of our goals was to keep the syntax compact and to enforce that targets only react to preceding ones, we adopted a postfix-based approach. Specifically, we use $ and $$ appended to target names to indicate reactive behavior.
$
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.
Examples
To demonstrate the power and simplicity of TargetJS, we’ll walk through a four examples: We’ll start with a simple box animation, scale it up to 10 boxes, extend it to infinite scrolling that generates an unlimited number of boxes, and finally add an API call to populate the visible boxes with details.
1. Growing and Shrinking Box
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;
}
});
Explanation
Before we explain how the program works, it’s worth mentioning that the entire UI and its behavior are defined directly within a single JavaScript file. There is no separate HTML or CSS.
As noted in the introduction, targets execute in the order they are defined. Thus, they run in the following sequence:
-
background
: This target runs first, setting the element’s background color tomediumpurple
. Once the assignment is complete, its lifecycle ends. -
width
: Next, thewidth
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. -
height$
: Finally, theheight$
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. Aswidth
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>
2. Creating 10 Growing and Shrinking Boxes
import { App } from 'targetj';
App({
width: 100,
children() {
return Array.from({length: 10}, (_, i) => ({
background: 'mediumpurple',
width: [{ list: [100, 250, 100] }, 50, 10],
height$() {
return this.prevTargetValue / 2;
},
bottomMargin: 2
}));
}
});
In this example, we create 10 instances of the box from the previous example.
Explanation
The children
target is a special type that appends a new node or multiple nodes if the value is an array. In our example, it adds 10 nodes to the parent, and its lifecycle ends.
3. Infinite Scrolling
import { App, getEvents } from 'targetj';
App({
width: 100,
height: 300,
domHolder: true,
preventDefault: true,
children() {
return Array.from({length: 10}, (_, i) => ({
background: 'mediumpurple',
width: [{ list: [100, 250, 100] }, 50, 10],
height$() {
return this.prevTargetValue / 2;
},
bottomMargin: 2
}));
},
onScroll() {
this.setTarget("scrollTop", Math.max(0, this.getScrollTop() + getEvents().deltaY()));
},
onVisibleChildrenChange() {
return !this.visibleChildren.length || this.getLastChild().getY() < this.getHeight() ? 'children' : undefined;
}
});
In this example, we expand on the previous one to enable infinite scrolling by adding 10 nodes at a time to fill in the gaps as the user scrolls.
Explanation
domHolder
is a special target that indicates the dom
of its children should exist within itself. In the previous example, the dom
existed at the root level. TargetJS makes the dom
containment dynamic and encourages a flat tree structure whenever possible.
onScroll
is another special target that gets triggered every time the user scrolls.
onVisibleChildrenChange
is a special target that gets triggered when the visibility of the children changes. It returns the children
target to activate it, adding another 10 nodes.
The above example could be also written as:
import { App, getEvents } from 'targetj';
App({
width: 100,
height: 300,
domHolder: true,
preventDefault: true,
onVisibleChildrenChange() {},
children$() {
if (!this.visibleChildren.length || this.getLastChild().getY() < this.getHeight()) {
return Array.from({length: 10}, (_, i) => ({
background: 'mediumpurple',
width: [{ list: [100, 250, 100] }, 50, 10],
height$() {
return this.prevTargetValue / 2;
},
bottomMargin: 2
}));
}
},
onScroll() {
this.setTarget("scrollTop", Math.max(0, this.getScrollTop() + getEvents().deltaY()));
}
});
In this version, the children
target reacts each time onVisibleChildrenChange
executes, as it has the $
postfix indicating that it should run whenever the preceding target does.
4. Building Infinite Scrolling with Live Data from APIs
import { App, getEvents } from 'targetj';
App({
width: 100,
height: 300,
domHolder: true,
preventDefault: true,
onVisibleChildrenChange() {},
children$() {
if (!this.visibleChildren.length || this.getLastChild().getY() < this.getHeight()) {
return Array.from({ length: 10 }, (_, i) => ({
background: 'mediumpurple',
width: [{ list: [100, 250, 100] }, 50, 10],
height$() {
return this.prevTargetValue / 2;
},
bottomMargin: 2
}));
}
},
loadItems$$() {
this.visibleChildren.filter(child => !child.loaded).forEach(child => {
child.loaded = true;
fetch(this, `https://targetjs.io/api/randomUser?id=${child.oid}`);
});
},
populate$() {
this.prevTargetValue?.forEach(data =>
this.getChildByOid(data.id).setTarget("html", data.name)
);
},
onScroll() {
this.setTarget("scrollTop", Math.max(0, this.getScrollTop() + getEvents().deltaY()));
}
});
In this final example, we enable loading the details of each box by making an API call.
- The
loadItems
target is responsible for making the API calls. Since it ends with$$
, it is only triggered after the animation of all children has completed. - The
populate
target handles populating the received data. It only runs once all API results have been received.
Alternatively, populate
can be implemented to react to each individual API result:
populate$() {
const data = this.prevTargetValue;
if (data) {
this.getChildByOid(data.id).setTarget("html", data.name);
}
}
In this version, populate
is executed every time an individual API result is received.
It is worth to mention that TargetJS maintains the order of API results to match the sequence of API calls. For example, if the second box’s API result arrives before the first, populate$
will still wait until the first result is received to preserve order.
Finally, if you want to embed this infinite scrolling component directly into your page without external JavaScript, you can use:
<div
tg-width="100"
tg-height="300"
tg-domHolder="true"
tg-preventDefault="true"
tg-onVisibleChildrenChange=""
tg-children$="function() {
if (!this.visibleChildren.length || this.getLastChild().getY() < this.getHeight()) {
return Array.from({ length: 10 }, (_, i) => ({
background: 'mediumpurple',
width: [{ list: [100, 250, 100] }, 50, 10],
height$() {
return this.prevTargetValue / 2;
},
bottomMargin: 2
}));
}
}"
tg-load$$="function() {
this.visibleChildren.filter(child => !child.loaded).forEach(child => {
child.loaded = true;
TargetJS.fetch(this, `https://targetjs.io/api/randomUser?id=${child.oid}`);
});
}"
tg-populate$$="function() {
if (this.prevTargetValue) {
this.prevTargetValue.forEach(data => this.getChildByOid(data.id).setTarget('html', data.name));
}
}"
tg-onScroll="function() {
this.setTarget('scrollTop', Math.max(0, this.getScrollTop() + TargetJS.getEvents().deltaY()));
}"
></div>
Ready to see to learn more?
Visit: GitHub Repo
Site: targetjs.io
This content originally appeared on DEV Community and was authored by Ahmad Wasfi