Skip to content

Commit

Permalink
updates to address feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
chancancode committed Mar 9, 2018
1 parent 483654e commit 77952db
Showing 1 changed file with 125 additions and 75 deletions.
200 changes: 125 additions & 75 deletions text/0000-custom-components.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
- Start Date: 2017-03-13
- RFC PR: (leave this empty)
- Ember Issue: (leave this empty)
- RFC PR: https://github.com/emberjs/rfcs/pull/213
- Ember Issue: https://github.com/emberjs/ember.js/issues/16301

# Summary

This RFC aims to expose a _low-level primitive_ for defining _custom
components_.

This API will allow addon authors to provide special-purpose component base
classes that their users can subclass from in the apps. These components are
classes that their users can subclass from in apps. These components are
invokable in templates just like any other Ember components (decendents of
`Ember.Component`) today.

# Motivation

The ability to author reusable, composable components is a core features of
The ability to author reusable, composable components is a core feature of
Ember.js. Despite being a [last-minute addition](http://emberjs.com/blog/2013/06/23/ember-1-0-rc6.html)
to Ember 1.0, the `Ember.Component` API and programming mode has proven itself
to Ember 1.0, the `Ember.Component` API and programming model has proven itself
to be an extremely versatile tool and has aged well over time into the primary
unit of composition in Ember's view layer.

Expand Down Expand Up @@ -87,8 +87,7 @@ objects (i.e. `{ singleton: true, instantiate: true }`), meaning that Ember
will create and maintain (at most) one instance of each unique component
manager for every application instance.

To register a component manager, an addon will typically put it inside its
`app` tree:
To register a component manager, an addon will put it inside its `app` tree:

```js
// ember-basic-component/app/component-managers/basic.js
Expand All @@ -100,6 +99,10 @@ export default EmberObject.extend({
});
```

(Typically, the convention is for addons to define classes like this in its
`addon` tree and then re-export them from the `app` tree. For brevity, we will
just inline them in the `app` tree directly for the examples in this RFC.)

This allows the component manager to participate in the DI system – receiving
injections, using services, etc. Alternatively, component managers can also
be registered with imperative API. This could be useful for testing or opt-ing
Expand Down Expand Up @@ -146,29 +149,13 @@ class:
import EmberObject from '@ember/object';
import { setComponentManager } from '@ember/component';

const MyComponent = EmberObject.extend({
export default setComponentManager('awesome', EmberObject.extend({
// ...
});

setComponentManager(MyComponent, 'awesome');

export default MyComponent;
}));
```

This tells Ember to use the `awesome` manager (`component-manager:awesome`) for
the `foo-bar` component. Since the `setComponentManager` function also returns
the class, this can also be simplified as:

```js
// my-app/app/components/foo-bar.js

import EmberObject from '@ember/object';
import { setComponentManager } from '@ember/component';

export default setComponentManager(EmberObject.extend({
// ...
}), 'awesome');
```
the `foo-bar` component. `setComponentManager` function returns the class.

In the future, this function can also be invoked as a decorator:

Expand All @@ -178,16 +165,15 @@ In the future, this function can also be invoked as a decorator:
import EmberObject from '@ember/object';
import { componentManager } from '@ember/component';

@componentManager('awesome')
export default EmberObject.extend({
export default @componentManager('awesome') EmberObject.extend({
// ...
});
```

In reality, app developer would never have to write this in their apps, since
the component manager would already be assigned on a super-class provided by
the framework or an addon. The `setComponentManager` function is essentially a
low-level API designed for addon authors and not intended to be used by app
In reality, an app developer would never have to write this in their apps,
since the component manager would already be assigned on a super-class provided
by the framework or an addon. The `setComponentManager` function is essentially
a low-level API designed for addon authors and not intended to be used by app
developers.

For example, the `Ember.Component` class would have the `classic` component
Expand All @@ -212,20 +198,19 @@ Similarly, an addon can provided the following super-class:
import EmberObject from '@ember/object';
import { componentManager } from '@ember/component';

@componentManager('basic')
export default EmberObject.extend({
export default setComponentManager('basic', EmberObject.extend({
// ...
});
}));
```

With this, app developers can simply inherit from this in their app:

```js
// my-app/app/components/foo-bar.js

import TurboComponent from 'ember-basic-component';
import BasicComponent from 'ember-basic-component';

export default TurboComponent.extend({
export default BasicComponent.extend({
// ...
});
```
Expand Down Expand Up @@ -457,7 +442,7 @@ Imagine if `post.title` changed from `fOo BaR` to `FoO bAr`. Since the value
is passed through the `uppercase` helper, the component will see `FOO BAR` in
both cases.

Generally speaking, Ember does not provide any guarentee on how it determine
Generally speaking, Ember does not provide any guarentee on how it determines
whether components need to be re-rendered, and the semantics may vary between
releases – i.e. this method may be called more or less often as the internals
changes. The _only_ guarentee is that if something _has_ changed, this method
Expand All @@ -469,6 +454,27 @@ the extra book-keeping.

For example:

```js
// ember-basic-component/index.js

import EmberObject from '@ember/object';
import { setComponentManager } from '@ember/component';

function NOOP() {}

export default setComponentManager('basic', EmberObject.extend({
// Users of BasicComponent can override this hook to be notified when an
// argument will change
argumentWillChange: NOOP,

// Users of BasicComponent can override this hook to be notified when an
// argument will change
argumentDidChange: NOOP,

// ...
}));
```

```js
// ember-basic-component/app/component-managers/basic.js

Expand Down Expand Up @@ -496,7 +502,7 @@ export default EmberObject.extend({
// generally assume that they have exactly the same keys. However, future
// additions such as "splat arguments" in the template layer might change
// that assumption.
for (let key of Object.keys(oldArgs)) {
for (let key in oldArgs) {
let oldValue = oldArgs[key];
let newValue = newArgs[key];

Expand Down Expand Up @@ -524,7 +530,14 @@ template to reflect any changes.
## Capabilities

In addition to the methods specified above, component managers are required to
have a `capabilities` property:
have a `capabilities` property. This property must be set to the result of
calling the `capabilities` function provided by Ember.

### Versioning

The first, mandatory, argument to the `capabilities` function is the component
manager API, which is denoted in the `${major}.${minor}` format, matching the
minimum Ember version this manager is targeting. For example:

```js
// ember-basic-component/app/component-managers/basic.js
Expand All @@ -549,23 +562,16 @@ export default EmberObject.extend({
});
```

This property must be set to the result of calling the `capabilities` function
provided by Ember.

The first, mandatory, argument to the `capabilities` function is the component
manager API, which is denoted in the `${major}.${minor}` format, matching the
minimum Ember version this manager is targeting.

This allows Ember to introduce new capabilities and make improvements to this
API without breaking existing code.

Here is a hypothical scenario for such a change:

1. Ember 3.2 implemented and shipped the component manager API as described in
this API.
this RFC.

2. The `ember-basic-component` addon releases its first version with the
component manager shown above (notably, it declared `capabilities('3.2')`).
2. The `ember-basic-component` addon released version 1.0 with the component
manager shown above (notably, it declared `capabilities('3.2')`).

3. In Ember 3.5, we determined that constructing the arguments object passed to
the hooks is a major performance bottleneck, and changes the API to pass a
Expand All @@ -579,13 +585,57 @@ Here is a hypothical scenario for such a change:
4. The `ember-basic-component` addon author would like to take advantage of
this performance optimization, so it updates its component manager code to
work with the arguments proxy and changes its capabilities declaration to
`capabilities('3.5')` in a major release.
`capabilities('3.5')` in version 2.0.

This system allows us to rapidly improve the API and take advantage of
underlying rendering engine features as soon as they become available.

Note that addon authors are not _required_ to update to the newer API.
Concretely, component manager APIs have the following support policy:

* API versions will continue to be supported in the same major release of
Ember. As shown in the example above, `ember-basic-component` 1.0 (which
targets component manager API version 3.2), will continue to work on
Ember 3.5. However, the reverse is not true – component manager API version
3.5 will (somewhat obviously) not work in Ember 3.2.

* In addition, to ensure a smooth transition path for addon authors and app
developers ...TODO...

Addon authors can also choose to target multiple versions of the component
manager API using [ember-compatibility-helpers](https://github.com/pzuraq/ember-compatibility-helpers/):

```js
// ember-basic-component/app/component-managers/basic.js

import { gte } from 'ember-compatibility-helpers';

let ComponentManager;

if (gte('3.5')) {
ComponentManager = EmberObject.extend({
capabilities: capabilities('3.5'),

// ...
});
} else {
ComponentManager = EmberObject.extend({
capabilities: capabilities('3.2'),

// ...
});
}

export default ComponentManager;
```

Since the conditionals are resolved at build time, the irrevelant code will be
stripped from production builds, avoiding any deprecation warnings.

This system allows us to rapidly improve the API and take advantage of the
underlying rendering engine featuers as soon as they become available.
### Optional Features

The second, optional, argument to the `capabilities` is an object enumerating
the optional features requested by the component manager.
The second, optional, argument to the `capabilities` function is an object
enumerating the optional features requested by the component manager.

In the hypothical example above, while the "reified" arguments objects may be
a little slower, they are certainly easier to work with, and the performance
Expand Down Expand Up @@ -661,7 +711,7 @@ called before or after the component's DOM tree is removed from the document).
Therefore, this hook is not suitable for invoking user callbacks intended for
performing DOM cleanup, such as `willDestroyElement` in the classic components
API. We expect a subsequent RFC addressing DOM-related functionalities to
clearify this issues or provide another specialized method for that purpose.
clarify this issues or provide another specialized method for that purpose.

Similar to the other async lifecycle callbacks, this API provides no guarentee
about ordering with respect to siblings or parent-child relationships. Further,
Expand Down Expand Up @@ -712,7 +762,7 @@ export default EmberObject.extend({
import EmberObject from '@ember/object';
import { setComponentManager } from '@ember/component';

export default setComponentManager(EmberObject.extend(), 'basic');
export default setComponentManager('basic', EmberObject.extend());
```

### Usage
Expand Down Expand Up @@ -793,7 +843,7 @@ import { setComponentManager } from '@ember/component';

// Our `createComponent` method does not actually do anything with the factory,
// so we don't even need to export a class here, `{}` would work just fine.
export default setComponentManager({}, 'template-only');
export default setComponentManager('template-only', {});
```

### Usage
Expand All @@ -813,7 +863,7 @@ Hello world! I have no backing class! {{this}} would be <code>null</code>.
## Recycling Components

This example implements an API which maintain a pool of recycled component
instances to avoid allocation costs, similar to Flex's "sustain" feature.
instances to avoid allocation costs, similar to [flexi-sustain](https://github.com/html-next/flexi-sustain).

This example also make use of the "state bucket" pattern.

Expand Down Expand Up @@ -898,12 +948,12 @@ import { setComponentManager } from '@ember/component';

function NOOP() {}

export default setComponentManager(EmberObject.extend({
export default setComponentManager('pooled', EmberObject.extend({
// Override this to implement reset any state on the instance
willRecycle(): NOOP,

// ...
}), 'pooled');
}));
```

# How We Teach This
Expand All @@ -919,10 +969,10 @@ do not expect the guides need to be updated for this feature (at least not the
components section).

For documentation purposes, each Ember.js release will only document the latest
component manager API. The documentation will also include the steps needed to
upgrade, as well as a list of new capabilities added since the last version.
Documentation for a specific version of the component manager API can be viewed
from the versioned documentation site.
component manager API, along with the available optional capabilities for that
realease. The documentation will also include the steps needed to upgrade from
the previous version. Documentation for a specific version of the component
manager API can be viewed from the versioned documentation site.

# Drawbacks

Expand All @@ -948,17 +998,17 @@ without rationalizing or exposing the underlying primitives.

## Follow-up RFCs

We expect a few follow-up RFCs to introduce additional capabilities that are
not included in this minimal proposal. These RFCs can run either in parallel
to this RFC or be submitted after this initial RFC has been implemented and
tested in the wild.
We expect to rapidly iterate and improve the component manager API through the
RFC process and in-the-field usage/implementation experience. Here are a few
examples of additional capabilities that we hope to see proposed after this
initial (and intentionally minimal) proposal is finalized:

1. Expose a way to get access to the component's DOM structure, such as its
element and bounds. This RFC would also need to introduce a hook for DOM
teardown and address how event handling/delegation would work.
1. Expose a way to access to the component's DOM structure, such as its bounds.
This RFC would also need to introduce a hook for DOM teardown and address
how event handling/delegation would work.

2. Expose a way to get access to the [reference][1]-based APIs. This could
include the ability to customize the component's "tag" ([validator][2]).
2. Expose a way to access to the [reference][1]-based APIs. This could include
the ability to customize the component's "tag" ([validator][2]).

[1]: https://github.com/glimmerjs/glimmer-vm/blob/master/guides/04-references.md
[2]: https://github.com/glimmerjs/glimmer-vm/blob/master/guides/05-validators.md
Expand Down Expand Up @@ -1004,7 +1054,7 @@ class BasicComponent {
// ...
}

export default setComponentManager(BasicComponent, 'basic');
export default setComponentManager('basic', BasicComponent);
```

```js
Expand Down

0 comments on commit 77952db

Please sign in to comment.