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

Don't require get/unwrap when working with config and objects in the environment #58

Merged
merged 10 commits into from
Mar 14, 2024
109 changes: 109 additions & 0 deletions ember-exclaim/src/-private/bind-computeds.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/* eslint-disable ember/no-computed-properties-in-native-classes */
import { get, set, defineProperty, computed } from '@ember/object';
import { deprecate } from '@ember/debug';
import { alias } from '@ember/object/computed';
import { HelperSpec, Binding } from './ui-spec.js';
import { recordCanonicalPath } from './paths.js';

/**
* Given a piece of a UI spec `data` and an environment `env`,
* locates all `Binding` and `HelperSpec` values and installs
* Ember computed properties with appropriate dependencies
* 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 bindComputeds(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 = new ConfigObject();
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) {
recordCanonicalPath(host, key, env, value.path.join('.'));
defineProperty(
host,
key,
alias(`${getEnvKey(host, env)}.${value.path.join('.')}`)
);
} else if (value instanceof HelperSpec) {
const envKey = getEnvKey(host, env);
const dependentKeys = value.bindings.map(
(binding) => `${envKey}.${binding.path.join('.')}`
);
defineProperty(
host,
key,
computed(...dependentKeys, { get: () => value.invoke(env) })
);
} else {
host[key] = bindComputeds(value, env);
}
}

const envKeys = new WeakMap();
function getEnvKey(object, environment) {
const key = envKeys.get(object);
if (key) {
return key;
}

const envKey = `-environment-${Math.random().toString().slice(2)}`;
Object.defineProperty(object, envKey, {
value: environment,
enumerable: false,
writable: false,
});
envKeys.set(object, envKey);
return envKey;
}

class ConfigObject {
get(key) {
deprecate(
'Calling `.get()` on UI config objects is deprecated. Use normal direct property access.',
true,
{
id: 'ember-exclaim.get-set',
for: 'ember-exclaim',
since: { available: '2.0.0', enabled: '2.0.0' },
until: '3.0.0',
}
);
return get(this, key);
}

set(key, value) {
deprecate(
'Directly calling `.set()` on UI config objects is deprecated. Use the importable `set` or set via a parent object.',
true,
{
id: 'ember-exclaim.get-set',
for: 'ember-exclaim',
since: { available: '2.0.0', enabled: '2.0.0' },
until: '3.0.0',
}
);
return set(this, key, value);
}
}
5 changes: 0 additions & 5 deletions ember-exclaim/src/-private/binding.js

This file was deleted.

4 changes: 1 addition & 3 deletions ember-exclaim/src/-private/build-spec-processor.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import Binding from './binding';
import ComponentSpec from './component-spec';
import HelperSpec from './helper-spec';
import { transform, rule, simple, subtree, rest } from 'botanist';
import { ComponentSpec, HelperSpec, Binding } from './ui-spec.js';

const hasOwnProperty = Function.prototype.call.bind(
Object.prototype.hasOwnProperty
Expand Down
13 changes: 0 additions & 13 deletions ember-exclaim/src/-private/component-spec.js

This file was deleted.

105 changes: 24 additions & 81 deletions ember-exclaim/src/-private/environment.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
/* eslint-disable ember/no-computed-properties-in-native-classes */
import { makeArray } from '@ember/array';
import { set, get } from '@ember/object';
import { isHTMLSafe } from '@ember/template';
import createEnvComputed from './environment/create-env-computed';
import EnvironmentData from './environment/data';
import EnvironmentArray from './environment/array';
import Binding from './binding';
import { extractKey } from './environment/utils';
import { set, get, computed, defineProperty } from '@ember/object';
import { resolveCanonicalPath } from './paths';
import { bindComputeds } from './bind-computeds';

/*
* Wraps an object that may contain exclaim Bindings, automatically resolving
Expand All @@ -29,6 +26,10 @@ export default class Environment {
return set(this, key, value);
}

bind(data) {
return bindComputeds(data, this);
}

on(type, listener) {
let listeners = this.__listeners__[type] || (this.__listeners__[type] = []);
listeners.push(listener);
Expand All @@ -54,90 +55,32 @@ export default class Environment {
object = this;
}

const resolveFieldMeta = this.__resolveFieldMeta__;
const resolvedPath = resolvePath(object, path);
return resolveFieldMeta(resolvedPath);
return this.__resolveFieldMeta__(resolveCanonicalPath(object, path));
}

unknownProperty(key) {
createEnvComputed(this, key, `__bound__.${findIndex(this.__bound__, key)}`);
defineProperty(this, key, broadcastingAlias(this, key));
return get(this, key);
}

setUnknownProperty(key, value) {
createEnvComputed(this, key, `__bound__.${findIndex(this.__bound__, key)}`);
set(this, key, value);
return get(this, key);
}
}

/*
* Given a piece of data and an environment, returns a wrapped version of that value that
* will resolve any Binding instances against the given environment.
*/
export function wrap(data, env, key) {
// Persist the original environment key if we're re-wrapping a new one
const realKey = extractKey(data) || key;
if (Array.isArray(data) || data instanceof EnvironmentArray) {
return EnvironmentArray.create({ data, env, key: realKey });
} else if (
(data && typeof data === 'object' && !isHTMLSafe(data)) ||
data instanceof EnvironmentData
) {
return EnvironmentData.create({ data, env, key: realKey });
} else {
return data;
}
}

/*
* Given a wrapped piece of data, returns the underlying one.
*/
export function unwrap(data) {
if (data instanceof EnvironmentArray || data instanceof EnvironmentData) {
return data.__wrapped__;
} else {
return data;
}
}

export function resolvePath(object, path) {
if (!path) return;

const parts = path.split('.');
const key = parts[parts.length - 1];
const host =
parts.length > 1 ? get(object, parts.slice(0, -1).join('.')) : object;
if (host instanceof Environment) {
return (
canonicalizeBinding(
host,
host.__bound__[findIndex(host.__bound__, key)][key]
) || key
);
} else if (host instanceof EnvironmentData) {
const canonicalized = canonicalizeBinding(
host.__env__,
host.__wrapped__[key]
);
const hostKey = extractKey(host);
return canonicalized || (hostKey && `${hostKey}.${key}`);
} else if (host instanceof EnvironmentArray) {
throw new Error('Cannot canonicalize the path to an array element itself.');
defineProperty(this, key, broadcastingAlias(this, key));
return set(this, key, value);
}
}

function canonicalizeBinding(env, value) {
if (value instanceof Binding) {
return resolvePath(env, value.path.join('.'));
} else if (
value instanceof EnvironmentData ||
value instanceof EnvironmentArray
) {
// We can wind up with wrapped values IN wrapped values in cases like `env.extend({ foo: env.get('bar') })`
// When this happens, we want to canonicalize on the original key
return resolvePath(env, extractKey(value));
}
function broadcastingAlias(host, key) {
const fullPath = `__bound__.${findIndex(host.__bound__, key)}.${key}`;
return computed(fullPath, {
get() {
return get(this, fullPath);
},
set(_, value) {
let result = set(this, fullPath, value);
this.trigger('change', key);
return result;
},
});
}

const hasProperty = Function.call.bind(Object.prototype.hasOwnProperty);
Expand Down
103 changes: 0 additions & 103 deletions ember-exclaim/src/-private/environment/array.js

This file was deleted.

Loading
Loading