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

Implement dummy app #2

Merged
merged 8 commits into from
Jun 21, 2017
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
165 changes: 149 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,159 @@
# ember-exclaim
# ember-exclaim [![Build Status](https://travis-ci.org/salsify/ember-exclaim.svg?branch=master)](https://travis-ci.org/salsify/ember-exclaim)

This README outlines the details of collaborating on this Ember addon.
This addon allows apps to expose declarative, JSON-configurable custom UIs that are backed by Ember components.

## Installation
## Simple Examples

* `git clone <repository-url>` this repository
* `cd ember-exclaim`
* `npm install`
The building blocks available to an ember-exclaim UI are defined by the app it's used in, but the dummy application in this project contains implementations of several basic components that might be useful. For example, in the [demo application](https://salsify.github.io/ember-exclaim), the following would render a header with some filler content below it:

## Running
```js
{
"$vbox": [
{ "$header": "Very Important Content" },
{ "$text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." }
]
}
```

* `ember serve`
* Visit your app at [http://localhost:4200](http://localhost:4200).
<img src="https://user-images.githubusercontent.com/108688/27345599-4e40b902-55b8-11e7-8e65-195117aeded4.png" width="369">

## Running Tests
Components can also read from and write back to an underlying data structure. For example, given this environment:

* `npm test` (Runs `ember try:each` to test your addon against multiple Ember versions)
* `ember test`
* `ember test --server`
```js
{
"target": "world"
}
```

## Building
The following UI would render "Hello, world!":

* `ember build`
```js
{
"$text": ["Hello, ", { "$bind": "target" }, "!"]
}
```

For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/).
And something like this would render an input that would update the underlying value of `target` as the user made changes:

```js
{
"$input": { "$bind": "target" }
}
```

