Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a tracked-based environment impl #60

Merged
merged 5 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ember-exclaim/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"dependencies": {
"@embroider/addon-shim": "^1.8.7",
"botanist": "^1.3.0",
"decorator-transforms": "^1.0.1"
"decorator-transforms": "^1.0.1",
"tracked-built-ins": "^3.3.0"
},
"devDependencies": {
"@babel/core": "^7.23.6",
Expand Down
6 changes: 1 addition & 5 deletions ember-exclaim/src/-private/env/computed.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ import { triggerChange } from './index.js';

// This module contains implementations of key operations
// (namely `extend` and `bind`) for environments whose
// reactivity model is based on Ember computeds. The idea
// is that we should be able to make a similar (but simpler)
// one that just uses native getters and setters to instead
// work more cleanly with `@tracked` data and have `ExclaimUi`
// decide which version to pass to `makeEnv` based on an arg.
// reactivity model is based on Ember computeds.

/**
* Returns wrapper around the given environment object that
Expand Down
21 changes: 11 additions & 10 deletions ember-exclaim/src/-private/env/index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
const EnvInternals = Symbol.for('env-internals');
// This fun bit of line noise is a workaround for
// https://github.com/embroider-build/ember-auto-import/issues/503#issuecomment-1064405138
const envInternals = (globalThis[Symbol.for('exclaim-env-internals')] ??=
new WeakMap());

export function makeEnv(data, onChange, { bind, extend }) {
return setInternals(data, { onChange, bind, extend });
}

export function isEnv(data) {
return EnvInternals in data;
return envInternals.has(data);
}

export function extendEnv(env, newBindings) {
const internals = env[EnvInternals];
const internals = envInternals.get(env);
const newEnv = internals.extend(env, newBindings);
const onChange = (key) => {
// We only want to propagate `onChange` if it's to a key in the
Expand All @@ -24,19 +27,17 @@ export function extendEnv(env, newBindings) {
}

export function bindData(config, env) {
return env[EnvInternals].bind(config, env);
return envInternals.get(env).bind(config, env);
}

export function triggerChange(env, key) {
env[EnvInternals].onChange?.(key);
envInternals.get(env).onChange?.(key);
}

function setInternals(env, internals) {
Object.defineProperty(env, EnvInternals, {
enumerable: false,
configurable: false,
get: () => internals,
});
if (!isEnv(env)) {
envInternals.set(env, internals);
}

return env;
}
88 changes: 88 additions & 0 deletions ember-exclaim/src/-private/env/tracked.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { TrackedObject } from 'tracked-built-ins';
import { HelperSpec, Binding } from '../ui-spec.js';
import { recordCanonicalPath } from '../paths.js';
import { triggerChange } from './index.js';

// This module contains implementations of key operations
// (namely `extend` and `bind`) for environments whose
// reactivity model is based on native getters and setters
// and therefore works cleanly with `@tracked` fields in
// environments.

/**
* Returns wrapper around the given environment object that
* will essentially behave exactly the same unless one of
* the added/overridden fields is accessed instead.
*/
export function extend(env, extraFields) {
const storage = new TrackedObject(extraFields);
return new Proxy(env, {
get(target, key) {
return Reflect.get(key in storage ? storage : target, key);
},
set(target, key, value) {
return Reflect.set(key in storage ? storage : target, key, value);
},
});
}

/**
* Given a piece of a UI spec `data` and an environment `env`,
* locates all `Binding` and `HelperSpec` values and installs
* appropriate getters and setters in their place.
*
* Note that this does not recurse through `ComponentSpec` values,
* as the embedded config within those should not be bound until
* the component spec is yielded and we know what environment to
* bind it to.
*/
export function bind(data, env) {
if (Array.isArray(data)) {
let result = Array(data.length);
for (let i = 0; i < data.length; i++) {
bindKey(result, i, data[i], env);
}
return result;
} else if (
typeof data === 'object' &&
data &&
Object.getPrototypeOf(data) === Object.prototype
) {
let result = {};
for (let key of Object.keys(data)) {
bindKey(result, key, data[key], env);
}
return result;
} else {
return data;
}
}

function bindKey(host, key, value, env) {
if (value instanceof Binding) {
const bindingPath = value.path.join('.');

recordCanonicalPath(host, key, env, bindingPath);
Object.defineProperty(host, key, {
enumerable: true,
get() {
return value.path.reduce((object, key) => object[key], env);
},
set(fieldValue) {
const parentPath = value.path.slice(0, -1);
const parent = parentPath.reduce((object, key) => object[key], env);
parent[value.path.at(-1)] = fieldValue;
triggerChange(env, bindingPath);
},
});
} else if (value instanceof HelperSpec) {
Object.defineProperty(host, key, {
enumerable: true,
get() {
return value.invoke(env);
},
});
} else {
host[key] = bind(value, env);
}
}
7 changes: 5 additions & 2 deletions ember-exclaim/src/components/exclaim-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import Component from '@ember/component';
import buildSpecProcessor from '../-private/build-spec-processor';
import { makeEnv } from '../-private/env/index.js';
import * as computedEnv from '../-private/env/computed.js';
import * as trackedEnv from '../-private/env/tracked.js';

export default Component.extend({
ui: null,
env: null,
implementationMap: null,
useClassicReactivity: false,

baseEnv: computed('env', 'onChange', function () {
return makeEnv(this.env ?? {}, this.onChange, computedEnv);
baseEnv: computed('env', 'onChange', 'useClassicReactivity', function () {
const envImpl = this.useClassicReactivity ? computedEnv : trackedEnv;
return makeEnv(this.env ?? {}, this.onChange, envImpl);
}),

content: computed('specProcessor', 'ui', function () {
Expand Down
1 change: 1 addition & 0 deletions playground-app/app/example/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
@ui={{json (or this.ui "{}")}}
@env={{json (or this.env "{}")}}
@implementationMap={{this.implementationMap}}
@useClassicReactivity={{true}}
local-class="result card"
as |error|
>
Expand Down
1 change: 1 addition & 0 deletions playground-app/app/index/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
env=(json this.envString)
implementationMap=this.implementationMap
wrapper=(component 'sample-wrapper')
useClassicReactivity=true
as |error|
}}
Error:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { htmlSafe } from '@ember/template';
import { setupRenderingTest } from 'ember-qunit';
import { resolveEnvPath } from 'ember-exclaim';

module('Integration | environment', function (hooks) {
module('Integration | environment | computed', function (hooks) {
setupRenderingTest(hooks);

function exclaimTest(name, { ui, env, implementationMap }) {
Expand Down Expand Up @@ -54,6 +54,7 @@ module('Integration | environment', function (hooks) {
@ui={{this.ui}}
@env={{this.env}}
@implementationMap={{this.implementationMap}}
@useClassicReactivity={{true}}
/>
`);

Expand Down
Loading
Loading