-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
49 changed files
with
1,421 additions
and
331 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.