![hello](https://user-images.githubusercontent.com/108688/27351743-593ffcb4-55cc-11e7-9fdb-cb8eb33daa42.gif)

## Usage

The entry point to a UI powered by ember-exclaim is the `{{exclaim-ui}}` component. It expects three arguments:
- `ui`: an object containing configuration for the UI that should be rendered
- `env`: a hash whose keys will be bindable from the `ui` config, to be read from and written to
- `componentMap`: a mapping of component names in the `ui` config to information about their backing Ember components

Each of these three things is described in further detail below.

### UI Configuration

The configuration for an ember-exclaim UI boils down to two special keys: `$component` and `$bind`.

#### `$component`

The basic way to invoke a component in an ember-exclaim UI is with a hash containing a `$component` key. The value of this key will be used to look up the underlying Ember component implementation according to the configured `componentMap`, and all other keys in the hash will become that component's `config`. As a concrete example, consider the following usage of a hypothetical `text` component:

```js
{
"$component": "text",
"content": "Hello"
}
```

This would invoke whatever Ember component is configured under the `text` key of the given `componentMap`, passing it a hash of configuration that looks like `{ content: 'Hello' }`.

#### `$bind`

To read and write values in the environment, the UI author can use `$bind`. For example, dynamically rendering whatever the value the configured environment has for `greeting` using the same text component described above might look like:

```js
{
"$component": "text",
"content": { "$bind": "greeting" }
}
```

Similarly, an `input` component could write to the environment's `greeting` value as the user makes changes:

```js
{
"$component": "input",
"value": { "$bind": "greeting" }
}
```

Note that component implementations might also expose `$bind`able values to their children, such as an `each` component that iterates an array and exposes each item in that array under a given name.

#### Component Shorthand

You may have noted that the examples in this section appear more verbose than those at the top of the document. By supplying the name of a _shorthand property_, components can be invoked using their name prefixed with a `$` as a key for that property, skipping the `$component` key completely.

For example, the `text` component in [the demo application](https://salsify.github.io/ember-exclaim) declares its shorthand property to be `content`, making this:

```js
{
"$component": "text",
"content": "Hello, world!"
}
```

Equivalent to this:

```js
{ "$text": "Hello, world!" }
```

Any other configuration keys the component expects can be specified the same way in either format.

### The Environment

Keys on the given `env` object are what powers `$bind` directives in the configured UI. The object in question may be a simple POJO or something richer, like an Ember Data model.

Note that `$bind` works with paths, too, so `{ $bind: 'foo.bar' }` would access the `bar` key of the `foo` object in the environment.

### The Component Map

The `componentMap` given to `{{exclaim-ui}}` dictates what components it can render. It should be a hash whose keys are the component names available for use in the UI config. The value for each key should itself be a hash containing the following:
- `componentPath`: the name to the Ember component to be invoked when this exclaim-ui component is used in the config, as you'd give it to the `{{component}}` helper
- `shorthandProperty` (optional): the name of a property that should be populated when shorthand notation is used for this component (see above)

## Implementing Components

The [demo app](https://salsify.github.io/ember-exclaim) for this repo contains [a variety of simple component implementations](tests/dummy/app/components/exclaim-components) that you can use as a starting point for building your own.

An ember-exclaim component implementation will receive two properties when rendered: `env` and `config`.

### `config`

The `config` property of the implementing component will contain all other information supplied in the `$component` hash representing it in the UI config. Any `$bind` directives in that config will be automatically be resolved when they are `get` or `set`. As an example, consider a lightweight implementation of the `input` component mentioned above.

```hbs
<input type="text" value={{config.value}} oninput={{action (mut config.value) value='target.value'}}>
```

When invoked as `{ "$component": "input", "value": {"$bind":"x"} }` with `x` in the environment having the value `'hello'`, this component will receive the equivalent of `{ value: 'hello' }` as its `config`, except that reading and writing `config.value` will redirect back to `x` on the environment.

### `env`

The `env` property will contain an object representing the environment that the component is being rendered in. This object has a method `extend`, which, given a hash, can be used to produce a new environment based on the original that contains the values from that hash.

### Rendering Children

In many cases, components may want to accept configuration for subcomponents that they can render under different circumstances, such as an `if` component that conditionally renders some content, or an `each` component that renders the same child multiple times against different values. Implementations can accomplish this by `{{yield}}`ing the configuration for the child component.

For example, the [`vbox`](tests/dummy/app/components/exclaim-components/vbox) component in the demo application applies a class with `flex-flow: column` to its root element and then simply renders all its children directly beneath:

```hbs
{{#each config.children as |child|}}
{{yield child}}
{{/each}}
```

By default, children will inherit the environment of their parent. This environment can be overridden by passing a new `env` value as a second parameter to `{{yield}}`, typically obtained by calling `extend` on the base environment (see above). Check the implementation of [`each`](tests/dummy/app/components/exclaim-components/each) and [`let`](tests/dummy/app/components/exclaim-components/let) in the demo app for examples of how this can be used.
19 changes: 19 additions & 0 deletions addon/-private/GLOSSARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Glossary

This document aims to be a brief summary of the terminology used within the implementation of this addon.

## UI Config

A **`ComponentSpec`** is the information necessary to render a specific exclaim component instance: specifically, it includes the path to the Ember implementation for that component and whatever user-specified config will be supplied to that component when it's rendered.

Any hash in the UI config with a `$component` key will be transformed into a `ComponentSpec` instance, with the value for that key being used to locate the implementation of the underlying Ember component in the configured `componentMap`. All other keys in that hash will be captured as that `ComponentSpec`'s `config`, which will be passed into the Ember component on instantiation.

A **`Binding`** is a reference to some value available in the salient `Environment` (see below), much like a variable reference in a programming language. A `Binding` is meaningless on its own, and must always be evaluated in the context of some `Environment`. Note that the `config` for a `ComponentSpec` may contain `Binding`s, which won't be resolved until they're actually used. This allows components to evaluate parts of their config in varying contexts, such as an `each` component rendering the same subcomponent config with varying values for its iteration variable.

## Runtime Elements

At runtime, exclaim UIs are evaluated relative to an **`Environment`**, which is analogous to scope in a programming language. An `Environment` contains all the bound values that are available to `Binding`s, and may itself contain `Binding` instances that point at other data within itself. When the time comes to resolve the `config` for a component to actual values, `ComponentSpec` instances expose a `resolveConfig(environment)` method, which returns an `EnvironmentData` instance for the configuration.

An **`EnvironmentData`** object is a proxy for some arbitrary hash or array that resolves any `Binding`s it contains relative to some `Environment`. You can think of `EnvironmentData` as a piece of data that remembers where it came from. For instance, given an `EnvironmentData` instance `data` wrapping the hash `{ hi: 'hello', bye: new Binding('farewell') }`, calling `data.get('hi')` would return the string `'hello'`, and calling `data.get('bye')` would return whatever the associated `Environment` contains for the key `farewell`.

Implementation note: when an `Environment` or `EnvironmentData` instance is asked to `get` a property, it first inspects whether the underlying value for that property is a `Binding`, and if so, resolves it. Once this resolution has occurred the first time, a computed property is generated so that subsequent lookups don't have to re-resolve, and changes to the underlying bound property will be reflected on the host `EnvironmentData` or `Environment`. Any non-primitive result of a `get` on an `Environment` or `EnvironmentData` instance will itself be a `EnvironmentData`, so that `Binding`s nested arbitrarily deep will always be resolved. There is also an `EnvironmentData` variant called `EnvironmentArray` which functions similarly but wraps arrays rather than objects.
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ const {
* Wraps an array, resolving any Bindings in it when requested to the corresponding
* paths in the given environment.
*/
export default class ArrayValue extends ArrayProxy {
constructor(value, env) {
super({ content: value });
this.__wrapped__ = (value instanceof ArrayValue) ? value.__wrapped__ : A(value);
export default class EnvironmentArray extends ArrayProxy {
constructor(data, env) {
super({ content: data });
this.__wrapped__ = (data instanceof EnvironmentArray) ? data.__wrapped__ : A(data);
this.__env__ = env;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ const {
* Wraps an object, resolving any Bindings in it when requested to the corresponding
* paths in the given environment.
*/
export default class Value extends EmberObject {
constructor(value, env) {
export default class EnvironmentData extends EmberObject {
constructor(data, env) {
super();
this.__wrapped__ = (value instanceof Value) ? value.__wrapped__ : value;
this.__wrapped__ = (data instanceof EnvironmentData) ? data.__wrapped__ : data;
this.__env__ = env;
}

Expand Down
28 changes: 14 additions & 14 deletions addon/-private/environment/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Ember from 'ember';
import createEnvComputed from './create-env-computed';
import Value from './value';
import ArrayValue from './array-value';
import EnvironmentData from './data';
import EnvironmentArray from './array';

const {
get,
Expand Down Expand Up @@ -39,27 +39,27 @@ export default class Environment extends EmberObject {
}

/*
* Given a value and an environment, returns a wrapped version of that value that
* 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(value, env) {
if (Array.isArray(value) || value instanceof ArrayValue) {
return new ArrayValue(value, env);
} else if (value && typeof value === 'object' || value instanceof Value) {
return new Value(value, env);
export function wrap(data, env) {
if (Array.isArray(data) || data instanceof EnvironmentArray) {
return new EnvironmentArray(data, env);
} else if (data && typeof data === 'object' || data instanceof EnvironmentData) {
return new EnvironmentData(data, env);
} else {
return value;
return data;
}
}

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

Expand Down
2 changes: 1 addition & 1 deletion addon/components/exclaim-ui/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default Component.extend({
return new Environment(get(this, 'env') || {});
}),

content: computed('specProcessor', 'interface', function() {
content: computed('specProcessor', 'ui', function() {
const processor = get(this, 'specProcessor');
const ui = get(this, 'ui');

Expand Down
28 changes: 28 additions & 0 deletions config/deploy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* jshint node: true */

module.exports = function(deployTarget) {
var ENV = {
build: {}
// include other plugin configuration that applies to all deploy targets here
};

if (deployTarget === 'development') {
ENV.build.environment = 'development';
// configure other plugins for development deploy target here
}

if (deployTarget === 'staging') {
ENV.build.environment = 'production';
// configure other plugins for staging deploy target here
}

if (deployTarget === 'production') {
ENV.build.environment = 'production';
// configure other plugins for production deploy target here
}

// Note: if you need to build some configuration asynchronously, you can return
// a promise that resolves with the ENV object instead of returning the
// ENV object synchronously.
return ENV;
};
13 changes: 5 additions & 8 deletions ember-cli-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@ const EmberAddon = require('ember-cli/lib/broccoli/ember-addon');

module.exports = function(defaults) {
var app = new EmberAddon(defaults, {
// Add options here
ace: {
themes: ['chrome'],
modes: ['json'],
workers: ['json'],
}
});

/*
This build file specifies the options for the dummy test app of this
addon, located in `/tests/dummy`
This build file does *not* influence how the addon or the app using it
behave. You most likely want to be modifying `./index.js` or app's build file
*/

return app.toTree();
};
13 changes: 9 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"name": "ember-exclaim",
"version": "0.0.0",
"description": "The default blueprint for ember-cli addons.",
"description": "An addon allowing apps to expose declarative, JSON-configurable custom UIs backed by Ember components",
"keywords": [
"ember-addon"
],
"license": "MIT",
"author": "",
"author": "Dan Freeman",
"directories": {
"doc": "doc",
"test": "tests"
},
"repository": "",
"repository": "salsify/ember-exclaim",
"scripts": {
"build": "ember build",
"start": "ember server",
Expand All @@ -32,9 +32,13 @@
"ember-cli": "^2.13.2",
"ember-cli-dependency-checker": "^1.3.0",
"ember-cli-dependency-lint": "^1.0.2",
"ember-cli-deploy": "^1.0.1",
"ember-cli-deploy-build": "^1.1.0",
"ember-cli-deploy-git": "^1.1.1",
"ember-cli-eslint": "^3.0.0",
"ember-cli-htmlbars-inline-precompile": "^0.4.0",
"ember-cli-inject-live-reload": "^1.4.1",
"ember-cli-node-assets": "^0.2.2",
"ember-cli-qunit": "^4.0.0",
"ember-cli-shims": "^1.1.0",
"ember-cli-sri": "^2.1.0",
Expand All @@ -56,6 +60,7 @@
"node": ">= 4"
},
"ember-addon": {
"configPath": "tests/dummy/config"
"configPath": "tests/dummy/config",
"demoURL": "https://salsify.github.io/ember-exclaim"
}
}
6 changes: 1 addition & 5 deletions tests/dummy/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ import Resolver from './resolver';
import loadInitializers from 'ember-load-initializers';
import config from './config/environment';

let App;

Ember.MODEL_FACTORY_INJECTIONS = true;

App = Ember.Application.extend({
const App = Ember.Application.extend({
modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix,
Resolver
Expand Down
Loading