I am trying to understand how to carry tracking transitivity across classes, as we are trying to transform a deep nest of computational classes using @computed and @alias to the @tracked model, so I wrote a little app to zero in on the fundamentals of moving tracked data around.
application.hbs:
<h1 id="title">Adventures with Ember Tracking</h1>
<div class="left-right">
<div>
<h2>Data input</h2>
<div class="in-rows">
<label for="tracked-value">Individual Tracked Value:
<input type="text" placeholder="Number" id="tracked-value" value={{this.trackedValue}} {{on 'change' this.valueChanged}}>
</label>
<label for="tracked-container">Value In Tracked Container:
<input type="text" placeholder="Number" id="tracked-container" value={{this.trackedContainer.value}} {{on 'change' this.containerValueChanged}}>
</label>
<h3>Bumps:</h3>
<button type="button" {{on 'click' this.bumpTrackedValue}}>Bump Tracked Value</button>
<button type="button" {{on 'click' this.bumpValueInTrackedContainer}}>Bump Value in Tracked Container</button>
<button type="button" {{on 'click' this.bumpTrackedValueInContainer}}>Bump Tracked Value In Container</button>
<h3>Resets:</h3>
<button type="button" {{on 'click' this.resetTrackedContainerFromValue}}>Reset Tracked Container Of Value From Tracked Value</button>
<button type="button" {{on 'click' this.resetTrackedContainerFromSum}}>Reset Tracked Container Of Value From Sum</button>
<button type="button" {{on 'click' this.resetTrackedValueInContainerFromValue}}>Reset Container Of Tracked Value From Tracked Value</button>
<button type="button" {{on 'click' this.resetTrackedValueInContainerFromSum}}>Set Container Of Tracked Value From Sum</button>
</div>
</div>
<div>
<h2>Data Output</h2>
<p>Individual Value: {{this.trackedValue}}</p>
<p>Value In Tracked Container: {{this.trackedContainer.value}}</p>
<p>Tracked Value In Container: {{this.trackedValueContainer.value}}</p>
<p>Sneaky Value In Tracked Container: {{this.stealthValue}}</p>
<p>Sum - getter - uses both: {{this.sum}}</p>
<p>Sum In Tracked Container: {{this.sumTrackedContainer.value}}</p>
<p>Tracked Sum In Container: {{this.trackedSumContainer.value}}</p>
</div>
</div>
application.js
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
class ValueContainer {
constructor(value) { this.value = value;}
value;
}
class TrackedValueContainer {
constructor(value) { this.value = value;}
@tracked value;
}
export default class ApplicationController extends Controller {
@tracked trackedValue = 0;
@tracked trackedContainer = new ValueContainer(0);
@tracked sumTrackedContainer = new ValueContainer(0);
trackedValueContainer = new TrackedValueContainer(this.trackedValue);
trackedSumContainer = new TrackedValueContainer(this.sum);
get sum() {
return this.trackedValue + this.trackedContainer.value;
}
get stealthValue() {
return this.trackedValue + this.trackedContainer.value - this.trackedValue;
}
@action
valueChanged(event) {
this.trackedValue = parseInt(event.target.value);
}
@action
containerValueChanged(event) {
this.trackedContainer = new ValueContainer(parseInt(event.target.value));
}
@action
bumpTrackedValue() {
this.trackedValue++;
}
@action
bumpValueInTrackedContainer() {
this.trackedContainer.value++;
}
@action
bumpTrackedValueInContainer() {
this.trackedValueContainer.value++;
}
@action
resetTrackedContainerFromValue() {
this.trackedContainer = new ValueContainer(this.trackedValue);
}
@action
resetTrackedContainerFromSum() {
this.sumTrackedContainer = new ValueContainer(this.sum);
}
@action
resetTrackedValueInContainerFromValue() {
this.trackedValueContainer = new TrackedValueContainer(this.trackedValue);
}
@action
resetTrackedValueInContainerFromSum() {
this.trackedSumContainer = new TrackedValueContainer(this.sum);
}
}
app.css (in case anybody cares)
.left-right {
display: flex;
flex-direction: row;
gap: 20px;
}
.in-rows {
display: flex;
flex-direction: column;
gap: 10px;
}
Some of this is no surprise. If my container is tracked but my value isn’t and I increment the value, the change happens but doesn’t show up in the browser. Neither does the viewed value of the getter that uses this value update.
- Click
Bump Value in Tracked Container
Nothing changes. - Click
Bump Tracked Value
. The values ofSum
andSneaky Value
both update with values showing indirectly that the value in the tracked container had been successfully.
This makes sense. The value isn’t tracked. Only its container is. Transitivity doesn’t extend inward. The kids don’t get into the theater for free on the parent’s ticket.
One thing that kind of makes sense, but only once I think of it, is that if I pass a constructor a tracked value or the value of an accessor that uses a tracked value, and the constructor puts it into a tracked value, tracking transitivity is broken across the construction. For instance, if I pass the sum
accessor value into a constructor that puts it into a tracked member value
, value
won’t track changes in sum
.
This also makes sense. Function parameters only carry the current value, not the place it came from. What I have done in the past is to pass the container of a tracked value, the place where it is being maintained, into a constructor and hold the reference to the container. As long as everybody is talking to the same tracked instance, everybody benefits from the tracking. You can set up a wrapper class to be used as needed for tracked values you need to pass along.
Things get dicier, though, if the thing you need to share is a getter, since you need to feed it with the things it needs, which may themselves be a long chain of getters (we may go ten deep) leading back eventually to tracked values. I think maybe we can get some leverage from careful use of closures? I don’t have this space mapped out, so this may be my next stab at research.
The third thing which really took me by surprise was that, if I tracked a value in an untracked container and then the container was re-initialized (fresh new), the UI stopped tracking the value.
- Click
Bump Tracked Value In Container
a few times and watch the Tracked Value In Container number go up. - Click
Reset Container Of Tracked Value From Tracked Value
. - Click
Bump Tracked Value In Container
again. The numbers no longer go up.
Transitivity was broken. This means that transitivity apparently doesn’t extend outward either. The reference from the HBS to the original path (this.trackedValueContainer.value
) doesn’t wake up and refresh when this.trackedValueContainer is changed. Mom or Dad doesn’t get into the theater for free on the kids’ tickets either.
To do that, I suppose the container and the value would both need to be tracked. So what I’m learning here is that changes to what isn’t explicitly tracked won’t trigger repaints, even if they are parents or children of things that are explicitly tracked. Nobody gets carried along with the tide (although I think there’s an addon or two to make it happen for the kids.)
Does all of this sound like expected stuff? Or did I mess up in my implementation somewhere, leading me to make wrong conclusions? Please advise…