Skip to content

Instantly share code, notes, and snippets.

@sukima
Last active August 8, 2020 15:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save sukima/3b1a42ece3f2c7d5f67af807933ae1df to your computer and use it in GitHub Desktop.
Save sukima/3b1a42ece3f2c7d5f67af807933ae1df to your computer and use it in GitHub Desktop.
tracking polyfill autotrack
import Controller from '@ember/controller';
import EmberObject, { action, computed, notifyPropertyChange } from '@ember/object';
let currentAutoTrackings = [];
function trackedData(key, initializer) {
let values = new WeakMap();
let autoTrackingData = new WeakMap();
let hasInitializer = typeof initializer === 'function';
function notify(target, property) {
console.log(`notify: ${property}`);
notifyPropertyChange(target, property);
}
function consumeTracking(self) {
for (let [currentTarget, currentProperty] of currentAutoTrackings) {
let data = autoTrackingData.get(self) ?? new Map();
let trackedProperties = data.get(currentTarget) ?? new Set();
trackedProperties.add(currentProperty);
data.set(currentTarget, trackedProperties);
autoTrackingData.set(self, data);
}
}
function notifyAutoTracked(self) {
if (!autoTrackingData.has(self)) { return; }
for (let [target, properties] of autoTrackingData.get(self)) {
for (let property of properties) {
notify(target, property);
}
}
}
function cleanupAutoTracked(self) {
autoTrackingData.get(self)?.clear();
}
function getter(self) {
let value;
consumeTracking(self);
if (hasInitializer && !values.has(self)) {
value = initializer.call(self);
values.set(self, value);
} else {
value = values.get(self);
}
return value;
}
function compatGetter(self) {
consumeTracking(self);
return initializer.call(self);
}
function setter(self, value) {
notify(self, key);
notifyAutoTracked(self);
cleanupAutoTracked(self);
values.set(self, value);
}
return { compatGetter, getter, setter };
}
function tracked(target, key, descriptor) {
let { getter, setter } = trackedData(key, descriptor?.initializer);
return {
enumerable: true,
configurable: true,
get() {
return getter(this);
},
set(newValue) {
setter(this, newValue);
},
};
}
function beginAutoTracking(self, key) {
currentAutoTrackings.unshift([self, key]);
}
function endAutoTracking() {
currentAutoTrackings.shift();
}
function autotrack(target, key, descriptor) {
let getter = descriptor.get;
return {
enumberable: true,
configurable: true,
get() {
beginAutoTracking(this, key);
try {
return getter.call(this);
} finally {
endAutoTracking(this, key);
}
},
};
}
function dependentKeyCompat(target, key, descriptor) {
let { compatGetter } = trackedData(key, descriptor.get);
return autotrack(target, key, {
...descriptor,
enumerable: true,
configurable: true,
get() {
return compatGetter(this);
},
});
}
EmberObject.reopen({
init() {
this._super(...arguments);
for (let el of Object.getOwnPropertyNames(this.constructor.prototype)) {
let desc = Object.getOwnPropertyDescriptor(this.constructor.prototype, el);
if (desc.get?.name.startsWith('get ')) {
Object.defineProperty(this, el, autotrack(this, el, desc))
}
}
}
});
export default class ApplicationController extends Controller {
@tracked appName = 'FOOBAR';
@dependentKeyCompat
get fancy() {
console.log('compute fancy');
return `${this.appName}-autotrack-getter`;
}
@computed('appName')
get computedFancy() {
console.log('compute computedFancy');
return `${this.appName}-computed-dependency`;
}
get computedFancyCombined() {
console.log('compute computedFancyCombined');
return `${this.computedDependTest}-2`;
}
@computed('fancy')
get computedDependTest() {
console.log('compute computedDependTest');
return `${this.fancy}-1`;
}
@action
testIt() {
this.appName = this.appName.startsWith('F') ? 'BARFOO' : 'FOOBAR';
}
}
:root {
--base-bg-color: white;
--base-fg-color: black;
--primary-color: lightskyblue;
--secondary-color: lavender;
--dimmed-color: gainsboro;
--info-color: lightblue;
--success-color: palegreen;
--warning-color: orange;
--danger-color: orangered;
}
body {
font-family: 'Georgia', serif;
font-size: 14pt;
background-color: var(--base-bg-color);
color: var(--base-fg-color);
}
.container {
max-width: 60ch;
}
.btn {
font-size: 1rem;
background-color: var(--dimmed-color);
border: solid thin currentColor;
box-shadow: 1px 1px 2px #666;
padding-block: 0.5rem;
padding-inline: 0.5rem;
border-radius: 6px;
}
.btn:active {
box-shadow: none;
transform: translate(1px, 1px);
}
.grid-list {
display: grid;
grid-template-columns: max-content 1fr;
grid-gap: 1rem 0.5rem;
place-items: center start;
}
.grid-list dt {
max-width: 18ch;
}
.grid-list dd {
margin: 0;
}
.debug-output {
border: solid thin currentColor;
padding-block: 0.5rem;
padding-inline: 0.5rem;
border-radius: 4px;
}
.info {
background-color: var(--info-color);
}
.primary {
background-color: var(--primary-color);
}
.secondary {
background-color: var(--secondary-color);
}
.success {
background-color: var(--success-color);
}
.warning {
background-color: var(--warning-color);
}
.danger {
background-color: var(--danger-color);
}
<div class="[ container ]">
<button
type="button"
class="[ main-control ] [ btn ] [ primary ]"
{{on "click" this.testIt}}
>
<code>this.appName = 'BARFOO'</code>
</button>
<dl class="[ debug-output ] [ grid-list ] [ info ]">
<dt>Direct access</dt>
<dd><samp>{{this.appName}}</samp></dd>
<dt>Autotracked getter</dt>
<dd><samp>{{this.fancy}}</samp></dd>
<dt>Computed with dependancy</dt>
<dd><samp>{{this.computedFancy}}</samp></dd>
<dt>Computed through autotracked getter</dt>
<dd><samp>{{this.computedFancyCombined}}</samp></dd>
</dl>
</div>
{
"version": "0.17.1",
"EmberENV": {
"FEATURES": {},
"_TEMPLATE_ONLY_GLIMMER_COMPONENTS": false,
"_APPLICATION_TEMPLATE_WRAPPER": true,
"_JQUERY_INTEGRATION": true
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.js",
"ember": "3.12.2",
"ember-template-compiler": "3.12.2",
"ember-testing": "3.12.2"
},
"addons": {
"@glimmer/component": "1.0.0"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment