From 35b1fee9f09659191fe3af96582ee1fdaecc92d1 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Mon, 22 Apr 2019 19:53:26 +0800 Subject: [PATCH 1/8] Component Templates Co-location --- text/0481-component-templates-co-location.md | 647 +++++++++++++++++++ 1 file changed, 647 insertions(+) create mode 100644 text/0481-component-templates-co-location.md diff --git a/text/0481-component-templates-co-location.md b/text/0481-component-templates-co-location.md new file mode 100644 index 0000000000..2f8caef250 --- /dev/null +++ b/text/0481-component-templates-co-location.md @@ -0,0 +1,647 @@ +- Start Date: 2019-04-12 +- Relevant Team(s): Ember.js, Ember CLI +- RFC PR: (after opening the RFC PR, update this with a link to it and update the file name) +- Tracking: (leave this empty) + +# Component Templates Co-location + +## Summary + +We propose to place a component's JavaScript file (the class) and its template +in the same directory on the file system. + +Today: + +``` +app +├── components +│ ├── just-class.js +│ ├── my-widget.js +│ ├── nested +│ │ └── another-widget.js +│ └── ... +├── models +│ └── ... +├── routes +│ └── ... +├── services +│ └── ... +├── templates +│ ├── components +│ │ ├── just-template.hbs +│ │ ├── my-widget.hbs +│ │ ├── nested +│ │ │ └── another-widget.hbs +│ │ └── ... +│ ├── some-route.hbs +│ └── ... +└── ... +``` + +Proposed: + +``` +app +├── components +│ ├── just-class.js +│ ├── just-template.hbs +│ ├── my-widget.hbs +│ ├── my-widget.js +│ ├── nested +│ │ ├── another-widget.hbs +│ │ └── another-widget.js +│ └── ... +├── models +│ └── ... +├── routes +│ └── ... +├── services +│ └── ... +├── templates +│ ├── some-route.hbs +│ └── ... +└── ... +``` + +## Motivation + +Today, a component's JavaScript file is located in the `app/components` +directory. On the other hand, it's template is located in the +`app/templates/components` directory. + +This design decision is a relic of Ember's pre-1.0 days. In those early days, +before components were even conceived, "views" were the primary building blocks +in Ember's UI programming model. Like components, they consist of a view class +located in `app/views/$NAME.js` (subclassing `Ember.View`), and/or a template +located in `app/templates/$NAME.hbs`. In other words, every template in Ember +could optionally have an associated view class, and vice versa. + +Similar to components, they can be "invoked" in other templates using either +the `{{view "$NAME"}}` or `{{#view "$NAME"}}...{{/view}}` syntax. At the time, +this was how Ember developers create reusable pieces of UI in their app. + +Unlike components, when a view is "invoked", the view instance does not become +the `this` context of its template. Instead, the `this` from the calling side +will persist (similar to partials), while properties on the view instance are +accessible from within the template with `{{view.someProperty}}` syntax. + +Over time, this programming model has gone through several iterations of +refinements (remember the short-lived "controls"? 😀), eventually evolved into +the component-based programming model as we know it today. + +When components were introduced, it was decided that they would live in a +separate "namespace" than views: their JavaScript files are located in +`app/components` instead of `app/views`, and it uses the now-familiar curly +invocation syntax instead of the `view` keyword. + +This decision causes two problems. First, given a template file, such as +`app/templates/foo-bar.hbs`, how do we know if it belongs to a component or a +view? Furthermore, since they live in different namespaces, it is totally legal +to have unrelated components and views with the same names, which could result +in naming collisions if their templates were both located in `app/templates`. +To solve this problem, component templates were also given a separate namespace +`app/templates/components`, as opposed to sharing `app/templates` with views. + +Over the course of the Ember 1.x release series, components have taken over as +the primary UI programming primitive. Views were eventually deprecated and +removed from Ember's API entirely in Ember 2.0. Despite that, the ramifications +of that era are still around us. + +We argue that the current file system layout for component files is one such +example. Given how much Ember has evolved since its early days, most of the +design constraints we had at the time no longer apply. On the other hand, new +features in the framework – such as the introduction of Ember CLI and the addon +ecosystem – has brought new capabilities and constraints to the table. As a +result, this design has caused a lot of "ergonomics papercuts", which we have +enumerated below. While each of these may feel inconsequential on their own, +together they cause a lot of unnecessary friction and feel out-of-place in the +modern Ember experience. + +### 1. Component class/template coupling + +The JavaScript class and template of a component are not just _related_ but +fundamentally _coupled_. Together, they provide the whole implementation of +the component. It is impossible to understand one of them without the other. +When one file is changed, the other likely need to be updated as well. Placing +these files physically far apart on the file system fails to communicate this +tight coupling, in addition to causing a lot of inconveniences when navigating +a project. + +### 2. No single enumeration of components + +Given the importance of components in modern Ember apps, when navigating a +large-scale or otherwise unfamiliar project, some of the most common task for +Ember developers is to enumerate all components from the given app or to +determine whether there exists a component with a specific name. The former is +useful for getting a general sense of the scope, scale and coding style, and +the latter is sometimes necessary to disambiguate components from helpers or +to determine whether a component is part of the app or provided by an addon. + +Unfortunately, under the current filesystem layout, neither `app/components` or +`app/templates/components` provide a complete view for those purposes. With +template-only components, it is possible to define a component without a class, +therefore a casual scan of `app/components` will not discover those components. +With the release of Octane, we expect template-only components to become more +common. Likewise, albeit less common, it is also possible to define a component +with *only* a class, making `app/templates/components` potentially incomplete +also. + +Therefore, the "correct" way to explore the components in an app is to mentally +merge the contents of both folders, which is difficult and counterproductive. + +### 3. Deviation from route template conventions + +In the view-centric days of Ember, `app/templates` was a "kitchen-sink" folder +without much internal structure – it mixes route templates with reusable view +templates, as well as component templates, each of which could be grouped into +their own deeper nesting structure. + +With Ember 2.0 removing views, and therefore view templates, `app/templates` +now has a clear purpose and internal structure: each template file corresponds +to a route, and each folder corresponding to a nested route. For example, +`app/templates/post.hbs` is the template for the top-level `post` route, and +`app/templates/admin/authors/new.hbs` is the template for `admin.authors.new`. + +Of course, there is one prominent exception to this: `app/templates/components` +breaks the otherwise clear convention. Unlike other folders in `app/templates`, +`app/templates/components` does not represent a nested route. Furthermore, its +internal structure (if any) also differs from its siblings: folders represent +arbitrary logical/domain groupings, rather than sub-routes. + +Since `app/templates/components` did not start with a special character (e.g. +`app/templates/-components`), it shows up in the middle between its routes +siblings in most apps, due to lexicographical sorting of the filenames. + +This inconsistency causes confusion and prevents us from teaching the otherwise +clear convention crisply. + +### 4. Lack of single import + +Even though the JavaScript class and template of a component are fundamentally +coupled and inseparable pieces, this relationship is only very loosely encoded +in Ember today. In fact – the only thing that ties them together is their name: +when Ember tries to render a component, it will separately lookup the class and +template by name through the resolver. These elements can both be present at +the same time, or in the cases of template-only or class-only components, one +of the resolutions will return a negative result. Only when both elements are +missing would Ember declare the component missing and raise an error. + +This setup falls short when designing components meant for subclassing. For +example: + +```js +// app/components/foo-bar.js + +import MyParentComponent from './my-parent'; + +export default class FooBarComponent extends MyParentComponent { + // ... +} +``` + +As far as Ember can tell, this file defines a class-only component by the name +of `foo-bar`. The fact that it inherits from `my-parent` is an irrelevant and +unobservable detail. While the developer may expect this to also inherit the +parent component's template (`app/templates/components/my-parent.hbs`), Ember +has no way to know that the two are related. + +Because of this problem, addons typically use the `layout` property from the +classic component API to get around this problem. When generating a component +in an addon, this is the default output from the blueprint: + +```js +// addon/components/foo-bar.js + +import Component from '@ember/component'; +import layout from '../templates/components/foo-bar'; + +export default Component.extend({ + layout +}); +``` + +```hbs +{{!-- addon/templates/components/foo-bar.hbs --}} + +{{yield}} +``` + +```js +// app/components/foo-bar.js + +export { default } from 'my-addon/components/foo-bar'; +``` + +First, within the `addon` folder, there is the component class and template. +Having these files in the `addon` folder allow them to be imported from +JavaScript (e.g. `import FooBar from 'my-addon/components/foo-bar';`) for +subclassing. However, as mentioned above, this does not also bring along the +component's template. + +To avoid developers having to also import the template, the addon component +blueprint sets the `layout` property on the component's class. At runtime, +when Ember fail to find a template associated with the component, it will +then fall back to this property. + +Finally, it re-exports the component in the `app` folder, which is merged with +the app's `app` folder, allowing the component to be resolved by the resolver +and invoked from Handlebars. + +This workaround gets the job done, but has several major drawbacks. + +First, it adds a significant amount of boilerplate to addon components, which +could be puzzling even to seasoned addon developers. In fact, the problem it +solves is unique to addons – there is nothing wrong with using inheritance in +apps – we just didn't think we could justify adding this amount of boilerplate +to every app and putting them in Ember's "happy path". + +Second, the `layout` property in classic component uses a feature in Glimmer VM +called "late-bound layout", in which we must assume a different template for +each _instance_ of the component (the `layout` property can be set in `init`). +This is fundamentally more difficult to optimize and excludes the component +from key optimizations (and opportunities for future optimizations). + +Most importantly, Glimmer components do not have the equivalent of the `layout` +property, meaning that addons (or components designed for inheritance in mind) +cannot be written using Glimmer components today. + +To solve these problems once and for all, we need to change things such that +component templates are automatically associated with their JavaScript class at +build time, and importing the JavaScript class should result in a value that +has this metadata associated with it, which needs to survive subclassing as +well. Solving this problem is also essential for unblocking template imports. + +We believe the technical design for allowing co-location will solve this +problem nicely. + +## Detailed design + +### High-level design + +We propose to allow placing a component's template adjacent to its JavaScript +file in `app/components`. For example, for a component named `foo-bar`, it will +be `app/components/foo-bar.js` and `app/components/foo-bar.hbs`. + +In addition, per the node resolution convention, we propose to allow `index` +files inside a directory to have the equivalent semantics. In the example +above, it could also be structured as `app/components/foo-bar/index.js` and +`app/components/foo-bar/index.hbs`. This allows additional files related to the +component (such as a `README.md` file) to be co-located on the filesystem. + +For template-only components, they can be either `app/components/foo-bar.hbs` +or `app/components/foo-bar/index.hbs` without a corresponding JavaScript file. + +Similarly, for addons, templates can be placed inside `addon/components` with +the same rules laid out above. + +In all of these case, if a template file is present in `app/components` or +`addon/components`, it will take precedence over any corresponding template +files in `app/templates`, the `layout` property on classic components, or a +template with the same name that is made available with the resolver API. +Instead of being resolved at runtime, a template in `app/components` will be +associated with the component's JavaScript class at build time. + +### Low-level primitives + +We propose to introduce the following low-level APIs: + +* The `setComponentTemplate` function takes two arguments, the first being the + pre-compiled (wire-format) template, the second being the component class. It + transparently associates the given template with the component class in a way + can be retrieved later with the `getComponentTemplate` function described + below. For convenience, `setComponentTemplate` will return the component + class (the second argument). + +* The `getComponentTemplate` function takes a component class and returns the + template associated with the given component class, if any, or one of its + superclasses, if any, or `undefined` if no template association was found. + +This is one possible way to implement these functions: + +```js +function setComponentTemplate(template, componentClass) { + Object.defineProperty(componentClass, "__template__", { + configurable: true, + enumerable: false, + writable: false, + value: template + }); + + return componentClass; +} + +function getComponentTemplate(componentClass) { + return componentClass.__template__; +} + +class Foo {} +class Bar extends Foo {} +class Baz extends Bar {} + +class Bat {} + + +// USAGE + +setComponentTemplate(precompile("foo template"), Foo); +setComponentTemplate(precompile("baz template"), Baz); + +getComponentTemplate(Foo); // => foo tempalte +getComponentTemplate(Bar); // => foo tempalte (inherited) +getComponentTemplate(Baz); // => baz tempalte (overridden) + +getComponentTemplate(Bat); // => undefined +``` + +In an actual implementation, we would probably want to avoid polluting the +component class with a string key (`__template__` in this example) and use a +`Symbol` or `WeakMap` based strategery. + +For performance reason, changing the template is not allowed once set. It is +also illegal to call `setComponentTemplate` on a component class that has +already been rendered, or once `getComponentTemplate` has been called on it. +Together, these rules ensure the results from calling `getComponentTemplate` +can be reliably cached, either by the internals of `getComponentTemplate` +itself or by one of its callers. + +While the default export of a component's JavaScript file is usually a class, +it is not a strict requirement with custom component managers. To accommodate +this, `setComponentTemplate` can be passed any JavaScript `Object`. + +### Build-time transformations + +The low-level `setComponentTemplate` and `getComponentTemplate` APIs are not +intended to be called by end-users directly, even though they will be public +and part of our semver stability guarantee. Instead, they are meant to be used +primarily in the output emitted by build tools. At build time, any template +files found in `{app,addon}/components` will be inlined into the component's +JavaScript file and removed from the build output. For example, given these +files on disk: + +```js +// app/components/foo-bar.js + +import Component from '@ember/component'; + +export default Component.extend({ + // ... +}); +``` + +```hbs +{{!-- app/components/foo-bar.hbs --}} + +foo bar! +``` + +The build output will be something to the effect of this JavaScript file: + +```js +// app/components/foo-bar.js + +import Component, { setComponentTemplate } from '@ember/component'; + +// output of precompile("foo bar!") +const TEMPLATE = { + id: "...", + block: "...", + meta: { moduleName: "app/components/foo-bar" } +}; + +const CLASS = Component.extend({ + // ... +}); + +export default setComponentTemplate(TEMPLATE, CLASS); +``` + +The variables are named here for clarity, but the actual build output would +be careful to avoid introducing hygiene issues and other observable semantic +changes to the JavaScript file. + +One caveat here is that each component JavaScript file should export a value +that is unique to that file. For example, this should be **avoided**: + +```js +// app/components/foo-bar.js + +import MyParentComponent from './my-parent'; + +// BAD: don't do this! +export default MyParentComponent; +``` + +This is problematic because `setComponentTemplate` will be called on +`MyParentComponent` directly, affecting the parent component and all of its +descendants. This can be avoided by subclassing, even when no customization is +required: + +```js +// app/components/foo-bar.js + +import MyParentComponent from './my-parent'; + +// GOOD: do this instead! +export default class extends MyParentComponent {} +``` + +Most cases of this problem can be linted against easily. + +### Template-only components + +For template-only components, we propose to introduce the following low-level +API: + +* The `templateOnlyComponent` function takes no arguments and produces a unique + value that can be used to represent a template-only component. + +Again, this function, though public, is not intended to be called by users +directly. It is primarily used in the output emitted by build tools. When a +template is found in `{app,addon}/components` but without a corresponding +JavaScript file, the build output will be something similar to the following: + +```js +// app/components/foo-bar.js + +import Component, { setComponentTemplate } from '@ember/component'; +import templateOnlyComponent from '@ember/component/template-only'; + +// output of precompile("foo bar!") +const TEMPLATE = { + id: "...", + block: "...", + meta: { moduleName: "app/components/foo-bar" } +}; + +const CLASS = templateOnlyComponent(); + +export default setComponentTemplate(TEMPLATE, CLASS); +``` + +In addition to build tooling, addon authors may also find this function +useful. Currently, the only way to create a "true" template-only component +is by enabling an optional feature in the app. Since addons cannot assume the +value of that flag in the consuming app, they currently cannot take advantage +of the feature. By providing this function, addon authors can work around this +problem by explicitly defining a JavaScript file with `templateOnlyComponent()` +as the default export. + +On the other hand, app developers, should _not_ have any reason to use this +function directly in their app, since they could just as easily enable the +optional feature across the board. + +### Codemod + +A codemod will be provided to seamlessly migrate component templates. + +Such a "codemod" will essentially just merges `app/templates/components` into +`app/components`. For a quick "taste test" of what the resulting tree will look +like in your app, the following commands give a close approximation: + +```bash +$ rsync --archive --remove-source-files app/templates/components/ app/components/ +$ rm -rf app/templates/components/ +``` + +Of course, the resulting output won't "work", but it can be useful for getting +a sense of what it's like to work with the new layout on editors, Github, etc. + +### Generator + +We propose to make some updates to the components generator to accompany this +change. + +1. It should accept a `--component-class` option. This controls which base + class is used for that component and whether native classes are used. The + legal values for this option are `@glimmer/component` (aliased as `-gc`), + `@ember/component` (aliased as `-ec`), `@ember/component/template-only` + (aliased as `-tc`) or an empty string (aliased as `--no-component-class` and + `-nc`). + + The latter two differ in that `@ember/component/template-only` would + generate an explicit JavaScript file with `templateOnlyComponent()` as the + default export, which is useful for addons, whereas `--no-component-class` + would skip generating a JavaScript file altogether. + + This option may be extended in the future to allow other custom components + to provide their own blueprints, but for now, passing anything other than + the allowed values will result in an error. + +2. It should accept a `--component-structure` option. + + When this option is set to `flat` (aliased as `-fs`), the component's JavaScript and template files will both be generated at the root of + `{app,addon}/components`. + + When this option is set to `nested` (aliased as `-ns`), a folder will be + generated for the component in `{app,addon}/components`, and the component's + JavaScript and template files will be generated as `index.{js,hbs}` inside + the folder. + + When this option is set to `classic` (aliased as `-cs`), the component's + template file will be generated in `{app,addon}/templates/components`. For + addons, when used with `--component-class=@ember/component`, this will also + emit the `layout` property workaround. + +3. Thses options will default to `--component-class=@ember/component` and + `--component-structure=classic` for backwards compatibility. + + However, the default values can be overridden in `.ember-cli` as usual, and + teams are encouraged to do so as they see fit. Due to a limitation in how + the system works, the names for these options are chosen such that they are + unlikely to conflict with options on other `ember` commands, which is why + they are a bit verbose. + + For Octane apps, the default app blueprint will include a `.ember-cli` file + that defaults to `--no-component-class` and `--component-structure=flat`. + The guides and documentation will assume these settings going forward. + +### Timing + +We propose to include this change as part of the Octane edition. Admittedly, +this is a rather late addition. However, there are a few compelling reasons for +including it: + +1. Module Unification was a much-anticipated feature that + [did not make it](https://blog.emberjs.com/2019/03/11/update-on-module-unification-and-octane.html) + into the Octane edition. Even though it tries to solve a lot of other + problems, component co-location is one of the main benefits. This proposal + allows us to incrementally deliver a solution one of the biggest pain points + while we work out the other issues. + +2. The Octane edition will enable template-only components by default, which + will encourage its use and intensifies some of the issues enumerated in the + motivation section. + +3. The Octane edition will highlight Glimmer Components as the main programming + model. Without the `setComponentTemplate` API proposed in this RFC, addons + authors will not be able to adopt Glimmer Components. This will, over time, + significantly increase the barrier to authoring addons, as most Ember + developers will no longer be familiar with the classic components API. + +4. The Octane edition presents a good opportunity to update the documentation + and get the whole community on board with adopting a feature like this. It + is important that build tools, linters, codemods, etc take this feature into + account, and shipping this as part of the Octane edition is a good way to + signal that.x + +Even though it is pretty late in the Octane timeframe, we believe this feature +is relatively limited in scope (compared to, say, `@tracked`) and can be +implemented relatively quickly. + +## How we teach this + +As mentioned above, the learning team will update the guides and API docs to +assume the "flat" co-located layout. + +With this layout, it should be much easier for new users to form a mental model +around components. We can start by teaching that the most basic component is +just a reusable piece of markup (template-only components), but can "upgraded" +to have dynamic content by taking arguments using the `@name` syntax, and +further "upgraded" to keep internal states by creating a JavaScript file next +to the template and finally add interactivity with element modifiers. + +Because template-only component is such a light-weight concept, it is arguably +not necessary to separate out the topic of "templates" from "components" in the +guides, but this can be addressed in a future revision of the guides. + +We will teach that the `templates` folder is used for route templates. This can +be introduced at the same time as the `routes` and `controllers` folder, which +would come at a later time than components. + +## Drawbacks + +Unlike the rest of the Octane features, this is not as battle-tested. Also, as +mentioned above, this addition would be quite last-minute. However, it is quite +normal to discover minor problems (e.g. issues with using Glimmer components in +addons today) through real-world usage, and that is precisely the point of the +Octane preview period. Since Octane is all about polish and cohesion, we think +it is worth addressing these issues before stamping on the quality seal. + +Another drawback is that it only address the co-location issue for components, +not other related types like [[route, controller, route template]] and [[model, +adapter, serializer]], or even co-location of tests. However, we believe the +situation with components is unique enough (see the motivation section) that +they are not merely *related*, but *coupled*. That, along with the fact that +components are much more common, sets them apart from the rest and justifies +solving the problem first. + +There is a small risk that we will subject the community to another migration +when we finalize the replacement of Module Unification (the "New File Layout"). +However, we feel pretty confident that regardless of where the collection of +components will end up on disk (e.g. `src/ui/components`), the internal +structure of that collection will closely match what is proposed in this RFC. +Ultimately, we expect there to be automatic migrators for these kinds of +changes anyway, so the cost of the possible churn is contained. + +## Alternatives + +We can forgo the opportunity to address these problems as part of Octane, and +instead land this proposal (or something like it) after the edition has +shipped. That would come at a cost of addon authors not being able to adopt +some of the Octane features until we find alternative solutions. + +## Unresolved questions + +* Where would the build tool changes be implemented? And how? +* How should the build tool changes interact with "pods" and `template.hbs`? +* How should the new generator options interact with "pods"? From df337d41f236cfb264d1a802b976c00d07ca0d5e Mon Sep 17 00:00:00 2001 From: Ricardo Mendes Date: Tue, 23 Apr 2019 11:12:15 +0100 Subject: [PATCH 2/8] Update 0481-component-templates-co-location.md --- text/0481-component-templates-co-location.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0481-component-templates-co-location.md b/text/0481-component-templates-co-location.md index 2f8caef250..870310ca80 100644 --- a/text/0481-component-templates-co-location.md +++ b/text/0481-component-templates-co-location.md @@ -66,7 +66,7 @@ app ## Motivation Today, a component's JavaScript file is located in the `app/components` -directory. On the other hand, it's template is located in the +directory. On the other hand, its template is located in the `app/templates/components` directory. This design decision is a relic of Ember's pre-1.0 days. In those early days, From 28004444f743207170f817ce146d4c71fabedd1e Mon Sep 17 00:00:00 2001 From: Keith Gautreaux Date: Wed, 24 Apr 2019 18:40:50 -0700 Subject: [PATCH 3/8] Fix 'tempalte' typos (#483) --- text/0481-component-templates-co-location.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/0481-component-templates-co-location.md b/text/0481-component-templates-co-location.md index 870310ca80..5dc4ff458b 100644 --- a/text/0481-component-templates-co-location.md +++ b/text/0481-component-templates-co-location.md @@ -346,9 +346,9 @@ class Bat {} setComponentTemplate(precompile("foo template"), Foo); setComponentTemplate(precompile("baz template"), Baz); -getComponentTemplate(Foo); // => foo tempalte -getComponentTemplate(Bar); // => foo tempalte (inherited) -getComponentTemplate(Baz); // => baz tempalte (overridden) +getComponentTemplate(Foo); // => foo template +getComponentTemplate(Bar); // => foo template (inherited) +getComponentTemplate(Baz); // => baz template (overridden) getComponentTemplate(Bat); // => undefined ``` From 7c8dce67215ae237b01956eb32014ab0284a62be Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Wed, 24 Apr 2019 19:45:22 -0700 Subject: [PATCH 4/8] setComponentTemplate takes a factory --- text/0481-component-templates-co-location.md | 53 ++++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/text/0481-component-templates-co-location.md b/text/0481-component-templates-co-location.md index 5dc4ff458b..0bb1f8fcec 100644 --- a/text/0481-component-templates-co-location.md +++ b/text/0481-component-templates-co-location.md @@ -143,7 +143,7 @@ template-only components, it is possible to define a component without a class, therefore a casual scan of `app/components` will not discover those components. With the release of Octane, we expect template-only components to become more common. Likewise, albeit less common, it is also possible to define a component -with *only* a class, making `app/templates/components` potentially incomplete +with _only_ a class, making `app/templates/components` potentially incomplete also. Therefore, the "correct" way to explore the components in an app is to mentally @@ -192,7 +192,7 @@ example: ```js // app/components/foo-bar.js -import MyParentComponent from './my-parent'; +import MyParentComponent from "./my-parent"; export default class FooBarComponent extends MyParentComponent { // ... @@ -212,8 +212,8 @@ in an addon, this is the default output from the blueprint: ```js // addon/components/foo-bar.js -import Component from '@ember/component'; -import layout from '../templates/components/foo-bar'; +import Component from "@ember/component"; +import layout from "../templates/components/foo-bar"; export default Component.extend({ layout @@ -229,7 +229,7 @@ export default Component.extend({ ```js // app/components/foo-bar.js -export { default } from 'my-addon/components/foo-bar'; +export { default } from "my-addon/components/foo-bar"; ``` First, within the `addon` folder, there is the component class and template. @@ -305,14 +305,14 @@ associated with the component's JavaScript class at build time. We propose to introduce the following low-level APIs: -* The `setComponentTemplate` function takes two arguments, the first being the +- The `setComponentTemplate` function takes two arguments, the first being the pre-compiled (wire-format) template, the second being the component class. It transparently associates the given template with the component class in a way can be retrieved later with the `getComponentTemplate` function described below. For convenience, `setComponentTemplate` will return the component class (the second argument). -* The `getComponentTemplate` function takes a component class and returns the +- The `getComponentTemplate` function takes a component class and returns the template associated with the given component class, if any, or one of its superclasses, if any, or `undefined` if no template association was found. @@ -340,11 +340,10 @@ class Baz extends Bar {} class Bat {} - // USAGE -setComponentTemplate(precompile("foo template"), Foo); -setComponentTemplate(precompile("baz template"), Baz); +setComponentTemplate(template("foo template"), Foo); +setComponentTemplate(template("baz template"), Baz); getComponentTemplate(Foo); // => foo template getComponentTemplate(Bar); // => foo template (inherited) @@ -381,7 +380,7 @@ files on disk: ```js // app/components/foo-bar.js -import Component from '@ember/component'; +import Component from "@ember/component"; export default Component.extend({ // ... @@ -399,14 +398,14 @@ The build output will be something to the effect of this JavaScript file: ```js // app/components/foo-bar.js -import Component, { setComponentTemplate } from '@ember/component'; +import Component, { setComponentTemplate } from "@ember/component"; -// output of precompile("foo bar!") -const TEMPLATE = { +// output of compiling "foo bar!" with ember-cli-htmlbars +const TEMPLATE = Ember.HTMLBars.template({ id: "...", block: "...", meta: { moduleName: "app/components/foo-bar" } -}; +}); const CLASS = Component.extend({ // ... @@ -425,7 +424,7 @@ that is unique to that file. For example, this should be **avoided**: ```js // app/components/foo-bar.js -import MyParentComponent from './my-parent'; +import MyParentComponent from "./my-parent"; // BAD: don't do this! export default MyParentComponent; @@ -439,7 +438,7 @@ required: ```js // app/components/foo-bar.js -import MyParentComponent from './my-parent'; +import MyParentComponent from "./my-parent"; // GOOD: do this instead! export default class extends MyParentComponent {} @@ -452,7 +451,7 @@ Most cases of this problem can be linted against easily. For template-only components, we propose to introduce the following low-level API: -* The `templateOnlyComponent` function takes no arguments and produces a unique +- The `templateOnlyComponent` function takes no arguments and produces a unique value that can be used to represent a template-only component. Again, this function, though public, is not intended to be called by users @@ -463,15 +462,15 @@ JavaScript file, the build output will be something similar to the following: ```js // app/components/foo-bar.js -import Component, { setComponentTemplate } from '@ember/component'; -import templateOnlyComponent from '@ember/component/template-only'; +import Component, { setComponentTemplate } from "@ember/component"; +import templateOnlyComponent from "@ember/component/template-only"; -// output of precompile("foo bar!") -const TEMPLATE = { +// output of compiling "foo bar!" with ember-cli-htmlbars +const TEMPLATE = Ember.HTMLBars.template({ id: "...", block: "...", meta: { moduleName: "app/components/foo-bar" } -}; +}); const CLASS = templateOnlyComponent(); @@ -621,7 +620,7 @@ Another drawback is that it only address the co-location issue for components, not other related types like [[route, controller, route template]] and [[model, adapter, serializer]], or even co-location of tests. However, we believe the situation with components is unique enough (see the motivation section) that -they are not merely *related*, but *coupled*. That, along with the fact that +they are not merely _related_, but _coupled_. That, along with the fact that components are much more common, sets them apart from the rest and justifies solving the problem first. @@ -642,6 +641,6 @@ some of the Octane features until we find alternative solutions. ## Unresolved questions -* Where would the build tool changes be implemented? And how? -* How should the build tool changes interact with "pods" and `template.hbs`? -* How should the new generator options interact with "pods"? +- Where would the build tool changes be implemented? And how? +- How should the build tool changes interact with "pods" and `template.hbs`? +- How should the new generator options interact with "pods"? From b9d3e08206e1eca05d1d4abd62e5bbc58f21459e Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Wed, 24 Apr 2019 19:47:50 -0700 Subject: [PATCH 5/8] update alias --- text/0481-component-templates-co-location.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0481-component-templates-co-location.md b/text/0481-component-templates-co-location.md index 0bb1f8fcec..ba1ce51a33 100644 --- a/text/0481-component-templates-co-location.md +++ b/text/0481-component-templates-co-location.md @@ -513,7 +513,7 @@ change. 1. It should accept a `--component-class` option. This controls which base class is used for that component and whether native classes are used. The legal values for this option are `@glimmer/component` (aliased as `-gc`), - `@ember/component` (aliased as `-ec`), `@ember/component/template-only` + `@ember/component` (aliased as `-cc`), `@ember/component/template-only` (aliased as `-tc`) or an empty string (aliased as `--no-component-class` and `-nc`). From 45bf52f0407737b3648ac5b760e4b170593c666c Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Fri, 26 Apr 2019 23:34:06 -0700 Subject: [PATCH 6/8] Remove Octane references --- text/0481-component-templates-co-location.md | 47 -------------------- 1 file changed, 47 deletions(-) diff --git a/text/0481-component-templates-co-location.md b/text/0481-component-templates-co-location.md index ba1ce51a33..caba6caa5d 100644 --- a/text/0481-component-templates-co-location.md +++ b/text/0481-component-templates-co-location.md @@ -554,39 +554,6 @@ change. that defaults to `--no-component-class` and `--component-structure=flat`. The guides and documentation will assume these settings going forward. -### Timing - -We propose to include this change as part of the Octane edition. Admittedly, -this is a rather late addition. However, there are a few compelling reasons for -including it: - -1. Module Unification was a much-anticipated feature that - [did not make it](https://blog.emberjs.com/2019/03/11/update-on-module-unification-and-octane.html) - into the Octane edition. Even though it tries to solve a lot of other - problems, component co-location is one of the main benefits. This proposal - allows us to incrementally deliver a solution one of the biggest pain points - while we work out the other issues. - -2. The Octane edition will enable template-only components by default, which - will encourage its use and intensifies some of the issues enumerated in the - motivation section. - -3. The Octane edition will highlight Glimmer Components as the main programming - model. Without the `setComponentTemplate` API proposed in this RFC, addons - authors will not be able to adopt Glimmer Components. This will, over time, - significantly increase the barrier to authoring addons, as most Ember - developers will no longer be familiar with the classic components API. - -4. The Octane edition presents a good opportunity to update the documentation - and get the whole community on board with adopting a feature like this. It - is important that build tools, linters, codemods, etc take this feature into - account, and shipping this as part of the Octane edition is a good way to - signal that.x - -Even though it is pretty late in the Octane timeframe, we believe this feature -is relatively limited in scope (compared to, say, `@tracked`) and can be -implemented relatively quickly. - ## How we teach this As mentioned above, the learning team will update the guides and API docs to @@ -609,13 +576,6 @@ would come at a later time than components. ## Drawbacks -Unlike the rest of the Octane features, this is not as battle-tested. Also, as -mentioned above, this addition would be quite last-minute. However, it is quite -normal to discover minor problems (e.g. issues with using Glimmer components in -addons today) through real-world usage, and that is precisely the point of the -Octane preview period. Since Octane is all about polish and cohesion, we think -it is worth addressing these issues before stamping on the quality seal. - Another drawback is that it only address the co-location issue for components, not other related types like [[route, controller, route template]] and [[model, adapter, serializer]], or even co-location of tests. However, we believe the @@ -632,13 +592,6 @@ structure of that collection will closely match what is proposed in this RFC. Ultimately, we expect there to be automatic migrators for these kinds of changes anyway, so the cost of the possible churn is contained. -## Alternatives - -We can forgo the opportunity to address these problems as part of Octane, and -instead land this proposal (or something like it) after the edition has -shipped. That would come at a cost of addon authors not being able to adopt -some of the Octane features until we find alternative solutions. - ## Unresolved questions - Where would the build tool changes be implemented? And how? From ac2c35fe556091068717ddc185ee5e2a539d239a Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Fri, 3 May 2019 03:16:20 -0500 Subject: [PATCH 7/8] apply learning team suggestions --- text/0481-component-templates-co-location.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/text/0481-component-templates-co-location.md b/text/0481-component-templates-co-location.md index caba6caa5d..37f7356b26 100644 --- a/text/0481-component-templates-co-location.md +++ b/text/0481-component-templates-co-location.md @@ -556,8 +556,19 @@ change. ## How we teach this -As mentioned above, the learning team will update the guides and API docs to -assume the "flat" co-located layout. +As mentioned above, we will update the learning resources to assume the "flat" +co-located layout. Throughout the Ember Guides, the Tutorials, and the CLI +Addon Tutorial, we would update the file paths in all component examples. Since +we assume the classic layout today, in most cases only the template paths would +need to be updated. The prose describing the location of files would also need +to change. + +The API documentation will describe the full set of file layout options +supported in the major version of Ember. A section should also be added to the +CLI guides, which describes how to properly import components from addons that +have a mix of layout types. We will not cover blending file layouts in the +Ember Guides, since they represent the happy path for an app, but we could link +to the CLI Guides explanation. With this layout, it should be much easier for new users to form a mental model around components. We can start by teaching that the most basic component is @@ -571,8 +582,8 @@ not necessary to separate out the topic of "templates" from "components" in the guides, but this can be addressed in a future revision of the guides. We will teach that the `templates` folder is used for route templates. This can -be introduced at the same time as the `routes` and `controllers` folder, which -would come at a later time than components. +be introduced in the Guides at the same time as the `routes` and `controllers` +folders/topics, which come later in the Table of Contents. ## Drawbacks From b3abb51e8e11fd237d236d48edf67ea0f93ea8bc Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Fri, 3 May 2019 03:23:55 -0500 Subject: [PATCH 8/8] remove unresolved questions --- text/0481-component-templates-co-location.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/text/0481-component-templates-co-location.md b/text/0481-component-templates-co-location.md index 37f7356b26..c9f6e89ac8 100644 --- a/text/0481-component-templates-co-location.md +++ b/text/0481-component-templates-co-location.md @@ -541,6 +541,9 @@ change. addons, when used with `--component-class=@ember/component`, this will also emit the `layout` property workaround. + When this option is set to `pods` (aliased as `--pods`), it will generate + `{app,addon}/components/$name/{component.js,template.hbs}`. + 3. Thses options will default to `--component-class=@ember/component` and `--component-structure=classic` for backwards compatibility. @@ -602,9 +605,3 @@ components will end up on disk (e.g. `src/ui/components`), the internal structure of that collection will closely match what is proposed in this RFC. Ultimately, we expect there to be automatic migrators for these kinds of changes anyway, so the cost of the possible churn is contained. - -## Unresolved questions - -- Where would the build tool changes be implemented? And how? -- How should the build tool changes interact with "pods" and `template.hbs`? -- How should the new generator options interact with "pods"?