From af27ce6c1f4c7f7f3de6327cf2542b737d8f2fd6 Mon Sep 17 00:00:00 2001 From: Ricardo Mendes Date: Mon, 15 Oct 2018 09:38:10 +0100 Subject: [PATCH] v3.6.0 with array helper --- .../managing-dependencies.md | 159 ++++ .../applications-and-instances.md | 18 + .../applications/dependency-injection.md | 260 ++++++ guides/v3.6.0/applications/initializers.md | 129 +++ guides/v3.6.0/applications/run-loop.md | 269 ++++++ guides/v3.6.0/applications/services.md | 142 ++++ guides/v3.6.0/components/block-params.md | 52 ++ .../customizing-a-components-element.md | 205 +++++ .../v3.6.0/components/defining-a-component.md | 122 +++ guides/v3.6.0/components/handling-events.md | 176 ++++ .../passing-properties-to-a-component.md | 119 +++ .../components/the-component-lifecycle.md | 280 +++++++ .../triggering-changes-with-actions.md | 492 +++++++++++ .../wrapping-content-in-a-component.md | 116 +++ .../v3.6.0/configuring-ember/build-targets.md | 74 ++ .../configuring-ember-cli.md | 12 + .../configuring-ember/configuring-your-app.md | 19 + guides/v3.6.0/configuring-ember/debugging.md | 145 ++++ .../disabling-prototype-extensions.md | 164 ++++ .../embedding-applications.md | 96 +++ .../v3.6.0/configuring-ember/feature-flags.md | 62 ++ .../handling-deprecations.md | 145 ++++ .../configuring-ember/specifying-url-type.md | 45 + .../contributing/adding-new-features.md | 163 ++++ guides/v3.6.0/contributing/repositories.md | 59 ++ guides/v3.6.0/controllers/index.md | 107 +++ .../v3.6.0/ember-inspector/component-tree.md | 50 ++ guides/v3.6.0/ember-inspector/container.md | 19 + guides/v3.6.0/ember-inspector/data.md | 39 + guides/v3.6.0/ember-inspector/deprecations.md | 40 + guides/v3.6.0/ember-inspector/index.md | 7 + guides/v3.6.0/ember-inspector/info.md | 17 + guides/v3.6.0/ember-inspector/installation.md | 82 ++ .../ember-inspector/object-inspector.md | 97 +++ guides/v3.6.0/ember-inspector/promises.md | 86 ++ .../ember-inspector/render-performance.md | 20 + guides/v3.6.0/ember-inspector/routes.md | 24 + .../v3.6.0/ember-inspector/troubleshooting.md | 67 ++ guides/v3.6.0/ember-inspector/view-tree.md | 63 ++ .../v3.6.0/getting-started/core-concepts.md | 83 ++ guides/v3.6.0/getting-started/index.md | 62 ++ guides/v3.6.0/getting-started/js-primer.md | 226 +++++ guides/v3.6.0/getting-started/quick-start.md | 297 +++++++ guides/v3.6.0/glossary/web-development.md | 91 ++ guides/v3.6.0/index.md | 117 +++ .../creating-updating-and-deleting-records.md | 168 ++++ guides/v3.6.0/models/customizing-adapters.md | 295 +++++++ .../v3.6.0/models/customizing-serializers.md | 784 ++++++++++++++++++ guides/v3.6.0/models/defining-models.md | 160 ++++ guides/v3.6.0/models/finding-records.md | 124 +++ guides/v3.6.0/models/handling-metadata.md | 87 ++ guides/v3.6.0/models/index.md | 366 ++++++++ .../models/pushing-records-into-the-store.md | 152 ++++ guides/v3.6.0/models/relationships.md | 482 +++++++++++ guides/v3.6.0/object-model/bindings.md | 74 ++ .../object-model/classes-and-instances.md | 267 ++++++ .../computed-properties-and-aggregate-data.md | 237 ++++++ .../object-model/computed-properties.md | 254 ++++++ guides/v3.6.0/object-model/enumerables.md | 209 +++++ guides/v3.6.0/object-model/index.md | 21 + guides/v3.6.0/object-model/observers.md | 155 ++++ .../reopening-classes-and-instances.md | 46 + guides/v3.6.0/pages.yml | 268 ++++++ guides/v3.6.0/routing/asynchronous-routing.md | 173 ++++ guides/v3.6.0/routing/defining-your-routes.md | 288 +++++++ guides/v3.6.0/routing/index.md | 21 + .../routing/loading-and-error-substates.md | 252 ++++++ .../preventing-and-retrying-transitions.md | 108 +++ guides/v3.6.0/routing/query-params.md | 345 ++++++++ guides/v3.6.0/routing/redirection.md | 114 +++ guides/v3.6.0/routing/rendering-a-template.md | 34 + .../routing/specifying-a-routes-model.md | 226 +++++ guides/v3.6.0/templates/actions.md | 174 ++++ .../templates/binding-element-attributes.md | 76 ++ guides/v3.6.0/templates/built-in-helpers.md | 88 ++ guides/v3.6.0/templates/conditionals.md | 92 ++ .../v3.6.0/templates/development-helpers.md | 59 ++ .../templates/displaying-a-list-of-items.md | 79 ++ .../displaying-the-keys-in-an-object.md | 76 ++ guides/v3.6.0/templates/handlebars-basics.md | 95 +++ guides/v3.6.0/templates/input-helpers.md | 115 +++ guides/v3.6.0/templates/links.md | 163 ++++ guides/v3.6.0/templates/writing-helpers.md | 400 +++++++++ guides/v3.6.0/testing/acceptance.md | 151 ++++ guides/v3.6.0/testing/index.md | 211 +++++ guides/v3.6.0/testing/testing-components.md | 523 ++++++++++++ guides/v3.6.0/testing/testing-controllers.md | 83 ++ guides/v3.6.0/testing/testing-helpers.md | 99 +++ guides/v3.6.0/testing/testing-models.md | 112 +++ guides/v3.6.0/testing/testing-routes.md | 91 ++ guides/v3.6.0/testing/unit-testing-basics.md | 185 +++++ guides/v3.6.0/tutorial/acceptance-test.md | 120 +++ .../v3.6.0/tutorial/autocomplete-component.md | 578 +++++++++++++ guides/v3.6.0/tutorial/deploying.md | 94 +++ guides/v3.6.0/tutorial/ember-cli.md | 142 ++++ guides/v3.6.0/tutorial/ember-data.md | 144 ++++ guides/v3.6.0/tutorial/hbs-helper.md | 127 +++ guides/v3.6.0/tutorial/installing-addons.md | 160 ++++ guides/v3.6.0/tutorial/model-hook.md | 128 +++ .../v3.6.0/tutorial/routes-and-templates.md | 383 +++++++++ guides/v3.6.0/tutorial/service.md | 430 ++++++++++ guides/v3.6.0/tutorial/simple-component.md | 395 +++++++++ guides/v3.6.0/tutorial/subroutes.md | 396 +++++++++ 103 files changed, 16796 insertions(+) create mode 100644 guides/v3.6.0/addons-and-dependencies/managing-dependencies.md create mode 100644 guides/v3.6.0/applications/applications-and-instances.md create mode 100644 guides/v3.6.0/applications/dependency-injection.md create mode 100644 guides/v3.6.0/applications/initializers.md create mode 100644 guides/v3.6.0/applications/run-loop.md create mode 100644 guides/v3.6.0/applications/services.md create mode 100644 guides/v3.6.0/components/block-params.md create mode 100644 guides/v3.6.0/components/customizing-a-components-element.md create mode 100644 guides/v3.6.0/components/defining-a-component.md create mode 100644 guides/v3.6.0/components/handling-events.md create mode 100644 guides/v3.6.0/components/passing-properties-to-a-component.md create mode 100644 guides/v3.6.0/components/the-component-lifecycle.md create mode 100644 guides/v3.6.0/components/triggering-changes-with-actions.md create mode 100644 guides/v3.6.0/components/wrapping-content-in-a-component.md create mode 100644 guides/v3.6.0/configuring-ember/build-targets.md create mode 100644 guides/v3.6.0/configuring-ember/configuring-ember-cli.md create mode 100644 guides/v3.6.0/configuring-ember/configuring-your-app.md create mode 100644 guides/v3.6.0/configuring-ember/debugging.md create mode 100644 guides/v3.6.0/configuring-ember/disabling-prototype-extensions.md create mode 100644 guides/v3.6.0/configuring-ember/embedding-applications.md create mode 100644 guides/v3.6.0/configuring-ember/feature-flags.md create mode 100644 guides/v3.6.0/configuring-ember/handling-deprecations.md create mode 100644 guides/v3.6.0/configuring-ember/specifying-url-type.md create mode 100644 guides/v3.6.0/contributing/adding-new-features.md create mode 100644 guides/v3.6.0/contributing/repositories.md create mode 100644 guides/v3.6.0/controllers/index.md create mode 100644 guides/v3.6.0/ember-inspector/component-tree.md create mode 100644 guides/v3.6.0/ember-inspector/container.md create mode 100644 guides/v3.6.0/ember-inspector/data.md create mode 100644 guides/v3.6.0/ember-inspector/deprecations.md create mode 100644 guides/v3.6.0/ember-inspector/index.md create mode 100644 guides/v3.6.0/ember-inspector/info.md create mode 100644 guides/v3.6.0/ember-inspector/installation.md create mode 100644 guides/v3.6.0/ember-inspector/object-inspector.md create mode 100644 guides/v3.6.0/ember-inspector/promises.md create mode 100644 guides/v3.6.0/ember-inspector/render-performance.md create mode 100644 guides/v3.6.0/ember-inspector/routes.md create mode 100644 guides/v3.6.0/ember-inspector/troubleshooting.md create mode 100644 guides/v3.6.0/ember-inspector/view-tree.md create mode 100644 guides/v3.6.0/getting-started/core-concepts.md create mode 100644 guides/v3.6.0/getting-started/index.md create mode 100644 guides/v3.6.0/getting-started/js-primer.md create mode 100644 guides/v3.6.0/getting-started/quick-start.md create mode 100644 guides/v3.6.0/glossary/web-development.md create mode 100644 guides/v3.6.0/index.md create mode 100644 guides/v3.6.0/models/creating-updating-and-deleting-records.md create mode 100644 guides/v3.6.0/models/customizing-adapters.md create mode 100644 guides/v3.6.0/models/customizing-serializers.md create mode 100644 guides/v3.6.0/models/defining-models.md create mode 100644 guides/v3.6.0/models/finding-records.md create mode 100644 guides/v3.6.0/models/handling-metadata.md create mode 100644 guides/v3.6.0/models/index.md create mode 100644 guides/v3.6.0/models/pushing-records-into-the-store.md create mode 100644 guides/v3.6.0/models/relationships.md create mode 100644 guides/v3.6.0/object-model/bindings.md create mode 100644 guides/v3.6.0/object-model/classes-and-instances.md create mode 100644 guides/v3.6.0/object-model/computed-properties-and-aggregate-data.md create mode 100644 guides/v3.6.0/object-model/computed-properties.md create mode 100644 guides/v3.6.0/object-model/enumerables.md create mode 100644 guides/v3.6.0/object-model/index.md create mode 100644 guides/v3.6.0/object-model/observers.md create mode 100644 guides/v3.6.0/object-model/reopening-classes-and-instances.md create mode 100644 guides/v3.6.0/pages.yml create mode 100644 guides/v3.6.0/routing/asynchronous-routing.md create mode 100644 guides/v3.6.0/routing/defining-your-routes.md create mode 100644 guides/v3.6.0/routing/index.md create mode 100644 guides/v3.6.0/routing/loading-and-error-substates.md create mode 100644 guides/v3.6.0/routing/preventing-and-retrying-transitions.md create mode 100644 guides/v3.6.0/routing/query-params.md create mode 100644 guides/v3.6.0/routing/redirection.md create mode 100644 guides/v3.6.0/routing/rendering-a-template.md create mode 100644 guides/v3.6.0/routing/specifying-a-routes-model.md create mode 100644 guides/v3.6.0/templates/actions.md create mode 100644 guides/v3.6.0/templates/binding-element-attributes.md create mode 100644 guides/v3.6.0/templates/built-in-helpers.md create mode 100644 guides/v3.6.0/templates/conditionals.md create mode 100644 guides/v3.6.0/templates/development-helpers.md create mode 100644 guides/v3.6.0/templates/displaying-a-list-of-items.md create mode 100644 guides/v3.6.0/templates/displaying-the-keys-in-an-object.md create mode 100644 guides/v3.6.0/templates/handlebars-basics.md create mode 100644 guides/v3.6.0/templates/input-helpers.md create mode 100644 guides/v3.6.0/templates/links.md create mode 100644 guides/v3.6.0/templates/writing-helpers.md create mode 100644 guides/v3.6.0/testing/acceptance.md create mode 100644 guides/v3.6.0/testing/index.md create mode 100644 guides/v3.6.0/testing/testing-components.md create mode 100644 guides/v3.6.0/testing/testing-controllers.md create mode 100644 guides/v3.6.0/testing/testing-helpers.md create mode 100644 guides/v3.6.0/testing/testing-models.md create mode 100644 guides/v3.6.0/testing/testing-routes.md create mode 100644 guides/v3.6.0/testing/unit-testing-basics.md create mode 100644 guides/v3.6.0/tutorial/acceptance-test.md create mode 100644 guides/v3.6.0/tutorial/autocomplete-component.md create mode 100644 guides/v3.6.0/tutorial/deploying.md create mode 100644 guides/v3.6.0/tutorial/ember-cli.md create mode 100644 guides/v3.6.0/tutorial/ember-data.md create mode 100644 guides/v3.6.0/tutorial/hbs-helper.md create mode 100644 guides/v3.6.0/tutorial/installing-addons.md create mode 100644 guides/v3.6.0/tutorial/model-hook.md create mode 100644 guides/v3.6.0/tutorial/routes-and-templates.md create mode 100644 guides/v3.6.0/tutorial/service.md create mode 100644 guides/v3.6.0/tutorial/simple-component.md create mode 100644 guides/v3.6.0/tutorial/subroutes.md diff --git a/guides/v3.6.0/addons-and-dependencies/managing-dependencies.md b/guides/v3.6.0/addons-and-dependencies/managing-dependencies.md new file mode 100644 index 0000000000..3497360b0a --- /dev/null +++ b/guides/v3.6.0/addons-and-dependencies/managing-dependencies.md @@ -0,0 +1,159 @@ +As you're developing your Ember app, you'll likely run into common scenarios that aren't addressed by Ember itself, +such as authentication or using SASS for your stylesheets. +Ember CLI provides a common format called [Ember Addons](#toc_addons) for distributing reusable libraries +to solve these problems. +Additionally, you may want to make use of front-end dependencies like a CSS framework +or a JavaScript datepicker that aren't specific to Ember apps. + +## Addons + +Ember Addons can be installed using [Ember CLI](http://ember-cli.com/extending/#developing-addons-and-blueprints) +(e.g. `ember install ember-cli-sass`). +Addons may bring in other dependencies by modifying your project's `package.json` file automatically. + +You can find listings of addons on [Ember Observer](http://emberobserver.com). + +## Regular npm packages + +While dependencies can be managed in several ways, +it's worth noting that the process can be greatly simplified for new developers by using ember-auto-import, +which offers zero config imports from npm packages. +It's easily installed using `ember install ember-auto-import`. +For further usage instructions, please follow the [project README](https://github.com/ef4/ember-auto-import). + +## Other assets + +Third-party JavaScript not available as an addon or npm package should be placed in the `vendor/` folder in your project. + +Your own assets (such as `robots.txt`, `favicon`, custom fonts, etc) should be placed in the `public/` folder in your project. + +## Compiling Assets + +When you're using dependencies that are not included in an addon, +you will have to instruct Ember CLI to include your assets in the build. +This is done using the asset manifest file `ember-cli-build.js`. +You should only try to import assets located in the `node_modules` and `vendor` folders. `bower_components` also still +works, but is recommended against, unless you have no other choice. Even bower recommends not to use itself anymore. + +### Globals provided by JavaScript assets + +The globals provided by some assets (like `moment` in the below example) can be used in your application +without the need to `import` them. +Provide the asset path as the first and only argument. + +```javascript {data-filename=ember-cli-build.js} +app.import('node_modules/moment/moment.js'); +``` + +You will need to add `"moment"` to the `globals` section in `.eslintrc.js` to prevent ESLint errors +about using an undefined variable. + +### Anonymous AMD JavaScript modules + +You can transform an anonymous AMD module to a named one by using the `amd` transformation. + +```javascript {data-filename=ember-cli-build.js} +app.import('node_modules/moment/moment.js', { + using: [ + { transformation: 'amd', as: 'moment' } + ] +}); +``` + +This transformation allows you to `import` moment in your app. (e.g. `import moment from 'moment';`) + +### CommonJS JavaScript modules + +[ember-cli-cjs-transform](https://github.com/rwjblue/ember-cli-cjs-transform) allows us to import CommonJS modules into +our Ember app. It also does auto-rollup and some nice caching, so it should pull in all the deps that are pulled in +with `require` for you automatically. It is not yet included with ember-cli by default, so you will need to install it. + +```bash +ember install ember-cli-cjs-transform +``` + +```javascript {data-filename=ember-cli-build.js} +app.import('node_modules/showdown/dist/showdown.js', { + using: [ + { transformation: 'cjs', as: 'showdown' } + ] +}); +``` + +You can now `import` them in your app. (e.g. `import showdown from 'showdown';`) + +### Environment-Specific Assets + +If you need to use different assets in different environments, specify an object as the first parameter. +That object's key should be the environment name, and the value should be the asset to use in that environment. + +```javascript {data-filename=ember-cli-build.js} +app.import({ + development: 'node_modules/moment/moment.js', + production: 'node_modules/moment/min/moment.min.js' +}); +``` + +If you need to import an asset in only one environment you can wrap `app.import` in an `if` statement. +For assets needed during testing, you should also use the `{type: 'test'}` option to make sure they +are available in test mode. + +```javascript {data-filename=ember-cli-build.js} +if (app.env === 'development') { + // Only import when in development mode + app.import('vendor/ember-renderspeed/ember-renderspeed.js'); +} +if (app.env === 'test') { + // Only import in test mode and place in test-support.js + app.import('node_modules/sinonjs/sinon.js', { type: 'test' }); + app.import('node_modules/sinon-qunit/lib/sinon-qunit.js', { type: 'test' }); +} +``` + +### CSS + +Provide the asset path as the first argument: + +```javascript {data-filename=ember-cli-build.js} +app.import('node_modules/foundation/css/foundation.css'); +``` + +All style assets added this way will be concatenated and output as `/assets/vendor.css`. + +### Other Assets + +All assets located in the `public/` folder will be copied as is to the final output directory, `dist/`. + +For example, a `favicon` located at `public/images/favicon.ico` will be copied to `dist/images/favicon.ico`. + +All third-party assets, included either manually in `vendor/` or via a package manager like npm, must be added via `import()`. + +Third-party assets that are not added via `import()` will not be present in the final build. + +By default, `import`ed assets will be copied to `dist/` as they are, with the existing directory structure maintained. + +```javascript {data-filename=ember-cli-build.js} +app.import('node_modules/font-awesome/fonts/fontawesome-webfont.ttf'); +``` + +This example would create the font file in `dist/font-awesome/fonts/fontawesome-webfont.ttf`. + +You can also optionally tell `import()` to place the file at a different path. +The following example will copy the file to `dist/assets/fontawesome-webfont.ttf`. + +```javascript {data-filename=ember-cli-build.js} +app.import('node_modules/font-awesome/fonts/fontawesome-webfont.ttf', { + destDir: 'assets' +}); +``` + +If you need to load certain dependencies before others, +you can set the `prepend` property equal to `true` on the second argument of `import()`. +This will prepend the dependency to the vendor file instead of appending it, which is the default behavior. + +```javascript {data-filename=ember-cli-build.js} +app.import('node_modules/es5-shim/es5-shim.js', { + type: 'vendor', + prepend: true +}); +``` diff --git a/guides/v3.6.0/applications/applications-and-instances.md b/guides/v3.6.0/applications/applications-and-instances.md new file mode 100644 index 0000000000..95aacbf3c5 --- /dev/null +++ b/guides/v3.6.0/applications/applications-and-instances.md @@ -0,0 +1,18 @@ +Every Ember application is represented by a class that extends [`Application`](https://emberjs.com/api/ember/release/classes/Application). +This class is used to declare and configure the many objects that make up your app. + +As your application boots, +it creates an [`ApplicationInstance`](https://emberjs.com/api/ember/release/classes/ApplicationInstance) that is used to manage its stateful aspects. +This instance acts as the "owner" of objects instantiated for your app. + +Essentially, the `Application` *defines your application* +while the `ApplicationInstance` *manages its state*. + +This separation of concerns not only clarifies the architecture of your app, +it can also improve its efficiency. +This is particularly true when your app needs to be booted repeatedly during testing +and / or server-rendering (e.g. via [FastBoot](https://github.com/tildeio/ember-cli-fastboot)). +The configuration of a single `Application` can be done once +and shared among multiple stateful `ApplicationInstance` instances. +These instances can be discarded once they're no longer needed +(e.g. when a test has run or FastBoot request has finished). diff --git a/guides/v3.6.0/applications/dependency-injection.md b/guides/v3.6.0/applications/dependency-injection.md new file mode 100644 index 0000000000..b3f07c942d --- /dev/null +++ b/guides/v3.6.0/applications/dependency-injection.md @@ -0,0 +1,260 @@ +Ember applications utilize the [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) +("DI") design pattern to declare and instantiate classes of objects and dependencies between them. +Applications and application instances each serve a role in Ember's DI implementation. + +An [`Application`](https://emberjs.com/api/ember/release/classes/Application) serves as a "registry" for dependency declarations. +Factories (i.e. classes) are registered with an application, +as well as rules about "injecting" dependencies that are applied when objects are instantiated. + +An [`ApplicationInstance`](https://emberjs.com/api/ember/release/classes/ApplicationInstance) serves as the "owner" for objects that are instantiated from registered factories. +Application instances provide a means to "look up" (i.e. instantiate and / or retrieve) objects. + +> _Note: Although an `Application` serves as the primary registry for an app, +each `ApplicationInstance` can also serve as a registry. +Instance-level registrations are useful for providing instance-level customizations, +such as A/B testing of a feature._ + +## Factory Registrations + +A factory can represent any part of your application, like a _route_, _template_, or custom class. +Every factory is registered with a particular key. +For example, the index template is registered with the key `template:index`, +and the application route is registered with the key `route:application`. + +Registration keys have two segments split by a colon (`:`). +The first segment is the framework factory type, and the second is the name of the particular factory. +Hence, the `index` template has the key `template:index`. +Ember has several built-in factory types, such as `service`, `route`, `template`, and `component`. + +You can create your own factory type by simply registering a factory with the new type. +For example, to create a `user` type, +you'd simply register your factory with `application.register('user:user-to-register')`. + +Factory registrations must be performed either in application +or application instance initializers (with the former being much more common). + +For example, an application initializer could register a `Logger` factory with the key `logger:main`: + +```javascript {data-filename=app/initializers/logger.js} +import EmberObject from '@ember/object'; + +export function initialize(application) { + let Logger = EmberObject.extend({ + log(m) { + console.log(m); + } + }); + + application.register('logger:main', Logger); +} + +export default { + name: 'logger', + initialize: initialize +}; +``` + +### Registering Already Instantiated Objects + +By default, Ember will attempt to instantiate a registered factory when it is looked up. +When registering an already instantiated object instead of a class, +use the `instantiate: false` option to avoid attempts to re-instantiate it during lookups. + +In the following example, the `logger` is a plain JavaScript object that should +be returned "as is" when it's looked up: + +```javascript {data-filename=app/initializers/logger.js} +export function initialize(application) { + let logger = { + log(m) { + console.log(m); + } + }; + + application.register('logger:main', logger, { instantiate: false }); +} + +export default { + name: 'logger', + initialize: initialize +}; +``` + +### Registering Singletons vs. Non-Singletons + +By default, registrations are treated as "singletons". +This simply means that an instance will be created when it is first looked up, +and this same instance will be cached and returned from subsequent lookups. + +When you want fresh objects to be created for every lookup, +register your factories as non-singletons using the `singleton: false` option. + +In the following example, the `Message` class is registered as a non-singleton: + +```javascript {data-filename=app/initializers/notification.js} +import EmberObject from '@ember/object'; + +export function initialize(application) { + let Message = EmberObject.extend({ + text: '' + }); + + application.register('notification:message', Message, { singleton: false }); +} + +export default { + name: 'notification', + initialize: initialize +}; +``` + +## Factory Injections + +Once a factory is registered, it can be "injected" where it is needed. + +Factories can be injected into whole "types" of factories with *type injections*. For example: + +```javascript {data-filename=app/initializers/logger.js} +import EmberObject from '@ember/object'; + +export function initialize(application) { + let Logger = EmberObject.extend({ + log(m) { + console.log(m); + } + }); + + application.register('logger:main', Logger); + application.inject('route', 'logger', 'logger:main'); +} + +export default { + name: 'logger', + initialize: initialize +}; +``` + +As a result of this type injection, +all factories of the type `route` will be instantiated with the property `logger` injected. +The value of `logger` will come from the factory named `logger:main`. + +Routes in this example application can now access the injected logger: + +```javascript {data-filename=app/routes/index.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + activate() { + // The logger property is injected into all routes + this.logger.log('Entered the index route!'); + } +}); +``` + +Injections can also be made on a specific factory by using its full key: + +```javascript +application.inject('route:index', 'logger', 'logger:main'); +``` + +In this case, the logger will only be injected on the index route. + +Injections can be made into any class that requires instantiation. +This includes all of Ember's major framework classes, such as components, helpers, routes, and the router. + +### Ad Hoc Injections + +Dependency injections can also be declared directly on Ember classes using `inject`. +Currently, `inject` supports injecting controllers (via `import { inject } from '@ember/controller';`) +and services (via `import { inject } from '@ember/service';`). + +The following code injects the `shopping-cart` service on the `cart-contents` component as the property `cart`: + +```javascript {data-filename=app/components/cart-contents.js} +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; + +export default Component.extend({ + cart: service('shopping-cart') +}); +``` + +If you'd like to inject a service with the same name as the property, +simply leave off the service name (the dasherized version of the name will be used): + +```javascript {data-filename=app/components/cart-contents.js} +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; + +export default Component.extend({ + shoppingCart: service() +}); +``` + +## Factory Instance Lookups + +To fetch an instantiated factory from the running application you can call the +[`lookup`](https://emberjs.com/api/ember/release/classes/ApplicationInstance/methods/lookup?anchor=lookup) method on an application instance. This method takes a string +to identify a factory and returns the appropriate object. + +```javascript +applicationInstance.lookup('factory-type:factory-name'); +``` + +The application instance is passed to Ember's instance initializer hooks and it +is added as the "owner" of each object that was instantiated by the application +instance. + +### Using an Application Instance Within an Instance Initializer + +Instance initializers receive an application instance as an argument, providing +an opportunity to look up an instance of a registered factory. + +```javascript {data-filename=app/instance-initializers/logger.js} +export function initialize(applicationInstance) { + let logger = applicationInstance.lookup('logger:main'); + + logger.log('Hello from the instance initializer!'); +} + +export default { + name: 'logger', + initialize: initialize +}; +``` + +### Getting an Application Instance from a Factory Instance + +[`Ember.getOwner`](https://emberjs.com/api/ember/release/classes/@ember%2Fapplication/methods/getOwner?anchor=getOwner) will retrieve the application instance that "owns" an +object. This means that framework objects like components, helpers, and routes +can use [`Ember.getOwner`](https://emberjs.com/api/ember/release/classes/@ember%2Fapplication/methods/getOwner?anchor=getOwner) to perform lookups through their application +instance at runtime. + +For example, this component plays songs with different audio services based +on a song's `audioType`. + +```javascript {data-filename=app/components/play-audio.js} +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import { getOwner } from '@ember/application'; + +// Usage: +// +// {{play-audio song=song}} +// +export default Component.extend({ + audioService: computed('song.audioType', function() { + if (!this.song) { + return null; + } + let applicationInstance = getOwner(this); + let audioType = this.song.audioType; + return applicationInstance.lookup(`service:audio-${audioType}`); + }), + + click() { + let player = this.audioService; + player.play(this.song.file); + } +}); +``` diff --git a/guides/v3.6.0/applications/initializers.md b/guides/v3.6.0/applications/initializers.md new file mode 100644 index 0000000000..25ed730829 --- /dev/null +++ b/guides/v3.6.0/applications/initializers.md @@ -0,0 +1,129 @@ +Initializers provide an opportunity to configure your application as it boots. + +There are two types of initializers: application initializers and application instance initializers. + +Application initializers are run as your application boots, +and provide the primary means to configure [dependency injections](../dependency-injection/) in your application. + +Application instance initializers are run as an application instance is loaded. +They provide a way to configure the initial state of your application, +as well as to set up dependency injections that are local to the application instance +(e.g. A/B testing configurations). + +Operations performed in initializers should be kept as lightweight as possible +to minimize delays in loading your application. +Although advanced techniques exist for allowing asynchrony in application initializers +(i.e. `deferReadiness` and `advanceReadiness`), these techniques should generally be avoided. +Any asynchronous loading conditions (e.g. user authorization) are almost always +better handled in your application route's hooks, +which allows for DOM interaction while waiting for conditions to resolve. + +## Application Initializers + +Application initializers can be created with Ember CLI's `initializer` generator: + +```bash +ember generate initializer shopping-cart +``` + +Let's customize the `shopping-cart` initializer to inject a `cart` property into all the routes in your application: + +```javascript {data-filename=app/initializers/shopping-cart.js} +export function initialize(application) { + application.inject('route', 'cart', 'service:shopping-cart'); +}; + +export default { + initialize +}; +``` + +## Application Instance Initializers + +Application instance initializers can be created with Ember CLI's `instance-initializer` generator: + +```bash +ember generate instance-initializer logger +``` + +Let's add some simple logging to indicate that the instance has booted: + +```javascript {data-filename=app/instance-initializers/logger.js} +export function initialize(applicationInstance) { + let logger = applicationInstance.lookup('logger:main'); + logger.log('Hello from the instance initializer!'); +} + +export default { + initialize +}; +``` + +## Specifying Initializer Order + +If you'd like to control the order in which initializers run, you can use the `before` and/or `after` options: + +```javascript {data-filename=app/initializers/config-reader.js} +export function initialize(application) { + // ... your code ... +}; + +export default { + before: 'websocket-init', + initialize +}; +``` + +```javascript {data-filename=app/initializers/websocket-init.js} +export function initialize(application) { + // ... your code ... +}; + +export default { + after: 'config-reader', + initialize +}; +``` + +```javascript {data-filename=app/initializers/asset-init.js} +export function initialize(application) { + // ... your code ... +}; + +export default { + after: ['config-reader', 'websocket-init'], + initialize +}; +``` + +Note that ordering only applies to initializers of the same type (i.e. application or application instance). +Application initializers will always run before application instance initializers. + +## Customizing Initializer Names + +By default initializer names are derived from their module name. This initializer will be given the name `logger`: + +```javascript {data-filename=app/instance-initializers/logger.js} +export function initialize(applicationInstance) { + let logger = applicationInstance.lookup('logger:main'); + logger.log('Hello from the instance initializer!'); +} + +export default { initialize }; +``` + +If you want to change the name you can simply rename the file, but if needed you can also specify the name explicitly: + +```javascript {data-filename=app/instance-initializers/logger.js} +export function initialize(applicationInstance) { + let logger = applicationInstance.lookup('logger:main'); + logger.log('Hello from the instance initializer!'); +} + +export default { + name: 'my-logger', + initialize +}; +``` + +This initializer will now have the name `my-logger`. diff --git a/guides/v3.6.0/applications/run-loop.md b/guides/v3.6.0/applications/run-loop.md new file mode 100644 index 0000000000..ea760e4f24 --- /dev/null +++ b/guides/v3.6.0/applications/run-loop.md @@ -0,0 +1,269 @@ +**Note:** + * _For basic Ember app development scenarios, you don't need to understand the run loop or use it directly. All common paths are paved nicely for you and don't require working with the run loop._ + * _However, the run loop will be helpful to understand the internals of Ember and to assist in customized performance tuning by manually batching costly work._ + +Ember's internals and most of the code you will write in your applications takes place in a run loop. +The run loop is used to batch, and order (or reorder) work in a way that is most effective and efficient. + +It does so by scheduling work on specific queues. +These queues have a priority, and are processed to completion in priority order. + +The most common case for using the run loop is integrating with a non-Ember API +that includes some sort of asynchronous callback. +For example: + +- DOM update and event callbacks +- `setTimeout` and `setInterval` callbacks +- `postMessage` and `messageChannel` event handlers +- AJAX callbacks +- Websocket callbacks + +## Why is the run loop useful? + +Very often, batching similar work has benefits. +Web browsers do something quite similar by batching changes to the DOM. + +Consider the following HTML snippet: + +```html +
+
+
+``` + +and executing the following code: + +```javascript +foo.style.height = '500px' // write +foo.offsetHeight // read (recalculate style, layout, expensive!) + +bar.style.height = '400px' // write +bar.offsetHeight // read (recalculate style, layout, expensive!) + +baz.style.height = '200px' // write +baz.offsetHeight // read (recalculate style, layout, expensive!) +``` + +In this example, the sequence of code forced the browser to recalculate style, and relayout after each step. +However, if we were able to batch similar jobs together, +the browser would have only needed to recalculate the style and layout once. + +```javascript +foo.style.height = '500px' // write +bar.style.height = '400px' // write +baz.style.height = '200px' // write + +foo.offsetHeight // read (recalculate style, layout, expensive!) +bar.offsetHeight // read (fast since style and layout are already known) +baz.offsetHeight // read (fast since style and layout are already known) +``` + +Interestingly, this pattern holds true for many other types of work. +Essentially, batching similar work allows for better pipelining, and further optimization. + +Let's look at a similar example that is optimized in Ember, starting with a `User` object: + +```javascript +import EmberObject, { + computed +} from '@ember/object'; + +let User = EmberObject.extend({ + firstName: null, + lastName: null, + + fullName: computed('firstName', 'lastName', function() { + return `${this.firstName} ${this.lastName}`; + }) +}); +``` + +and a template to display its attributes: + +```handlebars +{{firstName}} +{{fullName}} +``` + +If we execute the following code without the run loop: + +```javascript +let user = User.create({ firstName: 'Tom', lastName: 'Huda' }); +user.set('firstName', 'Yehuda'); +// {{firstName}} and {{fullName}} are updated + +user.set('lastName', 'Katz'); +// {{lastName}} and {{fullName}} are updated +``` + +We see that the browser will rerender the template twice. + +However, if we have the run loop in the above code, +the browser will only rerender the template once the attributes have all been set. + +```javascript +let user = User.create({ firstName: 'Tom', lastName: 'Huda' }); +user.set('firstName', 'Yehuda'); +user.set('lastName', 'Katz'); +user.set('firstName', 'Tom'); +user.set('lastName', 'Huda'); +``` + +In the above example with the run loop, since the user's attributes end up at the same values as before execution, +the template will not even rerender! + +It is of course possible to optimize these scenarios on a case-by-case basis, +but getting them for free is much nicer. +Using the run loop, we can apply these classes of optimizations not only for each scenario, but holistically app-wide. + +## How does the Run Loop work in Ember? + +As mentioned earlier, we schedule work (in the form of function invocations) on queues, +and these queues are processed to completion in priority order. + +What are the queues, and what is their priority order? + +1. `actions` +2. `routerTransitions` +3. `render` +4. `afterRender` +5. `destroy` + +Here, in this list, the "actions" queue has a higher priority than the "render" or "destroy" queue. + +## What happens in these queues? + +* The `actions` queue is the general work queue and will typically contain scheduled tasks e.g. promises. +* The `routerTransitions` queue contains transition jobs in the router. +* The `render` queue contains jobs meant for rendering, these will typically update the DOM. +* The `afterRender` queue contains jobs meant to be run after all previously scheduled render tasks are complete. +This is often good for 3rd-party DOM manipulation libraries, +that should only be run after an entire tree of DOM has been updated. +* The `destroy` queue contains jobs to finish the teardown of objects other jobs have scheduled to destroy. + +## In what order are jobs executed on the queues? +The algorithm works this way: + +1. Let the highest priority queue with pending jobs be: `CURRENT_QUEUE`, +if there are no queues with pending jobs the run loop is complete +2. Let a new temporary queue be defined as `WORK_QUEUE` +3. Move jobs from `CURRENT_QUEUE` into `WORK_QUEUE` +4. Process all the jobs sequentially in `WORK_QUEUE` +5. Return to Step 1 + +## An example of the internals + +Rather than writing the higher level app code that internally invokes the various run loop scheduling functions, +we have stripped away the covers, and shown the raw run-loop interactions. + +Working with this API directly is not common in most Ember apps, +but understanding this example will help you to understand the run-loops algorithm, +which will make you a better Ember developer. + + + +## How do I tell Ember to start a run loop? + +You should begin a run loop when the callback fires. + +The `Ember.run` method can be used to create a run loop. +In this example, jQuery and `Ember.run` are used to handle a click event and run some Ember code. + +This example uses the `=>` function syntax, which is a [new ES2015 syntax for callback functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) +that provides a lexical `this`. +If this syntax is new, +think of it as a function that has the same `this` as the context it is defined in. + +```javascript +$('a').click(() => { + Ember.run(() => { // begin loop + // Code that results in jobs being scheduled goes here + }); // end loop, jobs are flushed and executed +}); +``` + +## What happens if I forget to start a run loop in an async handler? + +As mentioned above, you should wrap any non-Ember async callbacks in `Ember.run`. +If you don't, Ember will try to approximate a beginning and end for you. +Consider the following callback: + +```javascript +$('a').click(() => { + console.log('Doing things...'); + + Ember.run.schedule('actions', () => { + // Do more things + }); +}); +``` + +The run loop API calls that _schedule_ work, i.e. [`run.schedule`](https://www.emberjs.com/api/ember/release/classes/@ember%2Frunloop/methods/schedule?anchor=schedule), +[`run.scheduleOnce`](https://www.emberjs.com/api/ember/release/classes/@ember%2Frunloop/methods/scheduleOnce?anchor=scheduleOnce), +[`run.once`](https://www.emberjs.com/api/ember/release/classes/@ember%2Frunloop/methods/once?anchor=once) have the property that they will approximate a run loop for you if one does not already exist. +These automatically created run loops we call _autoruns_. + +Here is some pseudocode to describe what happens using the example above: + +```javascript +$('a').click(() => { + // 1. autoruns do not change the execution of arbitrary code in a callback. + // This code is still run when this callback is executed and will not be + // scheduled on an autorun. + console.log('Doing things...'); + + Ember.run.schedule('actions', () => { + // 2. schedule notices that there is no currently available run loop so it + // creates one. It schedules it to close and flush queues on the next + // turn of the JS event loop. + if (! Ember.run.hasOpenRunLoop()) { + Ember.run.begin(); + nextTick(() => { + Ember.run.end() + }, 0); + } + + // 3. There is now a run loop available so schedule adds its item to the + // given queue + Ember.run.schedule('actions', () => { + // Do more things + }); + + }); + + // 4. This schedule sees the autorun created by schedule above as an available + // run loop and adds its item to the given queue. + Ember.run.schedule('afterRender', () => { + // Do yet more things + }); +}); +``` + +Although autoruns are convenient, they are suboptimal. +The current JS frame is allowed to end before the run loop is flushed, +which sometimes means the browser will take the opportunity to do other things, like garbage collection. +GC running in between data changing and DOM rerendering can cause visual lag and should be minimized. + +Relying on autoruns is not a rigorous or efficient way to use the run loop. +Wrapping event handlers manually are preferred. + +## How is run loop behaviour different when testing? + +When your application is in _testing mode_ then Ember will throw an error if you try to schedule work +without an available run loop. + +Autoruns are disabled in testing for several reasons: + +1. Autoruns are Embers way of not punishing you in production if you forget to open a run loop +before you schedule callbacks on it. +While this is useful in production, these are still situations that should be revealed in testing +to help you find and fix them. +2. Some of Ember's test helpers are promises that wait for the run loop to empty before resolving. +If your application has code that runs _outside_ a run loop, +these will resolve too early and give erroneous test failures which are difficult to find. +Disabling autoruns help you identify these scenarios and helps both your testing and your application! + +## Where can I find more information? + +Check out the [Ember.run](https://www.emberjs.com/api/ember/release/classes/@ember%2Frunloop) API documentation, +as well as the [Backburner library](https://github.com/ebryn/backburner.js/) that powers the run loop. diff --git a/guides/v3.6.0/applications/services.md b/guides/v3.6.0/applications/services.md new file mode 100644 index 0000000000..68f652cb3a --- /dev/null +++ b/guides/v3.6.0/applications/services.md @@ -0,0 +1,142 @@ +A [`Service`](https://www.emberjs.com/api/ember/release/modules/@ember%2Fservice) is an Ember object that lives for the duration of the application, and can be made available in different parts of your application. + +Services are useful for features that require shared state or persistent connections. Example uses of services might +include: + +* User/session authentication. +* Geolocation. +* WebSockets. +* Server-sent events or notifications. +* Server-backed API calls that may not fit Ember Data. +* Third-party APIs. +* Logging. + +### Defining Services + +Services can be generated using Ember CLI's `service` generator. +For example, the following command will create the `ShoppingCart` service: + +```bash +ember generate service shopping-cart +``` + +Services must extend the [`Service`](https://www.emberjs.com/api/ember/release/modules/@ember%2Fservice) base class: + +```javascript {data-filename=app/services/shopping-cart.js} +import Service from '@ember/service'; + +export default Service.extend({ +}); +``` + +Like any Ember object, a service is initialized and can have properties and methods of its own. +Below, the shopping cart service manages an items array that represents the items currently in the shopping cart. + +```javascript {data-filename=app/services/shopping-cart.js} +import Service from '@ember/service'; + +export default Service.extend({ + items: null, + + init() { + this._super(...arguments); + this.set('items', []); + }, + + add(item) { + this.items.pushObject(item); + }, + + remove(item) { + this.items.removeObject(item); + }, + + empty() { + this.items.clear(); + } +}); +``` + +### Accessing Services + +To access a service, +you can inject it in any container-resolved object such as a component or another service using the `inject` function from the `@ember/service` module. +There are two ways to use this function. +You can either invoke it with no arguments, or you can pass it the registered name of the service. +When no arguments are passed, the service is loaded based on the name of the variable key. +You can load the shopping cart service with no arguments like below. + +```javascript {data-filename=app/components/cart-contents.js} +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; + +export default Component.extend({ + //will load the service in file /app/services/shopping-cart.js + shoppingCart: service() +}); +``` + +Another way to inject a service is to provide the name of the service as the argument. + +```javascript {data-filename=app/components/cart-contents.js} +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; + +export default Component.extend({ + //will load the service in file /app/services/shopping-cart.js + cart: service('shopping-cart') +}); +``` + +This injects the shopping cart service into the component and makes it available as the `cart` property. + +Sometimes a service may or may not exist, like when an initializer conditionally registers a service. +Since normal injection will throw an error if the service doesn't exist, +you must look up the service using Ember's [`getOwner`](https://emberjs.com/api/ember/release/classes/@ember%2Fapplication/methods/getOwner?anchor=getOwner) instead. + +```javascript {data-filename=app/components/cart-contents.js} +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import { getOwner } from '@ember/application'; + +export default Component.extend({ + //will load the service in file /app/services/shopping-cart.js + cart: computed(function() { + return getOwner(this).lookup('service:shopping-cart'); + }) +}); +``` + +Injected properties are lazy loaded; meaning the service will not be instantiated until the property is explicitly called. + +Once loaded, a service will persist until the application exits. + +Below we add a remove action to the `cart-contents` component. + +```javascript {data-filename=app/components/cart-contents.js} +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; + +export default Component.extend({ + cart: service('shopping-cart'), + + actions: { + remove(item) { + this.cart.remove(item); + } + } +}); +``` +Once injected into a component, a service can also be used in the template. +Note `cart` being used below to get data from the cart. + +```handlebars {data-filename=app/templates/components/cart-contents.hbs} + +``` diff --git a/guides/v3.6.0/components/block-params.md b/guides/v3.6.0/components/block-params.md new file mode 100644 index 0000000000..77ce0778a7 --- /dev/null +++ b/guides/v3.6.0/components/block-params.md @@ -0,0 +1,52 @@ +Components can have properties passed in ([Passing Properties to a Component](../passing-properties-to-a-component/)), +but they can also return output to be used in a block expression. + +### Return values from a component with `yield` + +```handlebars {data-filename=app/templates/index.hbs} +{{blog-post post=model}} +``` + +```handlebars {data-filename=app/templates/components/blog-post.hbs} +{{yield post.title post.body post.author}} +``` + +Here an entire blog post model is being passed to the component as a single component property. +In turn the component is returning values using `yield`. +In this case the yielded values are pulled from the post being passed in +but anything that the component has access to can be yielded, such as an internal property or something from a service. + +### Consuming yielded values with block params + +The block expression can then use block params to bind names to any yielded values for use in the block. +This allows for template customization when using a component, +where the markup is provided by the consuming template, +but any event handling behavior implemented in the component is retained such as `click()` handlers. + +```handlebars {data-filename=app/templates/index.hbs} +{{#blog-post post=model as |title body author|}} +

{{title}}

+

by {{author}}

+

{{body}}

+{{/blog-post}} +``` + +The names are bound in the order that they are passed to `yield` in the component template. + +### Supporting both block and non-block component usage in one template + +It is possible to support both block and non-block usage of a component from a single component template +using the `has-block` helper. + +```handlebars {data-filename=app/templates/components/blog-post.hbs} +{{#if (has-block)}} + {{yield post.title post.body post.author}} +{{else}} +

{{post.title}}

+

Authored by {{post.author}}

+

{{post.body}}

+{{/if}} +``` + +This has the effect of providing a default template when using a component in the non-block form +but providing yielded values for use with block params when using a block expression. diff --git a/guides/v3.6.0/components/customizing-a-components-element.md b/guides/v3.6.0/components/customizing-a-components-element.md new file mode 100644 index 0000000000..b2081eed4b --- /dev/null +++ b/guides/v3.6.0/components/customizing-a-components-element.md @@ -0,0 +1,205 @@ +By default, each component is backed by a `
` element. If you were +to look at a rendered component in your developer tools, you would see +a DOM representation that looked something like: + +```html +
+

My Component

+
+``` + +You can customize what type of element Ember generates for your +component, including its attributes and class names, by creating a +subclass of `Component` in your JavaScript. + +### Customizing the Element + +To use a tag other than `div`, subclass `Component` and assign it +a `tagName` property. This property can be any valid HTML5 tag name as a +string. + +```javascript {data-filename=app/components/navigation-bar.js} +import Component from '@ember/component'; + +export default Component.extend({ + tagName: 'nav' +}); +``` + +```handlebars {data-filename=app/templates/components/navigation-bar.hbs} + +``` + +### Customizing the Element's Class + +You can specify the class of a component's element at invocation time the same +way you would for a regular HTML element: + +```handlebars +{{navigation-bar class="primary"}} +``` + +You can also specify which class names are applied to the component's +element by setting its `classNames` property to an array of strings: + +```javascript {data-filename=app/components/navigation-bar.js} +import Component from '@ember/component'; + +export default Component.extend({ + classNames: ['primary'] +}); +``` + +If you want class names to be determined by properties of the component, +you can use class name bindings. If you bind to a Boolean property, the +class name will be added or removed depending on the value: + +```javascript {data-filename=app/components/todo-item.js} +import Component from '@ember/component'; + +export default Component.extend({ + classNameBindings: ['isUrgent'], + isUrgent: true +}); +``` + +This component would render the following: + +```html +
+``` + +If `isUrgent` is changed to `false`, then the `is-urgent` class name will be removed. + +By default, the name of the Boolean property is dasherized. You can customize the class name +applied by delimiting it with a colon: + +```javascript {data-filename=app/components/todo-item.js} +import Component from '@ember/component'; + +export default Component.extend({ + classNameBindings: ['isUrgent:urgent'], + isUrgent: true +}); +``` + +This would render this HTML: + +```html +
+``` + +Besides the custom class name for the value being `true`, you can also specify a class name which is used when the value is `false`: + +```javascript {data-filename=app/components/todo-item.js} +import Component from '@ember/component'; + +export default Component.extend({ + classNameBindings: ['isEnabled:enabled:disabled'], + isEnabled: false +}); +``` + +This would render this HTML: + +```html +
+``` + +You can also specify a class which should only be added when the property is +`false` by declaring `classNameBindings` like this: + +```javascript {data-filename=app/components/todo-item.js} +import Component from '@ember/component'; + +export default Component.extend({ + classNameBindings: ['isEnabled::disabled'], + isEnabled: false +}); +``` + +This would render this HTML: + +```html +
+``` + +If the `isEnabled` property is set to `true`, no class name is added: + +```html +
+``` + +If the bound property's value is a string, that value will be added as a class name without +modification: + +```javascript {data-filename=app/components/todo-item.js} +import Component from '@ember/component'; + +export default Component.extend({ + classNameBindings: ['priority'], + priority: 'highestPriority' +}); +``` + +This would render this HTML: + +```html +
+``` + +### Customizing Attributes + +You can bind attributes to the DOM element that represents a component +by using `attributeBindings`: + +```javascript {data-filename=app/components/link-item.js} +import Component from '@ember/component'; + +export default Component.extend({ + tagName: 'a', + attributeBindings: ['href'], + + href: 'http://emberjs.com' +}); +``` + +You can also bind these attributes to differently named properties: + +```javascript {data-filename=app/components/link-item.js} +import Component from '@ember/component'; + +export default Component.extend({ + tagName: 'a', + attributeBindings: ['customHref:href'], + + customHref: 'http://emberjs.com' +}); +``` + +If the attribute is null, it won't be rendered: + +```javascript {data-filename=app/components/link-item.js} +import Component from '@ember/component'; + +export default Component.extend({ + tagName: 'span', + attributeBindings: ['title'], + + title: null, +}); +``` +This would render this HTML when no title is passed to the component: + +```html + +``` + +...and this HTML when a title of "Ember JS" is passed to the component: + +```html + +``` diff --git a/guides/v3.6.0/components/defining-a-component.md b/guides/v3.6.0/components/defining-a-component.md new file mode 100644 index 0000000000..4234f15dcc --- /dev/null +++ b/guides/v3.6.0/components/defining-a-component.md @@ -0,0 +1,122 @@ +To define a component, run: + +```bash +ember generate component my-component-name +``` + +Ember components are used to turn markup text and styles into reusable content. +Components consist of two parts: a JavaScript component file that defines behavior, and its accompanying Handlebars template that defines the markup for the component's UI. + +Components must have at least one dash in their name. So `blog-post` is an acceptable +name, and so is `audio-player-controls`, but `post` is not. This prevents clashes with +current or future HTML element names, aligns Ember components with the W3C [Custom +Elements](https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/custom/index.html) +spec, and ensures Ember detects the components automatically. + +A sample component template could look like this: + +```handlebars {data-filename=app/templates/components/blog-post.hbs} +
+

{{title}}

+

{{yield}}

+

Edit title: {{input type="text" value=title}}

+
+``` + +Given the above template, you can now use the `{{blog-post}}` component: + +```handlebars {data-filename=app/templates/index.hbs} +{{#each model as |post|}} + {{#blog-post title=post.title}} + {{post.body}} + {{/blog-post}} +{{/each}} +``` + +Its model is populated in `model` hook in the route handler: + +```javascript {data-filename=app/routes/index.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model() { + return this.store.findAll('post'); + } +}); +``` + +Each component is backed by an element under the hood. By default, +Ember will use a `
` element to contain your component's template. +To learn how to change the element Ember uses for your component, see +[Customizing a Component's +Element](./customizing-a-components-element/). + + +## Defining a Component Subclass + +Often times, your components will contain reused Handlebar templates. In +those cases, you do not need to write any JavaScript at all. Handlebars +allows you to you to define templates and reuse them as components. + +If you need to customize the behavior of the component you'll +need to define a subclass of [`Component`](https://www.emberjs.com/api/ember/release/classes/Component). For example, you would +need a custom subclass if you wanted to change a component's element, +respond to actions from the component's template, or manually make +changes to the component's element using JavaScript. + +Ember knows which subclass powers a component based on its filename. For +example, if you have a component called `blog-post`, you would create a +file at `app/components/blog-post.js`. If your component was called +`audio-player-controls`, the file name would be at +`app/components/audio-player-controls.js`. + +## Dynamically rendering a component + +The [`{{component}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/component?anchor=component) helper can be used to defer the selection of a component to +run time. The `{{my-component}}` syntax always renders the same component, +while using the `{{component}}` helper allows choosing a component to render on +the fly. This is useful in cases where you want to interact with different +external libraries depending on the data. Using the `{{component}}` helper would +allow you to keep different logic well separated. + +The first parameter of the helper is the name of a component to render, as a +string. So `{{component 'blog-post'}}` is the same as using `{{blog-post}}`. + +The real value of [`{{component}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/component?anchor=component) comes from being able to dynamically pick +the component being rendered. Below is an example of using the helper as a +means of choosing different components for displaying different kinds of posts: + +```handlebars {data-filename=app/templates/components/foo-component.hbs} +

Hello from foo!

+

{{post.body}}

+``` + +```handlebars {data-filename=app/templates/components/bar-component.hbs} +

Hello from bar!

+
{{post.author}}
+``` + +```javascript {data-filename=app/routes/index.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model() { + return this.store.findAll('post'); + } +}); +``` + +```handlebars {data-filename=app/templates/index.hbs} +{{#each model as |post|}} + {{!-- either foo-component or bar-component --}} + {{component post.componentName post=post}} +{{/each}} +``` + +When the parameter passed to `{{component}}` evaluates to `null` or `undefined`, +the helper renders nothing. When the parameter changes, the currently rendered +component is destroyed and the new component is created and brought in. + +Picking different components to render in response to the data allows you to +have different template and behavior for each case. The `{{component}}` helper +is a powerful tool for improving code modularity. diff --git a/guides/v3.6.0/components/handling-events.md b/guides/v3.6.0/components/handling-events.md new file mode 100644 index 0000000000..235c615016 --- /dev/null +++ b/guides/v3.6.0/components/handling-events.md @@ -0,0 +1,176 @@ +You can respond to user events on your component like double-clicking, hovering, +and key presses through event handlers. Simply implement the name of the event +you want to respond to as a method on your component. + +For example, imagine we have a template like this: + +```handlebars +{{#double-clickable}} + This is a double clickable area! +{{/double-clickable}} +``` + +Let's implement `double-clickable` such that when it is +clicked, an alert is displayed: + +```javascript {data-filename=app/components/double-clickable.js} +import Component from '@ember/component'; + +export default Component.extend({ + doubleClick() { + alert('DoubleClickableComponent was clicked!'); + } +}); +``` + +Browser events may bubble up the DOM which potentially target parent component(s) +in succession. To enable bubbling `return true;` from the event handler method +in your component. + +```javascript {data-filename=app/components/double-clickable.js} +import Component from '@ember/component'; +import Ember from 'ember'; + +export default Component.extend({ + doubleClick() { + console.info('DoubleClickableComponent was clicked!'); + return true; + } +}); +``` + +See the list of event names at the end of this page. Any event can be defined +as an event handler in your component. + +## Sending Actions + +In some cases your component needs to define event handlers, perhaps to support +various draggable behaviors. For example, a component may need to send an `id` +when it receives a drop event: + +```handlebars +{{drop-target dropAction=(action "didDrop")}} +``` + +You can define the component's event handlers to manage the drop event. +And if you need to, you may also stop events from bubbling, by using +`return false;`. + +```javascript {data-filename=app/components/drop-target.js} +import Component from '@ember/component'; + +export default Component.extend({ + attributeBindings: ['draggable'], + draggable: 'true', + + dragOver() { + return false; + }, + + drop(event) { + let id = event.dataTransfer.getData('text/data'); + this.dropAction(id); + } +}); +``` + +In the above component, `didDrop` is the `action` passed in. This action is +called from the `drop` event handler and passes one argument to the action - +the `id` value found through the `drop` event object. + + +Another way to preserve native event behaviors and use an action, is to +assign a (closure) action to an inline event handler. Consider the +template below which includes an `onclick` handler on a `button` element: + +```handlebars + +``` + +The `signUp` action is simply a function defined on the `actions` hash +of a component. Since the action is assigned to an inline handler, the +function definition can define the event object as its first parameter. + +```javascript +actions: { + signUp(event){ + // Only when assigning the action to an inline handler, the event object + // is passed to the action as the first parameter. + } +} +``` + +The normal behavior for a function defined in `actions` does not receive the +browser event as an argument. So, the function definition for the action cannot +define an event parameter. The following example demonstrates the +default behavior using an action. + +```handlebars + +``` + +```javascript +actions: { + signUp() { + // No event object is passed to the action. + } +} +``` + +To utilize an `event` object as a function parameter: + +- Define the event handler in the component (which is designed to receive the + browser event object). +- Or, assign an action to an inline event handler in the template (which + creates a closure action and does receive the event object as an argument). + + +## Event Names + +The event handling examples described above respond to one set of events. +The names of the built-in events are listed below. Custom events can be +registered by using [Application.customEvents](https://www.emberjs.com/api/ember/release/classes/Application/properties/customEvents?anchor=customEvents). + +Touch events: + +* `touchStart` +* `touchMove` +* `touchEnd` +* `touchCancel` + +Keyboard events + +* `keyDown` +* `keyUp` +* `keyPress` + +Mouse events + +* `mouseDown` +* `mouseUp` +* `contextMenu` +* `click` +* `doubleClick` +* `mouseMove` +* `focusIn` +* `focusOut` +* `mouseEnter` +* `mouseLeave` + +Form events: + +* `submit` +* `change` +* `focusIn` +* `focusOut` +* `input` + +HTML5 drag and drop events: + +* `dragStart` +* `drag` +* `dragEnter` +* `dragLeave` +* `dragOver` +* `dragEnd` +* `drop` diff --git a/guides/v3.6.0/components/passing-properties-to-a-component.md b/guides/v3.6.0/components/passing-properties-to-a-component.md new file mode 100644 index 0000000000..2565679218 --- /dev/null +++ b/guides/v3.6.0/components/passing-properties-to-a-component.md @@ -0,0 +1,119 @@ +Components are isolated from their surroundings, so any data that the component +needs has to be passed in. + +For example, imagine you have a `blog-post` component that is used to +display a blog post: + +```handlebars {data-filename=app/templates/components/blog-post.hbs} +
+

{{title}}

+

{{body}}

+
+``` + +Now imagine we have the following template and route: + +```javascript {data-filename=app/routes/index.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model() { + return this.store.findAll('post'); + } +}); +``` + +If we tried to use the component like this: + +```handlebars {data-filename=app/templates/index.hbs} +{{#each model as |post|}} + {{blog-post}} +{{/each}} +``` + +The following HTML would be rendered: + +```html +
+

+

+
+``` + +In order to make a property available to a component, you must pass it +in like this: + +```handlebars {data-filename=app/templates/index.hbs} +{{#each model as |post|}} + {{blog-post title=post.title body=post.body}} +{{/each}} +``` + +It is important to note that these properties stay in sync (technically +known as being "bound"). That is, if the value of `componentProperty` +changes in the component, `outerProperty` will be updated to reflect that +change. The reverse is true as well. + +In addition to making properties available, actions can be made available +to components. This allows data to flow back up to its parent. You pass actions +like this. + +```handlebars {data-filename=app/templates/index.hbs} + {{button-with-confirmation + text="Click here to unsubscribe." + onConfirm=(action "unsubscribe") + }} +``` + +It is important to note that actions can only be passed from a controller or another +component. They cannot be passed from a route. See [passing an action to the component](../triggering-changes-with-actions/#toc_passing-the-action-to-the-component) +for more details on how to pass actions. + +## Positional Params + +In addition to passing parameters in by name, you can pass them in by position. +In other words, you can invoke the above component example like this: + +```handlebars {data-filename=app/templates/index.hbs} +{{#each model as |post|}} + {{blog-post post.title post.body}} +{{/each}} +``` + +To set the component up to receive parameters this way, you need to +set the [`positionalParams`](https://www.emberjs.com/api/ember/release/classes/Component/properties/positionalParams?anchor=positionalParams) attribute in your component class. + +```javascript {data-filename=app/components/blog-post.js} +import Component from '@ember/component'; + +export default Component.extend({}).reopenClass({ + positionalParams: ['title', 'body'] +}); +``` + +Then you can use the attributes in the component exactly as if they had been +passed in like `{{blog-post title=post.title body=post.body}}`. + +Notice that the `positionalParams` property is added to the class as a +static variable via `reopenClass`. Positional params are always declared on +the component class and cannot be changed while an application runs. + +Alternatively, you can accept an arbitrary number of parameters by +setting `positionalParams` to a string, e.g. `positionalParams: 'params'`. This +will allow you to access those params as an array like so: + +```javascript {data-filename=app/components/blog-post.js} +import Component from '@ember/component'; +import { computed } from '@ember/object'; + +export default Component.extend({ + title: computed('params.[]', function(){ + return this.params[0]; + }), + body: computed('params.[]', function(){ + return this.params[1]; + }) +}).reopenClass({ + positionalParams: 'params' +}); +``` diff --git a/guides/v3.6.0/components/the-component-lifecycle.md b/guides/v3.6.0/components/the-component-lifecycle.md new file mode 100644 index 0000000000..cb4482adca --- /dev/null +++ b/guides/v3.6.0/components/the-component-lifecycle.md @@ -0,0 +1,280 @@ +Part of what makes components so useful is that they let you take complete control of a section of the DOM. +This allows for direct DOM manipulation, listening and responding to browser events, and using 3rd party JavaScript libraries in your Ember app. + +As components are rendered, re-rendered and finally removed, Ember provides _lifecycle hooks_ that allow you to run code at specific times in a component's life. + +To get the most use out of a component, it is important to understand these lifecycle methods. + +## Order of Lifecycle Hooks Called +Listed below are the component lifecycle [hooks](../../getting-started/core-concepts/#toc_hooks) in order of execution according to render scenario. + +### On Initial Render + +1. `init` +2. [`didReceiveAttrs`](#toc_formatting-component-attributes-with-didreceiveattrs) +3. `willRender` +4. [`didInsertElement`](#toc_integrating-with-third-party-libraries-with-didinsertelement) +5. [`didRender`](#toc_making-updates-to-the-rendered-dom-with-didrender) + +### On Re-Render + +1. [`didUpdateAttrs`](#toc_resetting-presentation-state-on-attribute-change-with-didupdateattrs) +2. [`didReceiveAttrs`](#toc_formatting-component-attributes-with-didreceiveattrs) +3. `willUpdate` +4. `willRender` +5. `didUpdate` +6. [`didRender`](#toc_making-updates-to-the-rendered-dom-with-didrender) + +### On Component Destroy + +1. [`willDestroyElement`](#toc_detaching-and-tearing-down-component-elements-with-willdestroyelement) +2. `willClearRender` +3. `didDestroyElement` + +## Lifecycle Hook Examples + + +Below are some samples of ways to use lifecycle hooks within your components. + +### Resetting Presentation State on Attribute Change with `didUpdateAttrs` + +`didUpdateAttrs` runs when the attributes of a component have changed, but not when the component is re-rendered, via `component.rerender`, +`component.set`, or changes in models or services used by the template. + +Since `didUpdateAttrs` is called prior to rerender, you can use this hook to execute code when specific attributes are changed. +This hook can be an effective alternative to an observer, as it will run prior to a re-render, but after an attribute has changed. + +An example of this scenario in action is a profile editor component. As you are editing one user, and the user attribute is changed, +you can use `didUpdateAttrs` to clear any error state that was built up from editing the previous user. + +```handlebars {data-filename=app/templates/components/profile-editor.hbs} +
    + {{#each errors as |error|}} +
  • {{error.message}}
  • + {{/each}} +
+
+ {{input name="user.name" value=name change=(action "required")}} + {{input name="user.department" value=department change=(action "required")}} + {{input name="user.email" value=email change=(action "required")}} +
+``` + +```javascript {data-filename=/app/components/profile-editor.js} +import Component from '@ember/component'; + +export default Component.extend({ + init() { + this._super(...arguments); + this.set('errors', []); + }, + + didUpdateAttrs() { + this._super(...arguments); + this.set('errors', []); + }, + + actions: { + required(event) { + if (!event.target.value) { + this.errors.pushObject({ message: `${event.target.name} is required`}); + } + } + } +}); +``` + +### Formatting Component Attributes with `didReceiveAttrs` + +`didReceiveAttrs` runs after `init`, and it also runs on subsequent re-renders, which is useful for logic that is the same on all renders. +It does not run when the re-render has been initiated internally. + +Since the `didReceiveAttrs` hook is called every time a component's attributes are updated whether on render or re-render, +you can use the hook to effectively act as an observer, ensuring code is executed every time an attribute changes. + +For example, if you have a component that renders based on a json configuration, but you want to provide your component with the option of taking the config as a string, +you can leverage `didReceiveAttrs` to ensure the incoming config is always parsed. + +```javascript {data-filename=app/components/profile-editor.js} +import Component from '@ember/component'; + +export default Component.extend({ + didReceiveAttrs() { + this._super(...arguments); + const profile = this.data; + if (typeof profile === 'string') { + this.set('profile', JSON.parse(profile)); + } else { + this.set('profile', profile); + } + } +}); +``` + +### Integrating with Third-Party Libraries with `didInsertElement` + +Suppose you want to integrate your favorite date picker library into an Ember project. +Typically, 3rd party JS/jQuery libraries require a DOM element to bind to. +So, where is the best place to initialize and attach the library? + +After a component successfully renders its backing HTML element into the DOM, it will trigger its [`didInsertElement()`][did-insert-element] hook. + +Ember guarantees that, by the time `didInsertElement()` is called: + +1. The component's element has been both created and inserted into the + DOM. +2. The component's element is accessible via the component's + [`$()`][dollar] + method. + +A component's [`$()`][dollar] method allows you to access the component's DOM element by returning a JQuery element. +For example, you can set an attribute using jQuery's `attr()` method: + +```javascript {data-filename=app/components/profile-editor.js} +import Component from '@ember/component'; + +export default Component.extend({ + didInsertElement() { + this._super(...arguments); + this.$().attr('contenteditable', true); + } +}); +``` + +[`$()`][dollar] will, by default, return a jQuery object for the component's root element, but you can also target child elements within the component's template by passing a selector: + +```javascript {data-filename=app/components/profile-editor.js} +import Component from '@ember/component'; + +export default Component.extend({ + didInsertElement() { + this._super(...arguments); + this.$('div p button').addClass('enabled'); + } +}); +``` + +Let's initialize our date picker by overriding the [`didInsertElement()`][did-insert-element] method. + +Date picker libraries usually attach to an `` element, so we will use jQuery to find an appropriate input within our component's template. + +```javascript {data-filename=app/components/profile-editor.js} +import Component from '@ember/component'; + +export default Component.extend({ + didInsertElement() { + this._super(...arguments); + this.$('input.date').myDatePickerLib(); + } +}); +``` + +[`didInsertElement()`][did-insert-element] is also a good place to +attach event listeners. This is particularly useful for custom events or +other browser events which do not have a [built-in event +handler](./handling-events/#toc_event-names). + +For example, perhaps you have some custom CSS animations trigger when the component +is rendered and you want to handle some cleanup when it ends: + +```javascript {data-filename=app/components/profile-editor.js} +import Component from '@ember/component'; + +export default Component.extend({ + didInsertElement() { + this._super(...arguments); + this.$().on('animationend', () => { + $(this).removeClass('sliding-anim'); + }); + } +}); +``` + +There are a few things to note about the `didInsertElement()` hook: + +- It is only triggered once when the component element is first rendered. +- In cases where you have components nested inside other components, the child component will always receive the `didInsertElement()` call before its parent does. +- Setting properties on the component in [`didInsertElement()`][did-insert-element] triggers a re-render, and for performance reasons, + is not allowed. +- While [`didInsertElement()`][did-insert-element] is technically an event that can be listened for using `on()`, it is encouraged to override the default method itself, + particularly when order of execution is important. + +[did-insert-element]: https://www.emberjs.com/api/ember/release/classes/Component/events/didInsertElement?anchor=didInsertElement +[dollar]: https://www.emberjs.com/api/ember/release/classes/Component/methods/$?anchor=%24 + +### Making Updates to the Rendered DOM with `didRender` + +The `didRender` hook is called during both render and re-render after the template has rendered and the DOM updated. +You can leverage this hook to perform post-processing on the DOM of a component after it's been updated. + +In this example, there is a list component that needs to scroll to a selected item when rendered. +Since scrolling to a specific spot is based on positions within the DOM, we need to ensure that the list has been rendered before scrolling. +We can first render this list, and then set the scroll. + +The component below takes a list of items and displays them on the screen. +Additionally, it takes an object representing which item is selected and will select and set the scroll top to that item. + +```handlebars {data-filename=app/templates/application.hbs} +{{selected-item-list items=items selectedItem=selection}} +``` + +When rendered the component will iterate through the given list and apply a class to the one that is selected. + + +```handlebars {data-filename=app/templates/components/selected-item-list.hbs} +{{#each items as |item|}} +
{{item.label}}
+{{/each}} +``` + +The scroll happens on `didRender`, where it will scroll the component's container to the element with the selected class name. + +```javascript {data-filename=/app/components/selected-item-list.js} +import Component from '@ember/component'; + +export default Component.extend({ + classNames: ['item-list'], + + didReceiveAttrs() { + this._super(...arguments); + this.items.forEach((item) => { + if (item.id === this.selectedItem.id) { + item.isSelected = true; + } + }); + }, + + didRender() { + this._super(...arguments); + this.$('.item-list').scrollTop(this.$('.selected-item').position().top); + } +}); +``` + +### Detaching and Tearing Down Component Elements with `willDestroyElement` + +When a component detects that it is time to remove itself from the DOM, Ember will trigger the [`willDestroyElement()`](https://www.emberjs.com/api/ember/release/classes/Component/events/willDestroyElement?anchor=willDestroyElement) method, +allowing for any teardown logic to be performed. + +Component teardown can be triggered by a number of different conditions. +For instance, the user may navigate to a different route, or a conditional Handlebars block surrounding your component may change: + +```handlebars {data-filename=app/templates/application.hbs} +{{#if falseBool}} + {{my-component}} +{{/if}} +``` + +Let's use this hook to cleanup our date picker and event listener from above: + +```javascript {data-filename=app/components/profile-editor.js} +import Component from '@ember/component'; + +export default Component.extend({ + willDestroyElement() { + this.$().off('animationend'); + this.$('input.date').myDatepickerLib().destroy(); + this._super(...arguments); + } +}); +``` diff --git a/guides/v3.6.0/components/triggering-changes-with-actions.md b/guides/v3.6.0/components/triggering-changes-with-actions.md new file mode 100644 index 0000000000..31fe85bea2 --- /dev/null +++ b/guides/v3.6.0/components/triggering-changes-with-actions.md @@ -0,0 +1,492 @@ +You can think of a component as a black box of UI functionality. +So far, you've learned how parent components can pass attributes in to a +child component, and how that component can use those attributes from +both JavaScript and its template. + +But what about the opposite direction? How does data flow back out of +the component to the parent? In Ember, components use **actions** to +communicate events and changes. + +Let's look at a simple example of how a component can use an action to +communicate with its parent. + +Imagine we're building an application where users can have accounts. We +need to build the UI for users to delete their account. Because we don't +want users to accidentally delete their accounts, we'll build a button +that requires the user to confirm in order to trigger some +action. + +Once we create this "button with confirmation" +component, we want to be able to reuse it all over our application. + +## Creating the Component + +Let's call our component `button-with-confirmation`. We can create it by +typing: + +```bash +ember generate component button-with-confirmation +``` + +We'll plan to use the component in a template something like this: + +```handlebars {data-filename=app/templates/components/user-profile.hbs} +{{button-with-confirmation + text="Click OK to delete your account." +}} +``` + +We'll also want to use the component elsewhere, perhaps like this: + +```handlebars {data-filename=app/templates/components/send-message.hbs} +{{button-with-confirmation + text="Click OK to send your message." +}} +``` + +## Designing the Action + +When implementing an action on a component that will be handled outside the component, you need to break it down into two steps: + +1. In the parent component, decide how you want to react to the action. + Here, we want to have the action delete the user's account when it's used in one place, and + send a message when used in another place. +2. In the component, determine when something has happened, and when to tell the + outside world. Here, we want to trigger the outside action (deleting the + account or sending the message) after the user clicks the button and then + confirms. + +Let's take it step by step. + +## Implementing the Action + +In the parent component, let's first define what we want to happen when the +user clicks the button and then confirms. In the first case, we'll find the user's +account and delete it. + +In Ember, each component can +have a property called `actions`, where you put functions that can be +[invoked by the user interacting with the component +itself](../../templates/actions/), or by child components. + +Let's look at the parent component's JavaScript file. In this example, +imagine we have a parent component called `user-profile` that shows the +user's profile to them. + +We'll implement an action on the parent component called +`userDidDeleteAccount()` that, when called, gets a hypothetical `login` +[service](../../applications/services/) and calls the service's +`deleteUser()` method. + +```javascript {data-filename=app/components/user-profile.js} +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; + +export default Component.extend({ + login: service(), + + actions: { + userDidDeleteAccount() { + this.login.deleteUser(); + } + } +}); +``` + +Now we've implemented our action, but we have not told Ember when we +want this action to be triggered, which is the next step. + +## Designing the Child Component + +Next, +in the child component we will implement the logic to confirm that the user wants to take the action they indicated by clicking the button: + +```javascript {data-filename=app/components/button-with-confirmation.js} +import Component from '@ember/component'; + +export default Component.extend({ + actions: { + launchConfirmDialog() { + this.set('confirmShown', true); + }, + + submitConfirm() { + // trigger action on parent component + this.set('confirmShown', false); + }, + + cancelConfirm() { + this.set('confirmShown', false); + } + } +}); +``` + +The component template will have a button and a div that shows the confirmation dialog +based on the value of `confirmShown`. + +```handlebars {data-filename=app/templates/components/button-with-confirmation.hbs} + +{{#if confirmShown}} +
+ + +
+{{/if}} +``` + +## Passing the Action to the Component + +Now we need to make it so that the `userDidDeleteAccount()` action defined in the parent component `user-profile` can be triggered from within `button-with-confirmation`. +We'll do this by passing the action to the child component in exactly the same way that we pass other properties. +This is possible since actions are simply functions, just like any other method on a component, +and they can therefore be passed from one component to another like this: + +```handlebars {data-filename=app/templates/components/user-profile.hbs} +{{button-with-confirmation + text="Click here to delete your account." + onConfirm=(action "userDidDeleteAccount") +}} +``` + +This snippet says "take the `userDidDeleteAccount` action from the parent and make it available on the child component as the property `onConfirm`." +Note the use here of the `action` helper, +which serves to return the function named `"userDidDeleteAccount"` that we are passing to the component. + +We can do a similar thing for our `send-message` component: + +```handlebars {data-filename=app/templates/components/send-message.hbs} +{{button-with-confirmation + text="Click to send your message." + onConfirm=(action "sendMessage") +}} +``` + +Now, we can use `onConfirm` in the child component to invoke the action on the +parent: + +```javascript {data-filename=app/components/button-with-confirmation.js} +import Component from '@ember/component'; + +export default Component.extend({ + actions: { + launchConfirmDialog() { + this.set('confirmShown', true); + }, + + submitConfirm() { + //call the onConfirm property to invoke the passed in action + this.onConfirm(); + }, + + cancelConfirm() { + this.set('confirmShown', false); + } + } +}); +``` + +Like normal attributes, actions can be a property on the component; the +only difference is that the property is set to a function that knows how +to trigger behavior. + +That makes it easy to remember how to add an action to a component. It's +like passing an attribute, but you use the `action` helper to pass +a function instead. Actions can only be passed from a controller or +component, they cannot be passed from a route. + +Actions in components allow you to decouple an event happening from how it's handled, leading to modular, +more reusable components. + +## Handling Action Completion + +Often actions perform asynchronous tasks, such as making an ajax request to a server. +Since actions are functions that can be passed in by a parent component, they are able to return values when called. +The most common scenario is for an action to return a promise so that the component can handle the action's completion. + +In our user `button-with-confirmation` component we want to leave the confirmation modal open until we know that the +operation has completed successfully. +This is accomplished by expecting a promise to be returned from `onConfirm`. +Upon resolution of the promise, we set a property used to indicate the visibility of the confirmation modal. + +```javascript {data-filename=app/components/button-with-confirmation.js} +import Component from '@ember/component'; + +export default Component.extend({ + actions: { + launchConfirmDialog() { + this.set('confirmShown', true); + }, + + submitConfirm() { + // call onConfirm with the value of the input field as an argument + let promise = this.onConfirm(); + promise.then(() => { + this.set('confirmShown', false); + }); + }, + + cancelConfirm() { + this.set('confirmShown', false); + } + } +}); +``` + +## Passing Arguments + +Sometimes the parent component invoking an action has some context needed for the action that the child component +doesn't. +Consider, for example, +the case where the `button-with-confirmation` component we've defined is used within `send-message`. +The `sendMessage` action that we pass to the child component may expect a message type parameter to be provided as an argument: + +```javascript {data-filename=app/components/send-message.js} +import Component from '@ember/component'; + +export default Component.extend({ + actions: { + sendMessage(messageType) { + //send message here and return a promise + } + } +}); +``` + +However, +the `button-with-confirmation` component invoking the action doesn't know or care what type of message it's collecting. +In cases like this, the parent template can provide the required parameter when the action is passed to the child. +For example, if we want to use the button to send a message of type `"info"`: + +```handlebars {data-filename=app/templates/components/send-message.hbs} +{{button-with-confirmation + text="Click to send your message." + onConfirm=(action "sendMessage" "info") +}} +``` + +Within `button-with-confirmation`, the code in the `submitConfirm` action does not change. +It will still invoke `onConfirm` without explicit arguments: + +```javascript {data-filename=app/components/button-with-confirmation.js} +const promise = this.onConfirm(); +``` +However the expression `(action "sendMessage" "info")` used in passing the action to the component creates a closure, +i.e. an object that binds the parameter we've provided to the function specified. +So now when the action is invoked, that parameter will automatically be passed as its argument, effectively calling `sendMessage("info")`, +despite the argument not appearing in the calling code. + +So far in our example, the action we have passed to `button-with-confirmation` is a function that accepts one argument, +`messageType`. +Suppose we want to extend this by allowing `sendMessage` to take a second argument, +the actual text of the message the user is sending: + +```javascript {data-filename=app/components/send-message.js} +import Component from '@ember/component'; + +export default Component.extend({ + actions: { + sendMessage(messageType, messageText) { + //send message here and return a promise + } + } +}); +``` + +We want to arrange for the action to be invoked from within `button-with-confirmation` with both arguments. +We've seen already that if we provide a `messageType` value to the `action` helper when we insert `button-with-confirmation` into its parent `send-message` template, +that value will be passed to the `sendMessage` action as its first argument automatically when invoked as `onConfirm`. +If we subsequently pass a single additional argument to `onConfirm` explicitly, +that argument will be passed to `sendMessage` as its second argument +(This ability to provide arguments to a function one at a time is known as [currying](https://en.wikipedia.org/wiki/Currying)). + +In our case, the explicit argument that we pass to `onConfirm` will be the required `messageText`. +However, remember that internally our `button-with-confirmation` component does not know or care that it is being used in a messaging application. +Therefore within the component's javascript file, +we will use a property `confirmValue` to represent that argument and pass it to `onConfirm` as shown here: + +```javascript {data-filename=app/components/button-with-confirmation.js} +import Component from '@ember/component'; + +export default Component.extend({ + actions: { + //... + submitConfirm() { + // call onConfirm with a second argument + let promise = this.onConfirm(this.confirmValue); + promise.then(() => { + this.set('confirmShown', false); + }); + }, + //... + } +}); +``` + +In order for `confirmValue` to take on the value of the message text, +we'll bind the property to the value of a user input field that will appear when the button is clicked. +To accomplish this, +we'll first modify the component so that it can be used in block form and we will [yield](../wrapping-content-in-a-component/) `confirmValue` to the block within the `"confirmDialog"` element: + +```handlebars {data-filename=app/templates/components/button-with-confirmation.hbs} + +{{#if confirmShown}} +
+ {{yield confirmValue}} + + +
+{{/if}} +``` + +With this modification, +we can now use the component in `send-message` to wrap a text input element whose `value` attribute is set to `confirmValue`: + +```handlebars {data-filename=app/templates/components/send-message.hbs} +{{#button-with-confirmation + text="Click to send your message." + onConfirm=(action "sendMessage" "info") + as |confirmValue|}} + {{input value=confirmValue}} +{{/button-with-confirmation}} +``` + +When the user enters their message into the input field, +the message text will now be available to the component as `confirmValue`. +Then, once they click the "OK" button, the `submitConfirm` action will be triggered, calling `onConfirm` with the provided `confirmValue`, +thus invoking the `sendMessage` action in `send-message` with both the `messageType` and `messageText` arguments. + +## Invoking Actions Directly on Component Collaborators + +Actions can be invoked on objects other than the component directly from the template. For example, in our +`send-message` component we might include a service that processes the `sendMessage` logic. + +```javascript {data-filename=app/components/send-message.js} +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; + +export default Component.extend({ + messaging: service(), + + // component implementation +}); +``` + +We can tell the action to invoke the `sendMessage` action directly on the messaging service with the `target` attribute. + +```handlebars {data-filename=app/templates/components/send-message.hbs} +{{#button-with-confirmation + text="Click to send your message." + onConfirm=(action "sendMessage" "info" target=messaging) + as |confirmValue| }} + {{input value=confirmValue}} +{{/button-with-confirmation}} +``` + +By supplying the `target` attribute, the action helper will look to invoke the `sendMessage` action directly on the messaging +service, saving us from writing code on the component that just passes the action along to the service. + +```javascript {data-filename=app/services/messaging.js} +import Service from '@ember/service'; + +export default Ember.Service.extend({ + actions: { + sendMessage(messageType, text) { + //handle message send and return a promise + } + } +}); +``` + +## Destructuring Objects Passed as Action Arguments + +A component will often not know what information a parent needs to process an action, and will just pass all the +information it has. +For example, our `user-profile` component is going to notify its parent, `system-preferences-editor`, that a +user's account was deleted, and passes along with it the full user profile object. + + +```javascript {data-filename=app/components/user-profile.js} +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; + +export default Component.extend({ + login: service(), + + actions: { + userDidDeleteAccount() { + this.login.deleteUser(); + this.didDelete(this.login.currentUserObj); + } + } +}); +``` + +All our `system-preferences-editor` component really needs to process a user deletion is an account ID. +For this case, the action helper provides the `value` attribute to allow a parent component to dig into the passed +object to pull out only what it needs. + +```handlebars {data-filename=app/templates/components/system-preferences-editor.hbs} +{{user-profile didDelete=(action "userDeleted" value="account.id")}} +``` + +Now when the `system-preferences-editor` handles the delete action, it receives only the user's account `id` string. + +```javascript {data-filename=app/components/system-preferences-editor.js} +import Component from '@ember/component'; + +export default Component.extend({ + actions: { + userDeleted(idStr) { + //respond to deletion + } + } +}); +``` + +## Calling Actions Up Multiple Component Layers + +When your components go multiple template layers deep, it is common to need to handle an action several layers up the tree. +Using the action helper, parent components can pass actions to child components through templates alone without adding JavaScript code to those child components. + +For example, say we want to move account deletion from the `user-profile` component to its parent `system-preferences-editor`. + +First we would move the `deleteUser` action from `user-profile.js` to the actions object on `system-preferences-editor`. + +```javascript {data-filename=app/components/system-preferences-editor.js} +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; + +export default Component.extend({ + login: service(), + actions: { + deleteUser(idStr) { + return this.login.deleteUserAccount(idStr); + } + } +}); +``` + +Then our `system-preferences-editor` template passes its local `deleteUser` action into the `user-profile` as that +component's `deleteCurrentUser` property. + +```handlebars {data-filename=app/templates/components/system-preferences-editor.hbs} +{{user-profile + deleteCurrentUser=(action 'deleteUser' login.currentUser.id) +}} +``` + +The action `deleteUser` is in quotes, since `system-preferences-editor` is where the action is defined now. Quotes indicate that the action should be looked for in `actions` local to that component, rather than in those that have been passed from a parent. + +In our `user-profile.hbs` template we change our action to call `deleteCurrentUser` as passed above. + +```handlebars {data-filename=app/templates/components/user-profile.hbs} +{{button-with-confirmation + onConfirm=(action deleteCurrentUser) + text="Click OK to delete your account." +}} +``` + +Note that `deleteCurrentUser` is no longer in quotes here as opposed to [previously](#toc_passing-the-action-to-the-component). Quotes are used to initially pass the action down the component tree, but at every subsequent level you are instead passing the actual function reference (without quotes) in the action helper. + +Now when you confirm deletion, the action goes straight to the `system-preferences-editor` to be handled in its local context. diff --git a/guides/v3.6.0/components/wrapping-content-in-a-component.md b/guides/v3.6.0/components/wrapping-content-in-a-component.md new file mode 100644 index 0000000000..9a08631009 --- /dev/null +++ b/guides/v3.6.0/components/wrapping-content-in-a-component.md @@ -0,0 +1,116 @@ +Sometimes, you may want to define a component that wraps content provided by other templates. + +For example, imagine we are building a `blog-post` component that we can use in our application to display a blog post: + +```handlebars {data-filename=app/templates/components/blog-post.hbs} +

{{title}}

+
{{body}}
+``` + +Now, we can use the `{{blog-post}}` component and pass it properties in another template: + +```handlebars +{{blog-post title=title body=body}} +``` + +See [Passing Properties to a Component](../passing-properties-to-a-component/) for more. + +In this case, the content we wanted to display came from the model. +But what if we want the developer using our component to be able to provide custom HTML content? + +In addition to the simple form you've learned so far, +components also support being used in **block form**. +In block form, components can be passed a Handlebars template that is rendered inside the component's template wherever the `{{yield}}` expression appears. + +To use the block form, add a `#` character to the beginning of the component name, +then make sure to add a closing tag. + +See the Handlebars documentation on [block expressions](http://handlebarsjs.com/#block-expressions) for more. + +In that case, we can use the `{{blog-post}}` component in **block form** and tell Ember where the block content should be rendered using the `{{yield}}` helper. +To update the example above, we'll first change the component's template: + +```handlebars {data-filename=app/templates/components/blog-post.hbs} +

{{title}}

+
{{yield}}
+``` + +You can see that we've replaced `{{body}}` with `{{yield}}`. +This tells Ember that this content will be provided when the component is used. + +Next, we'll update the template using the component to use the block form: + +```handlebars {data-filename=app/templates/index.hbs} +{{#blog-post title=title}} +

by {{author}}

+ {{body}} +{{/blog-post}} +``` + +It's important to note that the template scope inside the component block is the same as outside. +If a property is available in the template outside the component, it is also available inside the component block. + +## Sharing Component Data with its Wrapped Content + +There is also a way to share data within your blog post component with the content it is wrapping. +In our blog post component we want to provide a way for the user to configure what type of style they want to write their post in. +We will give them the option to specify either `markdown-style` or `html-style`. + +```handlebars {data-filename=app/templates/index.hbs} +{{#blog-post editStyle="markdown-style"}} +

by {{author}}

+ {{body}} +{{/blog-post}} +``` + +Supporting different editing styles will require different body components to provide special validation and highlighting. +To load a different body component based on editing style, +you can yield the component using the [`component helper`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/component?anchor=component) and [`hash helper`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/hash?anchor=hash). +Here, the appropriate component is assigned to a hash using nested helpers and yielded to the template. +Notice `editStyle` being used as an argument to the component helper. + +```handlebars {data-filename=app/templates/components/blog-post.hbs} +

{{title}}

+
{{yield (hash body=(component editStyle))}}
+``` + +Once yielded, the data can be accessed by the wrapped content by referencing the `post` variable. +Now a component called `markdown-style` will be rendered in `{{post.body}}`. + +```handlebars {data-filename=app/templates/index.hbs} +{{#blog-post editStyle="markdown-style" postData=myText as |post|}} +

by {{author}}

+ {{post.body}} +{{/blog-post}} +``` + +Finally, we need to share `myText` with the body in order to have it display. +To pass the blog text to the body component, we'll add a `postData` argument to the component helper. + +```handlebars {data-filename=app/templates/components/blog-post.hbs} +

{{title}}

+
+ {{yield (hash + body=(component editStyle postData=postData) + )}} +
+``` + +At this point, our block content has access to everything it needs to render, +via the wrapping `blog-post` component's template helpers. + +Additionally, since the component isn't instantiated until the block content is rendered, +we can add arguments within the block. +In this case we'll add a text style option which will dictate the style of the body text we want in our post. +When `{{post.body}}` is instantiated, it will have both the `editStyle` and `postData` given by its wrapping component, +as well as the `bodyStyle` declared in the template. + +```handlebars {data-filename=app/templates/index.hbs} +{{#blog-post editStyle="markdown-style" postData=myText as |post|}} +

by {{author}}

+ {{post.body bodyStyle="compact-style"}} +{{/blog-post}} +``` + +Components built this way are commonly referred to as "Contextual Components", +allowing inner components to be wrapped within the context of outer components without breaking encapsulation. diff --git a/guides/v3.6.0/configuring-ember/build-targets.md b/guides/v3.6.0/configuring-ember/build-targets.md new file mode 100644 index 0000000000..551ea4818b --- /dev/null +++ b/guides/v3.6.0/configuring-ember/build-targets.md @@ -0,0 +1,74 @@ +Ember CLI by default uses [Babel.js](https://babeljs.io/) to allow you to use tomorrow's JavaScript, today. + +It will ensure that you can use the newest features in the language and know that they will be transformed to JavaScript that can run in every browser you support. +That usually means generating ES5-compatible code that can work on any modern browser, back to Internet Explorer 11. + +But ES5 code is usually more verbose than the original Javascript, and over time, as browsers gain the ability to execute the new features in JavaScript and older browsers lose users, many users won't really want this verbose code as it increases their app's size and load times. + +That is why Ember CLI exposes a way of configuring what browsers your app targets. +It can figure out automatically what features are supported by the browsers you are targeting, +and apply the minimum set of transformations possible to your code. + +If you open `config/targets.js`, you will find the following code: + +```javascript {data-filename=config/targets.js} +const browsers = [ + 'last 1 Chrome versions', + 'last 1 Firefox versions', + 'last 1 Safari versions' +]; + +const isCI = !!process.env.CI; +const isProduction = process.env.EMBER_ENV === 'production'; + +if (isCI || isProduction) { + browsers.push('ie 11'); +} + +module.exports = { + browsers +}; +``` + +That default configuration has two parts. +In `production` environment it matches the wider set of browsers that Ember.js itself supports. +In non-production builds a narrower subset of browsers is used to provide the best experience +for developers to debug code that is much closer to what they actually wrote. + +However, if your app does not need to support IE anymore, you could change it to: + +```javascript {data-filename=config/targets.js} +module.exports = { + browsers: [ + 'last 1 edge versions', + 'last 1 Chrome versions', + 'last 1 Firefox versions', + 'last 1 Safari versions' + ] +}; +``` + +You are left with browsers that have full support of ES2015 and ES2016. +If you inspect the compiled code, you will see that some features are not compiled to ES5 code anymore, such as arrow functions and async/await. + +This feature is backed by [Browserlist](https://github.com/ai/browserslist) and [Can I Use](http://caniuse.com/). +These websites track usage stats of browsers, so you can use complex queries based on the user base of every browser. + +If you want to target all browsers with more than a 4% market share in Canada, +you'd have the following options: + +```javascript {data-filename=config/targets.js} +module.exports = { + browsers: [ + '> 4% in CA' + ] +}; +``` + +It is very important that you properly configure the targets of your app so you get the smallest and fastest code possible. + +Build targets can also be leveraged in other ways. + +Some addons might conditionally include polyfills only if needed. +Some linters may emit warnings when using features not yet fully supported in your targets. +Some addons may even automatically prefix unsupported CSS properties. diff --git a/guides/v3.6.0/configuring-ember/configuring-ember-cli.md b/guides/v3.6.0/configuring-ember/configuring-ember-cli.md new file mode 100644 index 0000000000..b28d9fa4d7 --- /dev/null +++ b/guides/v3.6.0/configuring-ember/configuring-ember-cli.md @@ -0,0 +1,12 @@ +In addition to configuring your app itself, you can also configure Ember CLI. +These configurations can be made by adding them to the `.ember-cli` file in your application's root. Many can also be made by passing them as arguments to the command line program. + +For example, a common desire is to change the port that Ember CLI serves the app from. It's possible to pass the port number from the command line with `ember server --port 8080`. To make this configuration permanent, edit your `.ember-cli` file like so: + +```json +{ + "port": 8080 +} +``` + +For a full list of command line options, run `ember help`. diff --git a/guides/v3.6.0/configuring-ember/configuring-your-app.md b/guides/v3.6.0/configuring-ember/configuring-your-app.md new file mode 100644 index 0000000000..05d9e73511 --- /dev/null +++ b/guides/v3.6.0/configuring-ember/configuring-your-app.md @@ -0,0 +1,19 @@ +Ember CLI ships with support for managing your application's environment. Ember CLI will setup a default environment config file at `config/environment`. Here, you can define an `ENV` object for each environment, which are currently limited to three: development, test, and production. + +The ENV object contains three important keys: + + - `EmberENV` can be used to define Ember feature flags (see the [Feature Flags guide](../feature-flags/)). + - `APP` can be used to pass flags/options to your application instance. + - `environment` contains the name of the current environment (`development`,`production` or `test`). + +You can access these environment variables in your application code by importing from `your-application-name/config/environment`. + +For example: + +```javascript +import ENV from 'your-application-name/config/environment'; + +if (ENV.environment === 'development') { + // ... +} +``` diff --git a/guides/v3.6.0/configuring-ember/debugging.md b/guides/v3.6.0/configuring-ember/debugging.md new file mode 100644 index 0000000000..48c0425e96 --- /dev/null +++ b/guides/v3.6.0/configuring-ember/debugging.md @@ -0,0 +1,145 @@ +Ember provides several configuration options that can help you debug problems +with your application. + +## Routing + +#### Log router transitions + +```javascript {data-filename=app/app.js} +import Application from '@ember/application'; + +export default Application.extend({ + // Basic logging, e.g. "Transitioned into 'post'" + LOG_TRANSITIONS: true, + + // Extremely detailed logging, highlighting every internal + // step made while transitioning into a route, including + // `beforeModel`, `model`, and `afterModel` hooks, and + // information about redirects and aborted transitions + LOG_TRANSITIONS_INTERNAL: true +}); +``` +## Views / Templates + +#### Log view lookups + +```javascript {data-filename=config/environment.js} +ENV.APP.LOG_VIEW_LOOKUPS = true; +``` +#### View all registered templates +```javascript +Ember.keys(Ember.TEMPLATES) +``` + +## Controllers + +#### Log generated controller + +```javascript {data-filename=config/environment.js} +ENV.APP.LOG_ACTIVE_GENERATION = true; +``` + +## Observers / Binding + +#### See all observers for an object, key + +```javascript +Ember.observersFor(comments, keyName); +``` + +#### Log object bindings + +```javascript {data-filename=config/environments.js} +ENV.APP.LOG_BINDINGS = true; +``` + +## Miscellaneous + +#### Turn on resolver resolution logging + +This option logs all the lookups that are done to the console. Custom objects +you've created yourself have a tick, and Ember generated ones don't. + +It's useful for understanding which objects Ember is finding when it does a lookup +and which it is generating automatically for you. + +```javascript {data-filename=app/app.js} +import Application from '@ember/application'; + +export default Application.extend({ + LOG_RESOLVER: true +}); +``` +#### Dealing with deprecations + +```javascript +Ember.ENV.RAISE_ON_DEPRECATION = true; +Ember.ENV.LOG_STACKTRACE_ON_DEPRECATION = true; +``` + + +#### Implement an Ember.onerror hook to log all errors in production + +```javascript +Ember.onerror = function(error) { + Ember.$.ajax('/error-notification', { + type: 'POST', + data: { + stack: error.stack, + otherInformation: 'exception message' + } + }); +} +``` + +#### Import the console + +If you are using imports with Ember, be sure to import the console: + +```javascript +Ember = { + imports: { + Handlebars: Handlebars, + jQuery: $, + console: window.console + } +}; +``` + +#### Errors within an `RSVP.Promise` + +There are times when dealing with promises that it seems like any errors +are being 'swallowed', and not properly raised. This makes it extremely +difficult to track down where a given issue is coming from. Thankfully, +`RSVP` has a solution for this problem built in. + +You can provide an `onerror` function that will be called with the error +details if any errors occur within your promise. This function can be anything, +but a common practice is to call `console.assert` to dump the error to the +console. + +```javascript {data-filename=app/app.js} +import { assert } from '@ember/debug'; +import RSVP from 'rsvp'; + +RSVP.on('error', function(error) { + assert(error, false); +}); +``` + +#### Errors within `Ember.run.later` ([Backburner.js](https://github.com/ebryn/backburner.js)) + +Backburner has support for stitching the stacktraces together so that you can +track down where an error thrown by `Ember.run.later` is being initiated from. Unfortunately, +this is quite slow and is not appropriate for production or even normal development. + +To enable full stacktrace mode in Backburner, and thus determine the stack of the task +when it was scheduled onto the run loop, you can set: + +```javascript +Ember.run.backburner.DEBUG = true; +``` + +Once the `DEBUG` value is set to `true`, when you are at a breakpoint you can navigate +back up the stack to the `flush` method in and check the `errorRecordedForStack.stack` +value, which will be the captured stack when this job was scheduled. diff --git a/guides/v3.6.0/configuring-ember/disabling-prototype-extensions.md b/guides/v3.6.0/configuring-ember/disabling-prototype-extensions.md new file mode 100644 index 0000000000..0d32529a38 --- /dev/null +++ b/guides/v3.6.0/configuring-ember/disabling-prototype-extensions.md @@ -0,0 +1,164 @@ +By default, Ember.js will extend the prototypes of native JavaScript +objects in the following ways: + +* `Array` is extended to implement the `Ember.Enumerable`, + `Ember.MutableEnumerable`, `Ember.MutableArray` and `Ember.Array` + interfaces. This polyfills ECMAScript 5 array methods in browsers that + do not implement them, adds convenience methods and properties to + built-in arrays, and makes array mutations observable. + +* `String` is extended to add convenience methods, such as + `camelize()` and `w()`. You can find a list of these methods with the + [Ember.String documentation](https://www.emberjs.com/api/ember/release/classes/String). + +* `Function` is extended with methods to annotate functions as + computed properties, via the `property()` method, and as observers, + via the `observes()` method. Use of these methods + is now discouraged and not covered in recent versions of the Guides. + +This is the extent to which Ember.js enhances native prototypes. We have +carefully weighed the tradeoffs involved with changing these prototypes, +and recommend that most Ember.js developers use them. These extensions +significantly reduce the amount of boilerplate code that must be typed. + +However, we understand that there are cases where your Ember.js +application may be embedded in an environment beyond your control. The +most common scenarios are when authoring third-party JavaScript that is +embedded directly in other pages, or when transitioning an application +piecemeal to a more modern Ember.js architecture. + +In those cases, where you can't or don't want to modify native +prototypes, Ember.js allows you to completely disable the extensions +described above. + +To do so, simply set the `EmberENV.EXTEND_PROTOTYPES` flag to `false`: + +```javascript {data-filename=config/environment.js} +ENV = { + EmberENV: { + EXTEND_PROTOTYPES: false + } +} +``` + +You can configure which classes to include prototype extensions +for in your application's configuration like so: + +```javascript {data-filename=config/environment.js} +ENV = { + EmberENV: { + EXTEND_PROTOTYPES: { + String: false, + Array: true + } + } +} +``` + +## Life Without Prototype Extension + +In order for your application to behave correctly, you will need to +manually extend or create the objects that the native objects were +creating before. + +### Arrays + +Native arrays will no longer implement the functionality needed to +observe them. If you disable prototype extension and attempt to use +native arrays with things like a template's `{{#each}}` helper, Ember.js +will have no way to detect changes to the array and the template will +not update as the underlying array changes. + +You can manually coerce a native array into an array that implements the +required interfaces using the convenience method `Ember.A`: + +```javascript +import { A } from '@ember/array'; + +let islands = ['Oahu', 'Kauai']; +islands.includes('Oahu'); +// => TypeError: Object Oahu,Kauai has no method 'includes' + +// Convert `islands` to an array that implements the +// Ember enumerable and array interfaces +A(islands); + +islands.includes('Oahu'); +// => true +``` + +### Strings + +Strings will no longer have the convenience methods described in the +[`Ember.String` API reference](https://www.emberjs.com/api/ember/release/classes/String). +Instead, +you can use the similarly-named methods of the `Ember.String` object and +pass the string to use as the first parameter: + +```javascript +import { camelize } from '@ember/string'; + +'my_cool_class'.camelize(); +// => TypeError: Object my_cool_class has no method 'camelize' + +camelize('my_cool_class'); +// => "myCoolClass" +``` + +### Functions + +The [Object Model](../../object-model/) section of the Guides describes +how to write computed properties, observers, and bindings without +prototype extensions. Below you can learn about how to convert existing +code to the format now encouraged. + +To annotate computed properties, use the `Ember.computed()` method to +wrap the function: + +```javascript +import { computed } from '@ember/object'; + +// This won't work: +fullName: function() { + return `${this.firstName} ${this.lastName}`; +}.property('firstName', 'lastName') + + +// Instead, do this: +fullName: computed('firstName', 'lastName', function() { + return `${this.firstName} ${this.lastName}`; +}) +``` + +Observers are annotated using `Ember.observer()`: + +```javascript +import { observer } from '@ember/object'; + +// This won't work: +fullNameDidChange: function() { + console.log('Full name changed'); +}.observes('fullName') + + +// Instead, do this: +fullNameDidChange: observer('fullName', function() { + console.log('Full name changed'); +}) +``` + +Evented functions are annotated using [`Ember.on()`](https://emberjs.com/api/ember/2.15/namespaces/Ember/methods/on?anchor=on): + +```javascript +import { on } from '@ember/object/evented'; + +// This won't work: +doStuffWhenInserted: function() { + /* awesome sauce */ +}.on('didInsertElement'); + +// Instead, do this: +doStuffWhenInserted: on('didInsertElement', function() { + /* awesome sauce */ +}); +``` diff --git a/guides/v3.6.0/configuring-ember/embedding-applications.md b/guides/v3.6.0/configuring-ember/embedding-applications.md new file mode 100644 index 0000000000..e73a9a2677 --- /dev/null +++ b/guides/v3.6.0/configuring-ember/embedding-applications.md @@ -0,0 +1,96 @@ +In most cases, your application's entire UI will be created by templates +that are managed by the router. + +But what if you have an Ember.js app that you need to embed into an +existing page, or run alongside other JavaScript frameworks, or serve from the +same domain as another app? + +### Changing the Root Element + +By default, your application will render the [application template](../../routing/defining-your-routes/#toc_the-application-route) +and attach it to the document's `body` element. + +You can tell the application to append the application template to a +different element by specifying its `rootElement` property: + +```javascript {data-filename="app/app.js" data-diff="+4"} +… + +App = Ember.Application.extend({ + rootElement: '#app', + modulePrefix: config.modulePrefix, + podModulePrefix: config.podModulePrefix, + Resolver +}); + +… +``` + +This property can be specified as either an element or a +[jQuery-compatible selector +string](http://api.jquery.com/category/selectors/). + +### Disabling URL Management + +You can prevent Ember from making changes to the URL by [changing the +router's `location`](../specifying-url-type/) to +`none`: + +```javascript {data-filename="config/environment.js" data-diff="-8,+9"} +/* eslint-env node */ + +module.exports = function(environment) { + var ENV = { + modulePrefix: 'my-blog', + environment: environment, + rootURL: '/', + locationType: 'auto', + locationType: 'none', + … + }; + + … + + return ENV; +} +``` + +### Specifying a Root URL + +If your Ember application is one of multiple web applications served from the same domain, it may be necessary to indicate to the router what the root URL for your Ember application is. By default, Ember will assume it is served from the root of your domain. + +For example, if you wanted to serve your blogging application from `http://emberjs.com/blog/`, it would be necessary to specify a root URL of `/blog/`. + +This can be achieved by configuring the `rootURL` property on `ENV`: + +```javascript {data-filename="config/environment.js" data-diff="-7,+8"} +/* eslint-env node */ + +module.exports = function(environment) { + var ENV = { + modulePrefix: 'my-blog', + environment: environment, + rootURL: '/', + rootURL: '/blog/', + locationType: 'auto', + … + }; +} +``` + +You will notice that this is then used to configure your application's router: + +```javascript {data-filename=app/router.js} +import Router from '@ember/routing/router'; +import config from './config/environment'; + +const Router = Router.extend({ + location: config.locationType, + rootURL: config.rootURL +}); + +Router.map(function() { +}); + +export default Router; +``` diff --git a/guides/v3.6.0/configuring-ember/feature-flags.md b/guides/v3.6.0/configuring-ember/feature-flags.md new file mode 100644 index 0000000000..ca2d81f13d --- /dev/null +++ b/guides/v3.6.0/configuring-ember/feature-flags.md @@ -0,0 +1,62 @@ +New features are added to Ember.js within conditional statements. + +Code behind these flags can be conditionally enabled +(or completely removed) based on your project's configuration. This +allows newly developed features to be selectively released when the +Ember.js community considers them ready for production use. + +## Feature Life-Cycle +A newly-flagged feature is only available in canary builds and can be enabled +at runtime through your project's configuration file. + +At the start of a beta cycle the Ember core team evaluates each new feature. +Features deemed stable are made available in the next beta and enabled by default. + +Beta features that receive negative feedback from the community are disabled in the next beta point +release, and are not included in the next stable release. They may still be included +in the next beta cycle if the issues/concerns are resolved. + +Once the beta cycle has completed, the next stable release will include any features that +were enabled during the beta cycle. At this point the feature flags will be removed from +the canary and future beta branches, and the feature becomes part of the framework. + +## Flagging Details +The flag status in the generated build is controlled by the [`features.json`](https://github.com/emberjs/ember.js/blob/master/features.json) +file in the root of the Ember.js project. This file lists all new features and their current status. + +A feature can have one of a three flags: + +* `true` - The feature is **present** and **enabled**: the code behind the flag is always enabled in + the generated build. +* `null` - The feature is **present** but **disabled** in the build output. It must be enabled at + runtime. +* `false` - The feature is entirely **disabled**: the code behind the flag is not present in + the generated build. + +The process of removing the feature flags from the resulting build output is +handled by [`defeatureify`](https://github.com/thomasboyt/defeatureify). + +## Feature Listing ([`FEATURES.md`](https://github.com/emberjs/ember.js/blob/master/FEATURES.md)) + +When a developer adds a new feature to the `canary` channel (i.e. the `master` branch on github), they +also add an entry to [`FEATURES.md`](https://github.com/emberjs/ember.js/blob/master/FEATURES.md) +explaining what the feature does, and linking to their originating pull request. +This list is kept current, and reflects what is available in each channel +(`release`, `beta`, and `canary`). + +## Enabling At Runtime +When using the Ember.js canary or beta builds you can enable a "**present** but **disabled**" +feature by setting its flag value to `true` before your application boots: + +```javascript {data-filename=config/environment.js} +let ENV = { + EmberENV: { + FEATURES: { + 'link-to': true + } + } +}; +``` + +For the truly ambitious developer, setting `ENV.EmberENV.ENABLE_ALL_FEATURES` to `true` will enable all +experimental features. diff --git a/guides/v3.6.0/configuring-ember/handling-deprecations.md b/guides/v3.6.0/configuring-ember/handling-deprecations.md new file mode 100644 index 0000000000..d55ad9b3ca --- /dev/null +++ b/guides/v3.6.0/configuring-ember/handling-deprecations.md @@ -0,0 +1,145 @@ +A valuable attribute of the Ember framework is its use of [Semantic Versioning](http://semver.org/) to aid projects in keeping up with +changes to the framework. Before any functionality or API is removed it first goes through a deprecation period where the functionality is +still supported, but usage of it generates a warning logged to the browser console. These warnings can pile up between major releases to a point where the amount of +deprecation warnings that scroll through the console becomes overwhelming. + + + +Fortunately, Ember provides a way for projects to deal with deprecations in an organized and efficient manner. + +## Filtering Deprecations + +When your project has a lot of deprecations, you can start by filtering out deprecations that do not have to be addressed right away. You +can use the [deprecation handlers](https://emberjs.com/api/ember/2.15/classes/Ember.Debug/methods/registerDeprecationHandler?anchor=registerDeprecationHandler) API to check for what +release a deprecated feature will be removed. An example handler is shown below that filters out all deprecations that are not going away +in release 2.0.0. + + +```javascript {data-filename= app/initializers/main.js} +import Ember from 'ember'; + +export function initialize() { + if (Ember.Debug && typeof Ember.Debug.registerDeprecationHandler === 'function') { + Ember.Debug.registerDeprecationHandler((message, options, next) => { + if (options && options.until && options.until !== '2.0.0') { + return; + } else { + next(message, options); + } + }); + } +} + +export default { initialize }; +``` + +The deprecation handler API was released in Ember 2.1. If you would like to leverage this API in a prior release of Ember you can install +the [ember-debug-handlers-polyfill](http://emberobserver.com/addons/ember-debug-handlers-polyfill) addon into your project. + +## Deprecation Workflow + +Once you've removed deprecations that you may not need to immediately address, you may still be left with many deprecations. Also, your remaining +deprecations may only occur in very specific scenarios that are not obvious. How then should you go about finding and fixing these? This +is where the [ember-cli-deprecation-workflow](http://emberobserver.com/addons/ember-cli-deprecation-workflow) addon can be extremely helpful. + +Once installed, the addon works in 3 steps: + +### 1. Gather deprecations into one source + +The ember-cli-deprecation-workflow addon provides a command that will collect deprecations from your console and generate JavaScript code listing +its findings. + +To collect deprecations, first run your in-browser test suite by starting your development server and navigating to [`http://localhost:4200/tests`](http://localhost:4200/tests). If your test suite isn't fully covering your app's functionality, you may also +manually exercise functionality within your app where needed. Once you've exercised the app to your satisfaction, run the following command within +your browser console: `deprecationWorkflow.flushDeprecations()`. This will print to the console JavaScript code, which you should then copy to a +new file in your project called `config/deprecation-workflow.js` + + + +Here's an example of a deprecation-workflow file after generated from the console: + +```javascript {data-filename= config/deprecation-workflow.js} +window.deprecationWorkflow = window.deprecationWorkflow || {}; +window.deprecationWorkflow.config = { + workflow: [ + { handler: "silence", matchMessage: "Ember.Handlebars.registerHelper is deprecated, please refactor to Ember.Helper.helper." }, + { handler: "silence", matchMessage: "`lookup` was called on a Registry. The `initializer` API no longer receives a container, and you should use an `instanceInitializer` to look up objects from the container." }, + { handler: "silence", matchMessage: "Using `Ember.HTMLBars.makeBoundHelper` is deprecated. Please refactor to using `Ember.Helper` or `Ember.Helper.helper`." }, + { handler: "silence", matchMessage: "Accessing 'template' in is deprecated. To determine if a block was specified to please use '{{#if hasBlock}}' in the components layout." }, + { handler: "silence", matchMessage: "Accessing 'template' in is deprecated. To determine if a block was specified to please use '{{#if hasBlock}}' in the components layout." }, + { handler: "silence", matchMessage: "Accessing 'template' in is deprecated. To determine if a block was specified to please use '{{#if hasBlock}}' in the components layout." } + ] +}; +``` + +You might notice that you have a lot of duplicated messages in your workflow file, like the 3 messages in the above example that start with +`Accessing 'template' in...`. This is because some of the deprecation messages provide context to the specific deprecation, making them +different than the same deprecation in other parts of the app. If you want to consolidate the +duplication, you can use a simple regular expression with a wildcard (`.*`) for the part of the message that varies per instance. + +Below is the same deprecation-workflow file as above, now with a regular expression on line 7 to remove some redundant messages. Note that the double quotes around `matchMessage` have also been replaced with forward slashes. + +```javascript {data-filename= config/deprecation-workflow.js} +window.deprecationWorkflow = window.deprecationWorkflow || {}; +window.deprecationWorkflow.config = { + workflow: [ + { handler: "silence", matchMessage: "Ember.Handlebars.registerHelper is deprecated, please refactor to Ember.Helper.helper." }, + { handler: "silence", matchMessage: "`lookup` was called on a Registry. The `initializer` API no longer receives a container, and you should use an `instanceInitializer` to look up objects from the container." }, + { handler: "silence", matchMessage: "Using `Ember.HTMLBars.makeBoundHelper` is deprecated. Please refactor to using `Ember.Helper` or `Ember.Helper.helper`." }, + { handler: "silence", matchMessage: /Accessing 'template' in .* is deprecated. To determine if a block was specified to .* please use '{{#if hasBlock}}' in the components layout./ } + ] +}; +``` + +Rerun your test suite as you make updates to your workflow file and you should validate that your deprecations are gone. Once that is completed, +you can proceed with enhancing your application without the sea of deprecation warnings clouding your log. + +### 2. "Turn on" a deprecation +Once you have built your `deprecation-workflow.js` file and your deprecations are silenced, you can begin to work on deprecations one by one +at your own leisure. To find deprecations, you can change the handler value of that message to either `throw` or `log`. Throw will +throw an actual exception when the deprecation is encountered, so that tests that use the deprecated feature will fail. Choosing to log will +simply log a warning to the console as before. These settings give you some flexibility on how you want to go about fixing the +deprecations. + +The code below is the deprecation-workflow file with the first deprecation set to throw an exception on occurrence. The image demonstrates what +that deprecation looks like when you run your tests. + +```javascript {data-filename= config/deprecation-workflow.js} +window.deprecationWorkflow = window.deprecationWorkflow || {}; +window.deprecationWorkflow.config = { + workflow: [ + { handler: "throw", matchMessage: "Ember.Handlebars.registerHelper is deprecated, please refactor to Ember.Helper.helper." }, + { handler: "silence", matchMessage: "`lookup` was called on a Registry. The `initializer` API no longer receives a container, and you should use an `instanceInitializer` to look up objects from the container." }, + { handler: "silence", matchMessage: "Using `Ember.HTMLBars.makeBoundHelper` is deprecated. Please refactor to using `Ember.Helper` or `Ember.Helper.helper`." }, + { handler: "silence", matchMessage: /Accessing 'template' in .* is deprecated. To determine if a block was specified to .* please use '{{#if hasBlock}}' in the components layout./ } + ] +}; +``` + + + +### 3. Fix and Repeat +After fixing a deprecation and getting your scenarios working again, you might want to leave the deprecation message in the workflow file with the +throw handler enabled. This will ensure you haven't missed anything, and ensure no new deprecated calls of that type are introduced to your project. +Next, it's just a matter of going down the list, updating the handler, and fixing each remaining deprecation. + +In the end, your deprecations can be fully turned on as "throw" and you should be able to use your application without error. At this point, you can +go ahead and update your Ember version! When you upgrade, be sure you remove the deprecations you've fixed from the deprecation workflow file, +so that you can start the process over for the next release. + +## Silencing Deprecation Warnings During Compile + +As you upgrade between releases, you might also notice that your terminal log begins to stream template-related deprecation warnings during the compile process, making +it difficult to review your compilation logs. + + + +If you are using the deprecation workflow process above, you will likely prefer to gather these warnings during runtime execution instead. The way to hide these +warnings during compile is to install the [ember-cli-template-lint](http://emberobserver.com/addons/ember-cli-template-lint) addon. It suppresses +template deprecation warnings during compile in favor of showing them in the browser console during test suite execution or application usage. + +## Deprecation Handling in Ember Inspector + +Ember Inspector also provides deprecation handling capability. It can work complimentary to ember-cli-deprecation-workflow. As you unsilence deprecations to +fix them, the inspector can allow you to more quickly find where in your code a deprecation occurs when you run into it at runtime, reducing the amount of +stack trace browsing you have to do. For more information on using deprecation handling in Ember Inspector, see its [guides section](../../ember-inspector/deprecations/). diff --git a/guides/v3.6.0/configuring-ember/specifying-url-type.md b/guides/v3.6.0/configuring-ember/specifying-url-type.md new file mode 100644 index 0000000000..4f78b56dc8 --- /dev/null +++ b/guides/v3.6.0/configuring-ember/specifying-url-type.md @@ -0,0 +1,45 @@ +The Ember router has four options to manage your application's URL: `history`, +which uses the HTML5 History API; `hash`, which uses anchor-based URLs; `auto`, +which uses `history` if supported by the user's browser, and falls back to +`hash` otherwise; and `none`, which doesn't update the URL. By default, Ember +CLI configures the router to use `auto`. You can change this option in +`config/environment.js` under `ENV.locationType`. + +## history + +When using `history`, Ember uses the browser's +[history](http://caniuse.com/history) API to produce URLs with a structure like +`/posts/new`. + +Given the following router, entering `/posts/new` will take you to the `posts.new` +route. + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('posts', function() { + this.route('new'); + }); +}); +``` + +Keep in mind that your server must serve the Ember app from all the URLs defined in your +`Router.map` function. In other words, if your user directly navigates to +`/posts/new`, your server must be configured to serve your Ember app in +response. + +## hash + +The `hash` option uses the URL's anchor to load the starting state of your +application and will keep it in sync as you move around. At present, this relies +on a [hashchange](http://caniuse.com/hashchange) event existing in the browser. + +In the router example above, entering `/#/posts/new` will take you to the `posts.new` +route. + +## none + +Finally, if you don't want the browser's URL to interact with your application +at all, you can disable the location API entirely by setting `ENV.locationType` +to `none`. This is useful for +testing, or when you don't want Ember to muck with the URL (for example when you embed your +application in a larger page). diff --git a/guides/v3.6.0/contributing/adding-new-features.md b/guides/v3.6.0/contributing/adding-new-features.md new file mode 100644 index 0000000000..25a3723a65 --- /dev/null +++ b/guides/v3.6.0/contributing/adding-new-features.md @@ -0,0 +1,163 @@ +In general, new feature development should be done on master. + +Bugfixes should not introduce new APIs or break existing APIs, and do +not need feature flags. + +Features can introduce new APIs, and need feature flags. They should not +be applied to the release or beta branches, since SemVer requires +bumping the minor version to introduce new features. + +Security fixes should not introduce new APIs, but may, if strictly +necessary, break existing APIs. Such breakages should be as limited as +possible. + +### Bug Fixes + +#### Urgent Bug Fixes + +Urgent bugfixes are bugfixes that need to be applied to the existing +release branch. If possible, they should be made on master and prefixed +with [BUGFIX release]. + +#### Beta Bug Fixes + +Beta bugfixes are bugfixes that need to be applied to the beta branch. +If possible, they should be made on master and tagged with [BUGFIX +beta]. + +#### Security Fixes + +Security fixes need to be applied to the beta branch, the current +release branch, and the previous tag. If possible, they should be made +on master and tagged with [SECURITY]. + +### Features + +Features must always be wrapped in a feature flag. Tests for the feature +must also be wrapped in a feature flag. + +Because the build-tools will process feature-flags, flags must use +precisely this format. We are choosing conditionals rather than a block +form because functions change the surrounding scope and may introduce +problems with early return. + +```javascript +if (Ember.FEATURES.isEnabled("feature")) { + // implementation +} +``` + +Tests will always run with all features on, so make sure that any tests +for the feature are passing against the current state of the feature. + +#### Commits + +Commits related to a specific feature should include a prefix like +[FEATURE htmlbars]. This will allow us to quickly identify all commits +for a specific feature in the future. Features will never be applied to +beta or release branches. Once a beta or release branch has been cut, it +contains all of the new features it will ever have. + +If a feature has made it into beta or release, and you make a commit to +master that fixes a bug in the feature, treat it like a bugfix as +described above. + +#### Feature Naming Conventions + +```javascript {data-filename=config/environment.js} +Ember.FEATURES['-'] // if package specific +Ember.FEATURES['container-factory-injections'] +Ember.FEATURES['htmlbars'] +``` + +### Builds + +The Canary build, which is based off master, will include all features, +guarded by the conditionals in the original source. This means that +users of the canary build can enable whatever features they want by +enabling them before creating their Ember.Application. + +```javascript {data-filename=config/environment.js} +module.exports = function(environment) { + let ENV = { + EmberENV: { + FEATURES: { + htmlbars: true + } + }, + } +} +``` + +### `features.json` + +The root of the repository will contain a `features.json` file, which will +contain a list of features that should be enabled for beta or release +builds. + +This file is populated when branching, and may not gain additional +features after the original branch. It may remove features. + +```javascript +{ + "htmlbars": true +} +``` + +The build process will remove any features not included in the list, and +remove the conditionals for features in the list. + +### Travis Testing + +For a new PR: + +1. Travis will test against master with all feature flags on. +2. If a commit is tagged with [BUGFIX beta], Travis will also + cherry-pick the commit into beta, and run the tests on that + branch. If the commit doesn't apply cleanly or the tests fail, the + tests will fail. +3. If a commit is tagged with [BUGFIX release], Travis will also cherry-pick + the commit into release, and run the test on that branch. If the commit + doesn't apply cleanly or the tests fail, the tests will fail. + +For a new commit to master: + +1. Travis will run the tests as described above. +2. If the build passes, Travis will cherry-pick the commits into the + appropriate branches. + +The idea is that new commits should be submitted as PRs to ensure they +apply cleanly, and once the merge button is pressed, Travis will apply +them to the right branches. + +### Go/No-Go Process + +Every six weeks, the core team goes through the following process. + +#### Beta Branch + +All remaining features on the beta branch are vetted for readiness. If +any feature isn't ready, it is removed from `features.json`. + +Once this is done, the beta branch is tagged and merged into release. + +#### Master Branch + +All features on the master branch are vetted for readiness. In order for +a feature to be considered "ready" at this stage, it must be ready as-is +with no blockers. Features are a no-go even if they are close and +additional work on the beta branch would make it ready. + +Because this process happens every six weeks, there will be another +opportunity for a feature to make it soon enough. + +Once this is done, the master branch is merged into beta. A +`features.json` file is added with the features that are ready. + +### Beta Releases + +Every week, we repeat the Go/No-Go process for the features that remain +on the beta branch. Any feature that has become unready is removed from +the `features.json`. + +Once this is done, a Beta release is tagged and pushed. diff --git a/guides/v3.6.0/contributing/repositories.md b/guides/v3.6.0/contributing/repositories.md new file mode 100644 index 0000000000..cb88849211 --- /dev/null +++ b/guides/v3.6.0/contributing/repositories.md @@ -0,0 +1,59 @@ +Ember is made up of several libraries. If you wish to add a feature or fix a bug please file a pull request against the appropriate repository. Be sure to check the libraries listed below before making changes in the Ember repository. + +# Main Repositories +**Ember.js** - The main repository for Ember. + +* [https://github.com/emberjs/ember.js](https://github.com/emberjs/ember.js) + +**Ember Data** - A data persistence library for Ember. + +* [https://github.com/emberjs/data](https://github.com/emberjs/data) + +**Ember Website** - Source for [http://emberjs.com](http://emberjs.com) + +* [https://github.com/emberjs/website](https://github.com/emberjs/website) + +**Ember Guides** - Source for [http://guides.emberjs.com](http://guides.emberjs.com) which you are currently reading. + +* [https://github.com/ember-learn/guides-source](https://github.com/ember-learn/guides-source) +* [https://github.com/ember-learn/guides-app](https://github.com/ember-learn/guides-app) + +# Libraries Used By Ember + +These libraries are part of the Ember asset output, but development of them takes place in a separate repository. + +## `Backburner` +* **backburner.js** - Implements the Ember run loop. +* [https://github.com/ebryn/backburner.js](https://github.com/ebryn/backburner.js) + +## `DAG Map` +* **dag-map** - A directed acyclic graph data structure for javascript +* [https://github.com/krisselden/dag-map](https://github.com/krisselden/dag-map) + +## `Glimmer 2` +* **glimmer** - Implements the really fast rendering engine now included in Ember +* [https://github.com/tildeio/glimmer](https://github.com/tildeio/glimmer) + +## `HTMLBars` +* **htmlbars** - The syntax for templating most often used with Ember +* [https://github.com/tildeio/htmlbars](https://github.com/tildeio/htmlbars) + +## `morph-range` + +* **morph-range** - Used by Ember for manipulating the text nodes known as morphs which are created for HTMLBars to keep track of text that could change. +* [https://github.com/krisselden/morph-range](https://github.com/krisselden/morph-range) + +## `Route Recognizer` + +* **route-recognizer** - A lightweight JavaScript library that matches paths against registered routes. +* [https://github.com/tildeio/route-recognizer](https://github.com/tildeio/route-recognizer) + +## `router.js` + +* **router.js** - A lightweight JavaScript library that builds on route-recognizer and rsvp to provide an API for handling routes. +* [https://github.com/tildeio/router.js](https://github.com/tildeio/router.js) + +## `RSVP` + +* **rsvp.js** - Implementation of the of Promises/A+ spec used by Ember. +* [https://github.com/tildeio/rsvp.js](https://github.com/tildeio/rsvp.js) diff --git a/guides/v3.6.0/controllers/index.md b/guides/v3.6.0/controllers/index.md new file mode 100644 index 0000000000..4730d833d9 --- /dev/null +++ b/guides/v3.6.0/controllers/index.md @@ -0,0 +1,107 @@ +### What is a Controller? + +A [Controller](https://emberjs.com/api/ember/release/classes/Controller) is routable object which receives a single property from the Route – `model` – which is the return value of the Route's [`model()`](https://emberjs.com/api/ember/3.3/classes/Route/methods?anchor=model) method. + +The model is passed from the Route to the Controller by default using the [`setupController()`](https://www.emberjs.com/api/ember/3.3/classes/Route/methods/setupController?anchor=setupController) function. The Controller is then often used to decorate the model with display properties such as retrieving the full name from a name model. + +A Controller is usually paired with an individual Route of the same name. + +[Routing Query Parameters](../routing/query-params/) should be defined within a controller. + +### Defining a Controller + +We only need to generate a Controller file if we want to customize the properties or provide any actions to the Route. If we have no customizations, Ember will provide a default Controller instance for us at run time. + +To generate a controller, run +```bash +ember generate controller my-controller-name +``` + +This creates a controller file at `app/controllers/my-controller-name.js`, and a unit test file at `tests/unit/controllers/my-controller-name-test.js`. + +The controller name `my-controller-name` must match the name of the Route that renders it. So if the Route is named `blog-post`, it should have a matching Controller named `blog-post`. The matching file names of the Controller and the Route signals to Ember that this Controller must be used when landing on the `blog-post` Route. + +### Where and When to use Controllers? + +Controllers are used as an extension of the model loaded from the Route. Any attributes or actions that we want to share with components used on that Route could be defined on the Controller and passed down through the Route’s template. + +Controllers are singletons so we should avoid keeping state that does not derive from either the Model or Query Parameters since these would persist in between activations such as when a user leaves the Route and then re-enters it. + +Controllers can also contain actions that enable the Route's components to update the Model or Query Parameters through it using Computed Properties. + +### Basic Controller Example + +Let's explore these concepts using an example of a route displaying a blog post. Assume that the route returns a `BlogPost` model that is presented in the template. + +The `BlogPost` model would have properties like: + +* `title` +* `intro` +* `body` +* `author` + +In the example below, we can see how the template is using the model properties to display some data. + +```handlebars {data-filename=app/templates/blog-post.hbs} +

{{model.title}}

+

by {{model.author}}

+ +
+ {{model.intro}} +
+
+
+ {{model.body}} +
+``` + +Consider the example where we want to have a controller for a `blog-post` route. In this controller, we are looking to keep track if the user has expanded the body or not. + +```javascript {data-filename=app/controllers/blog-post.js} +import Controller from '@ember/controller'; + +export default Controller.extend({ + isExpanded: false, + + actions: { + toggleBody() { + this.toggleProperty('isExpanded'); + } + } +}); +``` + +The property `isExpanded` keeps track if the user has expanded the body or not. The action `toggleBody()` provides a way for the user to provide their setting. Both of the them are used in the updated template below. + +```handlebars {data-filename=app/templates/blog-post.hbs} +

{{model.title}}

+

by {{model.author}}

+ +
+ {{model.intro}} +
+
+ +{{#if isExpanded}} + +
+ {{model.body}} +
+{{else}} + +{{/if}} +``` + +We can see that if the property `isExpanded` is toggled to true, we will show the body property of the model to the user. This `isExpanded` is stored in the controller. + +### Common questions + +###### Should we use controllers in my application? I've heard they're going away! + +Yes! Controllers are still an integral part of an Ember application architecture, and generated by the framework even if you don't declare a Controller module explicitly. + +###### When should we create a Controller? + +* We want to pass down actions or variables to share with a Route’s child components +* We have a computed property that depends on the results of the model hook +* We need to support query parameters diff --git a/guides/v3.6.0/ember-inspector/component-tree.md b/guides/v3.6.0/ember-inspector/component-tree.md new file mode 100644 index 0000000000..990ca11b61 --- /dev/null +++ b/guides/v3.6.0/ember-inspector/component-tree.md @@ -0,0 +1,50 @@ +The Components tab displays a collapsible representation of the views and components that are currently being rendered. Selecting a component from the tree will open it in the [Object Inspector](../object-inspector/). + + + +Components will be displayed with curly braces. Views are grayed out and not selectable. Use the [View Tree](../view-tree/) to get more information on Views that are being rendered. + +### Scrolling to a Component in the Browser + +Clicking the 'eye' icon to the right of a component will scroll that component into view in the browser. + +### Expanding and Collapsing Components + +Components can have their children hidden and shown by clicking the caret just to the left of the component. + +The two icons to the left of the search field will expand or collapse all components. + + + +### Filtering Components + +By typing in the search field you can limit the components that are shown in the tree. + + + +### Highlighting Templates + +#### Hovering over the Component Tree + +When you hover over the items in the Component Tree, the related component will be +highlighted in your app. For every highlighted component, you can see the +template name and its associated objects. + + + +#### Hovering over the app + +If you want to highlight a component directly within your app, click on the icon to the left of the search bar (this is the same behavior as the [View Tree](../view-tree/)) +As your our mouse passes over it, the related component will be +highlighted. + + + + +If you click on a highlighted template or component, the Inspector will select it. You can then +click on the backing objects to send them to the object inspector. + + + +Click on the `X` button to deselect a template. + diff --git a/guides/v3.6.0/ember-inspector/container.md b/guides/v3.6.0/ember-inspector/container.md new file mode 100644 index 0000000000..a3ee5be620 --- /dev/null +++ b/guides/v3.6.0/ember-inspector/container.md @@ -0,0 +1,19 @@ +Every Ember application has a container that maintains object instances for you. You can +inspect these instances using the Container tab. This is useful for objects +that don't fall under a dedicated menu, such as services. + + + +Click on the Container tab, and you will see a list of instances the container is holding. Click on a type to see the list of all instances of that type maintained by the container. + +### Inspecting Instances + +Click on a row to inspect a given instance using the Object Inspector. + + + +### Filter and Reload + +You can reload the container tab by clicking on the reload icon. To search for instances, type a query in the search box. + + diff --git a/guides/v3.6.0/ember-inspector/data.md b/guides/v3.6.0/ember-inspector/data.md new file mode 100644 index 0000000000..bcdd66f20c --- /dev/null +++ b/guides/v3.6.0/ember-inspector/data.md @@ -0,0 +1,39 @@ +You can inspect your models by clicking on the `Data` tab. Check out [Building a Data Custom Adapter][building-data-adapter] below if you maintain your own persistence library. +[building-data-adapter]:#toc_building-a-data-custom-adapter + +When you open the Data tab, you will see a list of model types defined +in your application, along with the number of loaded records. +The Inspector displays the loaded records when you click on a model type. + + + +### Inspecting Records + +Each row in the list corresponds to one record. The first four model attributes are shown in the list view. Clicking on the record will open the Object Inspector for that record, and display all attributes. + + + +### Record States and Filtering + +The Data tab is kept in sync with the data loaded in your application. +Any record additions, deletions, or changes are reflected immediately. If you have unsaved +records, they will be displayed in green by clicking on the New pill. + + + +Click on the Modified pill to display unsaved record modifications. + + + +You can also filter records by entering a query in the search box. + +### Building a Data Custom Adapter + +You can use your own data persistence library with the Inspector. Build a [data adapter][data-adapter-docs], and you can inspect your models +using the Data tab. Use [Ember Data's data adapter][ember-data-data-adapter] as an example for how to build your data adapter. + +[data-adapter-docs]: https://github.com/emberjs/ember.js/blob/3ac2fdb0b7373cbe9f3100bdb9035dd87a849f64/packages/ember-extension-support/lib/data_adapter.js +[ember-data-data-adapter]:https://github.com/emberjs/data/blob/d7988679590bff63f4d92c4b5ecab173bd624ebb/packages/ember-data/lib/system/debug/debug_adapter.js diff --git a/guides/v3.6.0/ember-inspector/deprecations.md b/guides/v3.6.0/ember-inspector/deprecations.md new file mode 100644 index 0000000000..2db71bf2b0 --- /dev/null +++ b/guides/v3.6.0/ember-inspector/deprecations.md @@ -0,0 +1,40 @@ +As part of making your app upgrades as smooth as possible, the Inspector gathers your deprecations, groups them, and displays them in a +way that helps you fix them. + +To view the list of deprecations in an app, click on the `Deprecations` menu. + + + +You can see the total number of deprecations next to the `Deprecations` menu. +You can also see the number of occurrences for each deprecation. + +### Ember CLI Deprecation Sources + +If you are using Ember CLI and have source maps enabled, you can see a +list of sources for each deprecation. If you are using Chrome or Firefox, +clicking on the source opens the sources panel and takes you to +the code that caused the deprecation message to be displayed. + + + + + +You can send the deprecation message's stack trace to the +console by clicking on `Trace in the console`. + + +### Transition Plans + +Click on the "Transition Plan" link for information on how to remove the deprecation warning, and you'll be taken to a helpful deprecation guide on the Ember website. + + + + +### Filtering and Clearing + +You can filter the deprecations by typing a query in the search box. +You can also clear the current deprecations by clicking on the clear icon +at the top. + + diff --git a/guides/v3.6.0/ember-inspector/index.md b/guides/v3.6.0/ember-inspector/index.md new file mode 100644 index 0000000000..5548dbaf74 --- /dev/null +++ b/guides/v3.6.0/ember-inspector/index.md @@ -0,0 +1,7 @@ +The Ember Inspector is a browser add-on designed to help you understand and debug your Ember applications. You can install it on [Google Chrome](installation/#toc_google-chrome), [Firefox](installation/#toc_firefox) and [other browsers](installation/#toc_via-bookmarklet) (via a bookmarklet) + +Here's a brief video showcasing some of the features of the Inspector: + +
+ +
diff --git a/guides/v3.6.0/ember-inspector/info.md b/guides/v3.6.0/ember-inspector/info.md new file mode 100644 index 0000000000..e09fce9750 --- /dev/null +++ b/guides/v3.6.0/ember-inspector/info.md @@ -0,0 +1,17 @@ +To see a list of libraries used in your application, click on the `Info` menu. This view displays the libraries used, along with their version. + + + +### Registering a Library + +If you would like to add your own application or library to the list, you can register it using: + +```javascript +Ember.libraries.register(libraryName, libraryVersion); +``` + +#### Ember Cli + +If you're using the [ember-cli-app-version] addon, your application's name and version will be added to the list automatically. + +[ember-cli-app-version]: https://github.com/embersherpa/ember-cli-app-version \ No newline at end of file diff --git a/guides/v3.6.0/ember-inspector/installation.md b/guides/v3.6.0/ember-inspector/installation.md new file mode 100644 index 0000000000..22451c5cd5 --- /dev/null +++ b/guides/v3.6.0/ember-inspector/installation.md @@ -0,0 +1,82 @@ +You can install the Inspector on Google Chrome, Firefox, other +browsers (via a bookmarklet), and on mobile devices by following the steps below. + +### Google Chrome + +You can install the Inspector on Google Chrome as a new Developer +Tool. To begin, visit the Extension page on the [Chrome Web Store](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi). + +Click on "Add To Chrome": + + + +Once installed, go to an Ember application, open the Developer Tools, +and click on the `Ember` tab at the far right. + + + +#### File:// protocol + +To use the Inspector with the `file://` protocol, visit `chrome://extensions` in Chrome and check the "Allow access to file URLs" checkbox: + + + +#### Enable Tomster + +You can configure a Tomster icon to show up in Chrome's URL bar whenever you are visiting a site that uses Ember. + +Visit `chrome://extensions`, then click on `Options`. + + + +Make sure the "Display the Tomster" checkbox is checked. + + + + +### Firefox + +Visit the Add-on page on the [Mozilla Add-ons +site][ember-inspector-mozilla]. + +Click on "Add to Firefox". + + + +Once installed, go to an Ember application, open the Developer Tools, +and click on the `Ember` tab. + + + +[ember-inspector-mozilla]: https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/ + + +#### Enable Tomster + +To enable the Tomster icon to show up in the URL bar whenever you are +visiting a site that uses Ember visit `about:addons`. + +Click on `Extensions` -> `Preferences`. + + + +Then make sure the "Display the Tomster icon when a site runs Ember.js" checkbox is checked. + + + + +### Via Bookmarklet + +If you are using a browser other than Chrome or Firefox, you can use the +bookmarklet option to use the Inspector. + +Add the following bookmark: + +Bookmark Me + +To open the Inspector, click on the new bookmark. Safari blocks popups by default, so you'll need to enable popups before using the bookmarklet. + +### Mobile Devices + +If you want to run the Inspector on a mobile device, +you can use the [Ember CLI Remote Inspector](https://github.com/joostdevries/ember-cli-remote-inspector) addon. diff --git a/guides/v3.6.0/ember-inspector/object-inspector.md b/guides/v3.6.0/ember-inspector/object-inspector.md new file mode 100644 index 0000000000..a3b60ec0b2 --- /dev/null +++ b/guides/v3.6.0/ember-inspector/object-inspector.md @@ -0,0 +1,97 @@ +The Inspector includes a panel that allows you to view and interact with your Ember objects. +To open it, click on any Ember object. You can then view the object's properties. + + +### Viewing Objects + +Here's what you see when you click on an object: + + + + + +The Inspector displays the parent objects and mixins that are composed into the chosen object, including the inherited properties. + +Each property value in this view is bound to your application, so if the value of a +property updates in your app, it will be reflected in the Inspector. + +If a property name is preceded by a calculator icon, that means it is a [computed property][computed-property]. If the value of a computed property hasn't yet been computed, you can +click on the calculator to compute it. +[computed-property]: ../../object-model/computed-properties/ + +### Exposing Objects to the Console + +#### Sending from the Inspector to the Console + +You can expose objects to the console by clicking on the `$E` button within the Inspector. +This will set the global `$E` variable to the chosen object. + + + +You can also expose properties to the console. When you hover over an object's properties, a `$E` button will appear +next to every property. Click on it to expose the property's value to the +console. + + + + +#### Sending from the Console to the Inspector + +You can send Ember objects and arrays to the Inspector by using +`EmberInspector.inspect` within the console. + +```javascript +let object = Ember.Object.create(); +EmberInspector.inspect(object); +``` + +Make sure the Inspector is active when you call this method. + + + +### Editing Properties + +You can edit `String`, `Number`, and `Boolean` properties in the Inspector. +Your changes will be reflected immediately in your app. Click on a property's value to start editing it. + + + +Edit the property and press the `ENTER` key to commit the change, or `ESC` to cancel. + +### Navigating the Inspector + +In addition to inspecting the properties above, you can inspect properties that hold Ember objects or arrays. +Click on the property's value to inspect it. + + + +You can continue drill into the Inspector as long as properties contain either an +Ember object or an array. +In the image below, we clicked on the `model` property first, then clicked +on the `store` property. + + + +You can see the path to the current object at the top of the +Inspector. You can go back to the previous object by clicking on the +left-facing arrow at the top left. + +### Custom Property Grouping + +Some properties are not only grouped by inheritance, but also +by framework level semantics. For example, if you inspect an Ember Data +model, you can see `Attributes`, `Belongs To`, `Has Many`, and `Flags` +groups. + + + +Library authors can customize how any object will display in the Inspector. +By defining a `_debugInfo` method, an object can tell the Inspector how it should be rendered. +For an example on how to customize an object's properties, see [Ember Data's +customization][ember-data-debug-info]. + + +[ember-data-debug-info]: https://github.com/emberjs/data/blob/f1be2af71d7402d034bc034d9502733647cad295/packages/ember-data/lib/system/debug/debug_info.js diff --git a/guides/v3.6.0/ember-inspector/promises.md b/guides/v3.6.0/ember-inspector/promises.md new file mode 100644 index 0000000000..488616bd45 --- /dev/null +++ b/guides/v3.6.0/ember-inspector/promises.md @@ -0,0 +1,86 @@ +The Inspector provides a way to look at all Promises created +in your application. Click on the `Promises` menu to start inspecting them. + + + + +You can see a hierarchical list of Promises with labels describing each +Promise, its state, its settled value, and the time it took to +settle. + + +### Promise States and Filtering + +Promises have different colors based on their state. + + + + + + + +You can filter by clicking on the following pills: `Rejected`, `Pending`, `Fulfilled`. + + + +You can also search for Promises by typing a query in the search box. + +To clear the currently logged Promises, click on the clear icon on the +top left of the tab. + +### Inspecting Settled Values + +If the fulfillment value of a Promise is an Ember object or an array, you can click +on that object to open it in the Object Inspector. + + + +If the rejection value is an `Error` object, you can send its stack trace to +the console. + + + +You can also click on the `$E` button to send the value to the console. + +### Tracing + +The Inspector provides a way to view a Promise's stack trace. +Tracing Promises is disabled by default for performance reasons. To +enable tracing, check the `Trace promise` checkbox. You may want to +reload to trace existing Promises. + + + +To trace a Promise, click on the `Trace` button next to the label, +which will send the Promise stack trace to the console. + + + +### Labeling Promises + +Promises generated by Ember are all labeled by default. +You can also label your own RSVP Promises to find them in the Inspector's Promises tab. +All RSVP methods can take a label as the final argument. + +```javascript + +let label = 'Find Posts' + +new RSVP.Promise(method, label); + +RSVP.Promise.resolve(value, label); + +RSVP.Promise.reject(reason, label); + +RSVP.Promise.all(array, label); + +RSVP.Promise.hash(hash, label); + +promise.then(success, failure, label); + +promise.catch(callback, label); + +promise.finally(callback, label); + +``` diff --git a/guides/v3.6.0/ember-inspector/render-performance.md b/guides/v3.6.0/ember-inspector/render-performance.md new file mode 100644 index 0000000000..33f6eba768 --- /dev/null +++ b/guides/v3.6.0/ember-inspector/render-performance.md @@ -0,0 +1,20 @@ +You can use the Inspector to measure your app's render times. Click on `Render Performance` to start inspecting render times. + + + +### Accuracy + +Using the Inspector adds a delay to your rendering, so the durations you see +are not an accurate representation of the speed of your production apps. Use these +times to compare durations and debug rendering bottlenecks, but not as +a way to accurately measure rendering times. + +### Toolbar + +Click on the "clear" icon to remove existing render logs. + +To measure components and templates that are rendered on initial application boot, +click on the "Reload" button at the top. This button ensures that the Inspector starts +measuring render times when your app boots. + +To filter the logs, type a query in the search box. diff --git a/guides/v3.6.0/ember-inspector/routes.md b/guides/v3.6.0/ember-inspector/routes.md new file mode 100644 index 0000000000..2b773076f7 --- /dev/null +++ b/guides/v3.6.0/ember-inspector/routes.md @@ -0,0 +1,24 @@ +The Routes tab displays a list of your application's routes. + +For the following code: + +```javascript +this.route('posts', function() { + this.route('new'); +}); +``` + +The Inspector displays these routes: + + + +As you can see, the Inspector shows the routes you defined as well as the routes +automatically generated by Ember. + +### Viewing the Current Route + +The Inspector highlights the currently active routes. However, if your app has grown too large for this to be useful, you can use the `Current Route Only` +checkbox to hide all routes except the currently active ones. + + \ No newline at end of file diff --git a/guides/v3.6.0/ember-inspector/troubleshooting.md b/guides/v3.6.0/ember-inspector/troubleshooting.md new file mode 100644 index 0000000000..02b9623816 --- /dev/null +++ b/guides/v3.6.0/ember-inspector/troubleshooting.md @@ -0,0 +1,67 @@ +Below are some common issues you may encounter when using the Inspector, along with the +necessary steps to solve them. If your issue is not listed below, please submit an +issue to the Inspector's [GitHub repo](https://github.com/emberjs/ember-inspector). + +### Ember Application Not Detected + +If the Inspector cannot detect an Ember application, you will see +the following message: + + + +Some of the reasons this may happen: + +- This is not an Ember application +- You are using an old Ember version ( < 1.0 ). +- You are using a protocol other than http or https. For file:// protocol, +follow [these steps](../installation/#toc_file-protocol). +- The Ember application is inside a sandboxed iframe with no URL (if you + are using JS Bin, follow [these steps](#toc_using-the-inspector-with-js-bin). + +### Using the Inspector with JS Bin + +Due to the way JS Bin uses iframes, the Inspector doesn't work with edit +mode. To use the Inspector with JS Bin, switch to the "live preview" mode by clicking on +the arrow circled below. + + + +### Application is not Detected Without Reload + +If you always have to reload your application after you open the Inspector, that may mean +the application's booted state is corrupt. This happens if you call `advanceReadiness` or +`deferReadiness` after the application has already booted. + +### Data Adapter Not Detected + +When you click on the Data tab, and see this message: + + + +It means that the data persistence library you're using does not support the Inspector. +If you are the library's author, [see this section](../data/#toc_building-a-data-custom-adapter) on how to add Inspector support to your library. + +### Promises Not Detected + +You click on the Promises tab, and see this message: + + + +This happens if you are using a version of Ember < 1.3. + +#### Missing Promises + +If the Promises tab is working, but there are Promises you can't find, +it's probably because these Promises were created before the +Inspector was activated. To detect Promises the moment the app boots, click on the `Reload` button below: + + + +#### Inspector Version Old on Firefox + +Firefox addons need to go through a review process with each update, so the Inspector is usually one version behind. + +Unfortunately we don't have control over the Firefox review process, so if you need +the latest Inspector version, download and install it manually from +[GitHub](https://github.com/emberjs/ember-inspector). diff --git a/guides/v3.6.0/ember-inspector/view-tree.md b/guides/v3.6.0/ember-inspector/view-tree.md new file mode 100644 index 0000000000..b09b304e92 --- /dev/null +++ b/guides/v3.6.0/ember-inspector/view-tree.md @@ -0,0 +1,63 @@ +You can use the View Tree to inspect your application's current state. +The View Tree shows you the currently rendered templates, models, controllers, and components, in a tree format. Click on the `View Tree` menu on the left to see these. + + + +Use the tips described in [Object Inspector](../object-inspector/) to inspect models and controllers. See below for templates and components. + +### Inspecting Templates + +To see how a template was rendered by Ember, click on the template in the View Tree. If +you're using Chrome or Firefox, you'll be sent to the Elements panel with that DOM element selected. + + + + + +### Components and Inline Views + +The View Tree ignores components and inline views by default. To load these into the View Tree check the `Components` and `All Views` checkboxes. + + + +You can then inspect components using the Object Inspector. + + +### Highlighting Templates + +#### Hovering over the View Tree + +When you hover over the items in the View Tree, the related templates will be +highlighted in your app. For every highlighted template, you can see the +template name, and its associated objects. + + + +#### Hovering over the app + +If you want to highlight a template or component directly within your app, click on the magnifying glass in the Inspector, then hover over the app. +As your our mouse passes over it, the related template or component will be +highlighted. + + + + +If you click on a highlighted template or component, the Inspector will select it. You can then +click on the backing objects to send them to the object inspector. + + + +Click on the `X` button to deselect a template. + + +### Duration + +The Duration column displays the render time for a given template, including the template's children. + + + +By measuring the render time, the Inspector adds a slight delay to the rendering process. As such, the duration is not an exact representation of expected rendering time for a production application. Thus, the rendering duration is more useful to compare times than as an absolute measure of performance. diff --git a/guides/v3.6.0/getting-started/core-concepts.md b/guides/v3.6.0/getting-started/core-concepts.md new file mode 100644 index 0000000000..355080d3e2 --- /dev/null +++ b/guides/v3.6.0/getting-started/core-concepts.md @@ -0,0 +1,83 @@ +Before you start writing any Ember code, it's a good idea to get an overview of how an +Ember application works. + +![ember core concepts](/images/ember-core-concepts/ember-core-concepts.png) + +## Router and Route Handlers +Imagine we are writing a web app for a site that lets users list their properties to rent. At any given time, we should be able to answer questions about the current state like _What rental are they looking at?_ and _Are they editing it?_ In Ember, the answer to these questions is determined by the URL. +The URL can be set in a few ways: + +* The user loads the app for the first time. +* The user changes the URL manually, such as by clicking the back button or by editing the address bar. +* The user clicks a link within the app. +* Some other event in the app causes the URL to change. + +No matter how the URL gets set, the first thing that happens is that the Ember router maps the URL to a route handler. + +The route handler then typically does two things: + +* It renders a template. +* It loads a model that is then available to the template. + +## Templates + +Ember uses templates to organize the layout of HTML in an application. + +Most templates in an Ember codebase are instantly familiar, and look like any +fragment of HTML. For example: + +```handlebars +
Hi, this is a valid Ember template!
+``` + +Ember templates use the syntax of [Handlebars](http://handlebarsjs.com) +templates. Anything that is valid Handlebars syntax is valid Ember syntax. + +Templates can also display properties provided to them from their context, which is either a component or a route's controller. For example: + +```handlebars +
Hi {{name}}, this is a valid Ember template!
+``` + +Here, `{{name}}` is a property provided by the template's context. + +Besides properties, double curly braces (`{{}}`) may also contain +helpers and components, which we'll discuss later. + +## Models + +Models represent persistent state. + +For example, a property rentals application would want to save the details of a rental when a user publishes it, and so a rental would have a model defining its details, perhaps called the _rental_ model. + +A model typically persists information to a web server, although models can be configured to save to anywhere else, such as the browser's Local Storage. + +## Components + +While templates describe how a user interface looks, components control how the user interface _behaves_. + +Components consist of two parts: a template written in Handlebars, and a source file written in JavaScript that defines the component's behavior. For example, our property rental application might have a component for displaying all the rentals called `all-rentals`, and another component for displaying an individual rental called `rental-tile`. The `rental-tile` component might define a behavior that lets the user hide and show the image property of the rental. + +Let's see these core concepts in action by building a property rental application in the next lesson. + +## Hooks + +In Ember, we use the term **hook** for methods that are automatically called within the Ember application. These are methods that can be expected to be called automatically, rather than having to call them specifically. + +Some examples of a hook are: + +* [Component Lifecycle Hooks](../../components/the-component-lifecycle/): the [`willRender()`](https://emberjs.com/api/ember/release/classes/Component/methods/willRender?anchor=willRender/) hook gets called before each time a component renders +* Route Hooks: the [`model()`](https://www.emberjs.com/api/ember/release/classes/Route/methods/model?anchor=model/) hook is used to load the model on a route + +In the following example, the [`didRender()`](https://emberjs.com/api/ember/release/classes/Component/methods?anchor=didRender/) component lifecycle hook is used to log "I rendered!" to the console after each time the component is rendered. + +```javascript {data-filename=/app/components/foo-did-render-example.js} +import Component from '@ember/component'; + +export default Component.extend({ + didRender() { + this._super(...arguments); + console.log('I rendered!'); + } +}); +``` diff --git a/guides/v3.6.0/getting-started/index.md b/guides/v3.6.0/getting-started/index.md new file mode 100644 index 0000000000..c30ff7943f --- /dev/null +++ b/guides/v3.6.0/getting-started/index.md @@ -0,0 +1,62 @@ +Getting started with Ember is easy. Ember projects are created and managed +through our command line build tool Ember CLI. +This tool provides: + +* Modern application asset management (including concatenation, minification, and versioning). +* Generators to help create components, routes, and more. +* A conventional project layout, making existing Ember applications easy to approach. +* Support for ES2015/ES6 JavaScript via the [Babel](https://babeljs.io/learn-es2015/) project. This includes support for [JavaScript modules](http://exploringjs.com/es6/ch_modules.html), which are used throughout this guide. +* A complete [QUnit](https://qunitjs.com/) test harness. +* The ability to consume a growing ecosystem of [Ember Addons](https://emberobserver.com/). + +## Dependencies + +### Git + +Ember requires Git to manage many of its dependencies. Git comes with Mac OS +X and most Linux distributions. Windows users can +download and run [this Git installer](http://git-scm.com/download/win). + +### Node.js and npm + +Ember CLI is built with JavaScript, and requires the most recent LTS version of the [Node.js](https://nodejs.org/) +runtime. It also requires dependencies fetched via [npm](https://www.npmjs.com/). npm is packaged with Node.js, so if your computer has Node.js +installed you are ready to go. + +If you're not sure whether you have Node.js or the right version, run this on your +command line: + +```bash +node --version +npm --version +``` + +If you get a *"command not found"* error or an outdated version for Node: + +* Windows or Mac users can download and run [this Node.js installer](http://nodejs.org/en/download/). +* Mac users often prefer to install Node using [Homebrew](http://brew.sh/). After +installing Homebrew, run `brew install node` to install Node.js. Alternatively, installer packages are available directly +from [Node.js](https://nodejs.org/en/download/). +* Linux users can use [this guide for Node.js installation on Linux](https://nodejs.org/en/download/package-manager/). + +If you get an outdated version of npm, run `npm install -g npm`. + +### Watchman (optional) + +On Mac and Linux, you can improve file watching performance by installing [Watchman](https://facebook.github.io/watchman/docs/install.html). + +## Installation + +Install Ember using npm: + +```bash +npm install -g ember-cli +``` + +To verify that your installation was successful, run: + +```bash +ember -v +``` + +If a version number is shown, you're ready to go. diff --git a/guides/v3.6.0/getting-started/js-primer.md b/guides/v3.6.0/getting-started/js-primer.md new file mode 100644 index 0000000000..b6bb284560 --- /dev/null +++ b/guides/v3.6.0/getting-started/js-primer.md @@ -0,0 +1,226 @@ +Many new features were introduced to JavaScript with the release of newer specifications like ECMAScript 2015, +also known as [ECMAScript 6 or ES6](https://developer.mozilla.org/en/docs/Web/JavaScript/New_in_JavaScript/ECMAScript_6_support_in_Mozilla). +While the Guides [assume you have a working knowledge of JavaScript](/#toc_assumptions), +not every feature of the JavaScript language may be familiar to the developer. + +In this guide we will be covering some JavaScript features, +and how they are used in Ember applications. + +## Variable declarations + +A variable declaration, also called binding, is when you assign a value to a variable name. +An example of declaring a variable containing the number 42 is like so: + +```javascript +var myNumber = 42; +``` + +JavaScript initially had two ways to declare variables, globally and `var`. +With the release of ES2015, `const` and `let` were introduced. +We will go through the different ways to declare a variable, +also called bindings because they *bind* a value to a variable name, +and why modern JavaScript tends to prefer `const` and `let`. + +### `var` + +Variable declarations using `var` exist in the entire body of the function where they are declared. +This is called function-scoping, the existence of the `var` is scoped to the function. +If you try to access a `var` outside of the function it is declared, +you will get an error that the variable is not defined. + +For our example, we will declare a `var` named `name`. +We will try to access it both inside the function and outside, +and see the results we get: + +```javascript +console.log(name); // ReferenceError: name is not defined + +function myFunction() { + var name = "Tomster"; + + console.log(name); // "Tomster" +} +``` + +This also means that if you have an `if` or a `for` in your code and declare a `var` inside them, +you can still access the variable outside of those blocks: + +```javascript +console.log(name); // undefined + +if (true) { + var name = "Tomster"; + + console.log(name); // "Tomster" +} +``` + +In the previous example, we can see that the first `console.log(name)` prints out `undefined` instead of the value. +That is because of a feature of JavaScript called *hoisting*. +Any variable declaration is moved by the programming language to the top of the scope it belongs to. +As we saw at the beginning, `var` is scoped to the function, +so the previous example is the same as: + +```javascript +var name; +console.log(name); // undefined + +if (true) { + name = "Tomster"; + + console.log(name); // "Tomster" +} +``` + +### `const` and `let` + +There are two major differences between `var` and both `const` and `let`. +`const` and `let` are both block-level declarations, and they are *not* hoisted. + +Because of this they are not accessible outside of the given block scope (meaning in a `function` or in `{}`) they are declared in. +You also cannot access them before they are declared, or you will get a [`ReferenceError`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError). + +```javascript +console.log(name) // ReferenceError: name is not defined + +if (person) { + console.log(name) // ReferenceError: name is not defined + + let name = 'Gob Bluth'; // "Gob Bluth" +} else { + console.log(name) // ReferenceError: name is not defined +} +``` + +`const` declarations have an additional restriction, they are *constant references*, +they always refer to the same thing. +To use a `const` declaration you have to specify the value it refers, +and you cannot change what the declaration refers to: + +```javascript +const firstName; // Uncaught SyntaxError: Missing initializer in const declaration +const firstName = 'Gob'; +firstName = 'George Michael'; // Uncaught SyntaxError: Identifier 'firstName' has already been declared +``` + +Note that `const` does not mean that the value it refers to cannot change. +If you have an array or an object, you can change their properties: + +```javascript +const myArray = []; +const myObject = { name: "Tom Dale" }; + +myArray.push(1); +myObject.name = "Leah Silber"; + +console.log(myArray); // [1] +console.log(myObject); // {name: "Leah Silber"} +``` + +### `for` loops + +Something that might be confusing is the behaviour of `let` in `for` loops. + +As we saw before, `let` declarations are scoped to the block they belong to. +In `for` loops, any variable declared in the `for` syntax belongs to the loop's block. + +Let's look at some code to see what this looks like. +If you use `var`, this happens: + +```javascript +for (var i = 0; i < 3; i++) { + console.log(i) // 0, 1, 2 +} + +console.log(i) // 3 +``` + +But if you use `let`, this happens instead: + +```javascript +for (let i = 0; i < 3; i++) { + console.log(i) // 0, 1, 2 +} + +console.log(i) // ReferenceError: i is not defined +``` + +Using `let` will avoid accidentally leaking and changing the `i` variable from outside of the `for` block. + +## Promises + +A `Promise` is an object that may produce a value some time in the future: either a resolved value, or a reason that it’s not resolved (e.g., a network error occurred). A `Promise` may be in one of 3 possible states: `fulfilled`, `rejected`, or `pending`. Promises were introduced in ES6 JavaScript. + +Why are Promises needed in Ember? JavaScript is single threaded, and some things like querying data from your backend server take time, thus blocking the thread. It is efficient to not block the thread while these computations or data fetches occur - Promises to the rescue! They provide a solution by returning a proxy object for a value not necessarily known when the `Promise` is created. While the `Promise` code is running, the rest of the code moves on. + +For example, we will declare a basic `Promise` named `myPromiseObject`. + +```javascript +let myPromiseObject = new Promise(function(resolve, reject) { + // on success + resolve(value); + + // on failure + reject(reason); +}); +``` + +Promises come equipped with some methods, out of which `then()` and `catch()` are most commonly used. You can dive into details by checking out the reference links. +`.then()` always returns a new `Promise`, so it’s possible to chain Promises with precise control over how and where errors are handled. + +We will use `myPromiseObject` declared above to show you how `then()` is used: + +```javascript +myPromiseObject.then(function(value) { + // on fulfillment +}, function(reason) { + // on rejection +}); +``` + +Let's look at some code to see how they are used in Ember: + +```javascript +store.findRecord('person', 1).then(function(person) { + // Do something with person when promise is resolved. + person.set('name', 'Tom Dale'); +}); +``` + +In the above snippet, `store.findRecord('person', 1)` can make a network request if the data is not +already present in the store. It returns a `Promise` object which can resolve almost instantly if the data is present in store, or it can take some time to resolve if the data is being fetched by a network request. + +Now we can come to part where these promises are chained: + +```javascript +store.findRecord('person', 1).then(function(person) { + + return person.get('post'); //get all the posts linked with person. + +}).then(function(posts){ + + myFirstPost = posts.get('firstObject'); //get the first post from collection. + return myFirstPost.get('comment'); //get all the comments linked with myFirstPost. + +}).then(function(comments){ + + // do something with comments + return store.findRecord('book', 1); //query for another record + +}).catch(function(err){ + + //handle errors + +}) +``` + +In the above code snippet, we assume that a person has many posts, and a post has many comments. So, `person.get('post')` will return a `Promise` object. We chain the response with `then()` so that when it's resolved, we get the first object from the resolved collection. Then, we get comments from it with `myFirstPost.get('comment')` which will again return a `promise` object, thus continuing the chain. + +### Resources + +For further reference you can consult Developer Network articles: + +* [`var`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var) +* [`const`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const) +* [`let`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let). +* [`promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). diff --git a/guides/v3.6.0/getting-started/quick-start.md b/guides/v3.6.0/getting-started/quick-start.md new file mode 100644 index 0000000000..0f4b765759 --- /dev/null +++ b/guides/v3.6.0/getting-started/quick-start.md @@ -0,0 +1,297 @@ +This guide will teach you how to build a simple app using Ember from scratch. + +We'll cover these steps: + +1. Installing Ember. +2. Creating a new application. +3. Defining a route. +4. Writing a UI component. +5. Building your app to be deployed to production. + +## Install Ember + +You can install Ember with a single command using npm, +the Node.js package manager. +Type this into your terminal: + +```bash +npm install -g ember-cli +``` + +Don't have npm? [Learn how to install Node.js and npm here](https://docs.npmjs.com/getting-started/installing-node). +For a full list of dependencies necessary for an Ember CLI project, +consult our [Installing Ember](../../getting-started/) guide. + +## Create a New Application + +Once you've installed Ember CLI via npm, +you will have access to a new `ember` command in your terminal. +You can use the `ember new` command to create a new application. + +```bash +ember new ember-quickstart +``` + +This one command will create a new directory called `ember-quickstart` and set up a new Ember application inside of it. +Out of the box, your application will include: + +* A development server. +* Template compilation. +* JavaScript and CSS minification. +* ES2015 features via Babel. + +By providing everything you need to build production-ready web applications in an integrated package, +Ember makes starting new projects a breeze. + +Let's make sure everything is working properly. +`cd` into the application directory `ember-quickstart` and start the development server by typing: + +```bash +cd ember-quickstart +ember serve +``` + +After a few seconds, you should see output that looks like this: + +```text +Livereload server on http://localhost:7020 +Serving on http://localhost:4200/ +``` + +(To stop the server at any time, type Ctrl-C in your terminal.) + +Open [`http://localhost:4200`](http://localhost:4200) in your browser of choice. +You should see an Ember welcome page and not much else. +Congratulations! You just created and booted your first Ember app. + +We will start by editing the `application` template. +This template is always on screen while the user has your application loaded. +In your editor, open `app/templates/application.hbs` and change it to the following: + +```handlebars {data-filename=app/templates/application.hbs} +

PeopleTracker

+ +{{outlet}} +``` + +Ember detects the changed file and automatically reloads the page for you in the background. +You should see that the welcome page has been replaced by "PeopleTracker". +You also added an `{{outlet}}` to this page, +which means that any nested route will be rendered in that place. + +## Define a Route + +Let's build an application that shows a list of scientists. +To do that, the first step is to create a route. +For now, you can think of routes as being the different pages that make up your application. + +Ember comes with _generators_ that automate the boilerplate code for common tasks. +To generate a route, type this in a new terminal window in your `ember-quickstart` directory: + +```bash +ember generate route scientists +``` + +You'll see output like this: + +```text +installing route + create app/routes/scientists.js + create app/templates/scientists.hbs +updating router + add route scientists +installing route-test + create tests/unit/routes/scientists-test.js +``` + +That is Ember telling you that it has created: + +1. A template to be displayed when the user visits `/scientists`. +2. A `Route` object that fetches the model used by that template. +3. An entry in the application's router (located in `app/router.js`). +4. A unit test for this route. + +Open the newly-created template in `app/templates/scientists.hbs` and add the following HTML: + +```handlebars {data-filename=app/templates/scientists.hbs} +

List of Scientists

+``` + +In your browser, open [`http://localhost:4200/scientists`](http://localhost:4200/scientists). +You should see the `

` you put in the `scientists.hbs` template, +right below the `

` from our `application.hbs` template. + +Now that we've got the `scientists` template rendering, +let's give it some data to render. +We do that by specifying a _model_ for that route, +and we can specify a model by editing `app/routes/scientists.js`. + +We'll take the code created for us by the generator and add a `model()` method to the `Route`: + +```javascript {data-filename="app/routes/scientists.js" data-diff="+4,+5,+6"} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model() { + return ['Marie Curie', 'Mae Jemison', 'Albert Hofmann']; + } +}); +``` + +This code example uses the latest features in JavaScript, +some of which you may not be familiar with. +Learn more with this [overview of the newest JavaScript features](https://ponyfoo.com/articles/es6). + +In a route's `model()` method, you return whatever data you want to make available to the template. +If you need to fetch data asynchronously, +the `model()` method supports any library that uses [JavaScript Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). + +Now let's tell Ember how to turn that array of strings into HTML. +Open the `scientists` template and add the following code to loop through the array and print it: + +```handlebars {data-filename="app/templates/scientists.hbs" data-diff="+3,+4,+5,+6,+7"} +

List of Scientists

+ +
    + {{#each model as |scientist|}} +
  • {{scientist}}
  • + {{/each}} +
+``` + +Here, we use the `each` helper to loop over each item in the array we provided from the `model()` hook and print it inside an `
  • ` element. + +## Create a UI Component + +As your application grows, you will notice you are sharing UI elements between multiple pages, +or using them multiple times on the same page. +Ember makes it easy to refactor your templates into reusable components. + +Let's create a `people-list` component that we can use in multiple places to show a list of people. + +As usual, there's a generator that makes this easy for us. +Make a new component by typing: + +```bash +ember generate component people-list +``` + +Copy and paste the `scientists` template into the `people-list` component's template and edit it to look as follows: + +```handlebars {data-filename=app/templates/components/people-list.hbs} +

    {{title}}

    + +
      + {{#each people as |person|}} +
    • {{person}}
    • + {{/each}} +
    +``` + +Note that we've changed the title from a hard-coded string ("List of Scientists") to a dynamic property (`{{title}}`). +We've also renamed `scientist` to the more-generic `person`, +decreasing the coupling of our component to where it's used. + +Save this template and switch back to the `scientists` template. +Replace all our old code with our new componentized version. +Components look like HTML tags but instead of using angle brackets (``) they use double curly braces (`{{component}}`). + +We're going to tell our component: + +1. What title to use, via the `title` attribute. +2. What array of people to use, via the `people` attribute. We'll + provide this route's `model` as the list of people. + +```handlebars {data-filename="app/templates/scientists.hbs" data-diff="-1,-2,-3,-4,-5,-6,-7,+8"} +

    List of Scientists

    + +
      + {{#each model as |scientist|}} +
    • {{scientist}}
    • + {{/each}} +
    +{{people-list title="List of Scientists" people=model}} +``` + +Go back to your browser and you should see that the UI looks identical. +The only difference is that now we've componentized our list into a version that's more reusable and more maintainable. + +You can see this in action if you create a new route that shows a different list of people. +As an exercise for the reader, +you may try to create a `programmers` route that shows a list of famous programmers. +By re-using the `people-list` component, you can do it in almost no code at all. + +## Click Events + +So far, your application is listing data, +but there is no way for the user to interact with the information. +In web applications you often want to listen for user events like clicks or hovers. +Ember makes this easy to do. +First add an `action` helper to the `li` in your `people-list` component. + +```handlebars {data-filename="app/templates/components/people-list.hbs" data-diff="-5,+6"} +

    {{title}}

    + +
      + {{#each people as |person|}} +
    • {{person}}
    • +
    • {{person}}
    • + {{/each}} +
    +``` + +The `action` helper allows you to add event listeners to elements and call named functions. +By default, the `action` helper adds a `click` event listener, +but it can be used to listen for any element event. +Now, when the `li` element is clicked a `showPerson` function will be called from the `actions` object in the `people-list` component. +Think of this like calling `this.actions.showPerson(person)` from our template. + +To handle this function call you need to modify the `people-list` component file to add the function to be called. +In the component, add an `actions` object with a `showPerson` function that alerts the first argument. + +```javascript {data-filename="app/components/people-list.js" data-diff="+4,+5,+6,+7,+8"} +import Component from '@ember/component'; + +export default Component.extend({ + actions: { + showPerson(person) { + alert(person); + } + } +}); +``` + +Now in the browser when a scientist's name is clicked, +this function is called and the person's name is alerted. + +## Building For Production + +Now that we've written our application and verified that it works in development, +it's time to get it ready to deploy to our users. + +To do so, run the following command: + +```bash +ember build --env production +``` + +The `build` command packages up all of the assets that make up your +application—JavaScript, templates, CSS, web fonts, images, and +more. + +In this case, we told Ember to build for the production environment via the `--env` flag. +This creates an optimized bundle that's ready to upload to your web host. +Once the build finishes, +you'll find all of the concatenated and minified assets in your application's `dist/` directory. + +The Ember community values collaboration and building common tools that everyone relies on. +If you're interested in deploying your app to production in a fast and reliable way, +check out the [Ember CLI Deploy](http://ember-cli-deploy.com/) addon. + +If you deploy your application to an Apache web server, first create a new virtual host for the application. +To make sure all routes are handled by index.html, +add the following directive to the application's virtual host configuration: + +```apacheconf +FallbackResource index.html +``` diff --git a/guides/v3.6.0/glossary/web-development.md b/guides/v3.6.0/glossary/web-development.md new file mode 100644 index 0000000000..699a6aa58c --- /dev/null +++ b/guides/v3.6.0/glossary/web-development.md @@ -0,0 +1,91 @@ +Joining a web development community can be a challenge within itself, especially when all the resources you visit assume you're familiar with other technologies that you're not familiar with. + +Our goal is to help you avoid that mess and come up to speed as fast as possible; you can consider us your internet friend. + +## CDN +Content Delivery Network + +This is typically a paid service you can use to get great performance for your app. Many CDNs act as caching proxies to your origin server; some require you to upload your assets to them. They give you a URL for each resource in your app. This URL will resolve differently for folks depending on where they're browsing. + +Behind the scenes, the CDN will distribute your content geographically with the goal of end-users being able to fetch your content with the lowest latency possible. For example, if a user is in India, they'd likely get content served from India faster than from the United States. + + +## CoffeeScript, TypeScript +These are both languages that compile to JavaScript. You're able to write your code using the syntax they offer and when ready you compile your TypeScript or CoffeeScript into JavaScript. + +[CoffeeScript vs TypeScript](http://www.stoutsystems.com/articles/coffeescript-versus-typescript/) + + +## Evergreen browsers +Browsers that update themselves (without user intervention). + +[Evergreen Browsers](http://tomdale.net/2013/05/evergreen-browsers/) + + +## ES3, ES5, ES5.1, ES6 (aka ES2015), etc +ES stands for ECMAScript, which is the specification that JavaScript is based on. The number that follows is the version of the specification. + +Most browsers support at least ES5, and some even have ES6 (also known as ES2015) support. You can check each browser's support (including yours) here: + +* [ES5 support](http://kangax.github.io/compat-table/es5/) +* [ES6 support](http://kangax.github.io/compat-table/es6/) + +[ECMAScript](https://en.wikipedia.org/wiki/ECMAScript) + + +## LESS, Sass +Both LESS and Sass are types of CSS preprocessor markup intended to give you much more control over your CSS. During the build process, the LESS or Sass resources compile down to vanilla CSS (which can be executed in a browser). + +[Sass/Less Comparison](https://gist.github.com/chriseppstein/674726) + + +## Linter, linting +A validation tool which checks for common issues in your JavaScript. You'd usually use this in your build process to enforce quality in your codebase. A great example of something to check for: *making sure you've always got your semicolons*. + +An example of some of the options you can configure: +[ESLint](http://eslint.org/docs/rules/) +[JSlint](http://jshint.com/docs/options/) + + +## Polyfill +This is a concept that typically means providing JavaScript which tests for features that are missing (prototypes not defined, etc) and "fills" them by providing an implementation. + + +## Promise +Asynchronous calls typically return a promise (or deferred). This is an object which has a state: it can be given handlers for when it's fulfilled or rejected. + +Ember makes use of these in places like the model hook for a route. Until the promise resolves, Ember is able to put the route into a "loading" state. + +* [An open standard for sound, interoperable JavaScript promises](https://promisesaplus.com/) +* [emberjs.com - A word on promises](http://emberjs.com/guides/routing/asynchronous-routing/#toc_a-word-on-promises) + + +## SSR +Server-Side Rendering + +[Inside FastBoot: The Road to Server-Side Rendering](http://emberjs.com/blog/2014/12/22/inside-fastboot-the-road-to-server-side-rendering.html) + + +## Transpile +When related to JavaScript, this can be part of your build process which "transpiles" (converts) your ES6 syntax JavaScript to JavaScript that is supported by current browsers. + +Besides ES6, you'll see a lot of content about compiling/transpiling CoffeeScript, a short-hand language which can "compile" to JavaScript. + +* Ember CLI specifically uses [Babel](https://babeljs.io/) via the [ember-cli-babel](https://github.com/babel/ember-cli-babel) plugin. + + +## UI +UI stands for User Interface and is essentially what the user sees and interacts with on a device. In terms of the web, the UI is generally composed of a series of pages containing visual elements such as buttons and icons that a user can interact with to perform a specific function. + + +## Shadow DOM +Not to be confused with Virtual DOM. Shadow DOM is still a work in progress, but basically a proposed way to have an "isolated" DOM encapsulated within your app's DOM. + +Creating a re-usable "widget" or control might be a good use-case for this. Browsers implement some of their controls using their own version of a shadow DOM. + +* [W3C Working Draft](http://www.w3.org/TR/shadow-dom/) +* [What the Heck is Shadow DOM?](http://glazkov.com/2011/01/14/what-the-heck-is-shadow-dom/) + + +## Virtual DOM +Not to be confused with Shadow DOM. The concept of a virtual DOM means abstracting your code (or in our case, Ember) away from using the browser's DOM in favor of a "virtual" DOM that can easily be accessed for read/writes or even serialized. diff --git a/guides/v3.6.0/index.md b/guides/v3.6.0/index.md new file mode 100644 index 0000000000..59ac41d4ea --- /dev/null +++ b/guides/v3.6.0/index.md @@ -0,0 +1,117 @@ +Welcome to the Ember.js Guides! This documentation will take you from +total beginner to Ember expert. + +## What is Ember? + +Ember is a JavaScript front-end framework designed to help you build websites with rich and complex user interactions. +It does so by providing developers both with many features that are essential to manage complexity in modern web applications, +as well as an integrated development toolkit that enables rapid iteration. + +Some of these features that you'll learn about in the guides are: + +* [Ember CLI](./configuring-ember/configuring-ember-cli/) - A robust development toolkit to create, develop, and build Ember applications. When you see an `$ ember ` instruction throughout the guides, that's Ember CLI! +* [Routing](./routing) - The central part of an Ember application. Enables developers to drive the application state from the URL. +* [Templating engine](./templates/handlebars-basics/) - Use Handlebars syntax to write your application's templates +* [Data layer](./models/) - Ember Data provides a consistent way to communicate with external APIs and manage application state +* [Ember Inspector](./ember-inspector/) - A browser extension, or bookmarklet, to inspect your application live. It's also useful for spotting Ember applications in the wild, try to install it and open up the [NASA website](https://www.nasa.gov/)! + +## Organization + +On the left side of each Guides page is a table of contents, +organized into sections that can be expanded to show the topics +they cover. Both the sections and the topics within each section are +ordered from basic to advanced concepts. + +The Guides are intended to contain practical explanations of how to +build Ember apps, focusing on the most widely-used features of Ember.js. +For comprehensive documentation of every Ember feature and API, see the +[Ember.js API documentation](http://emberjs.com/api/). + +The Guides begin with an explanation of how to get started with Ember, +followed by a tutorial on how to build your first Ember app. +If you're brand new to Ember, +we recommend you start off by following along with these first two sections of the Guides. + +## Assumptions + +While we try to make the Guides as beginner-friendly as we can, +we must establish a baseline so that the guides can keep focused on Ember.js functionality. +We will try to link to appropriate documentation whenever a concept is introduced. + +To make the most out of the guides, you should have a working knowledge of: + +* **HTML, CSS, JavaScript** - the building blocks of web pages. You can find documentation of each of these technologies at the [Mozilla Developer Network][mdn]. +* **Promises** - the native way to deal with asynchrony in your JavaScript code. See the relevant [Mozilla Developer Network][promises] section. +* **ES2015 modules** - you will better understand [Ember CLI's][ember-cli] project structure and import paths if you are comfortable with [JavaScript Modules][js-modules]. +* **ES2015 syntax** - Ember CLI comes with Babel.js by default so you can +take advantage of newer language features such as arrow functions, template +strings, destructuring, and more. You can check the +[Babel.js documentation][babeljs] or read [Understanding ECMAScript 6][es6] +online. + +## A Note on Mobile Performance + +Ember will do a lot to help you write fast apps, but it can't prevent you from +writing a slow one. This is especially true on mobile devices. To deliver a great +experience, it's important to measure performance early and often, and with a diverse +set of devices. + +Make sure you are testing performance on real devices. Simulated mobile +environments on a desktop computer give an optimistic-at-best representation of +what your real world performance will be like. The more operating systems and +hardware configurations you test, the more confident you can be. + +Due to their limited network connectivity and CPU power, great performance on +mobile devices rarely comes for free. You should integrate performance testing +into your development workflow from the beginning. This will help you avoid +making costly architectural mistakes that are much harder to fix if you only +notice them once your app is nearly complete. + +In short: + +1. Always test on real, representative mobile devices. +2. Measure performance from the beginning, and keep testing as your app + develops. + +These tips will help you identify problems early so they can be addressed systematically, rather than +in a last-minute scramble. + +## Reporting a problem + +Typos, missing words, and code samples with errors are all considered +documentation bugs. If you spot one of them, or want to otherwise improve +the existing guides, we are happy to help you help us! + +Some of the more common ways to report a problem with the guides are: + +* Using the pencil icon on the top-right of each guide page +* Opening an issue or pull request to [the GitHub repository][gh-guides] + +Clicking the pencil icon will bring you to GitHub's editor for that +guide so you can edit right away, using the Markdown markup language. +This is the fastest way to correct a typo, a missing word, or an error in +a code sample. + +If you wish to make a more significant contribution be sure to check our +[issue tracker][gh-guides-issues] to see if your issue is already being +addressed. If you don't find an active issue, open a new one. + +If you have any questions about styling or the contributing process, you +can check out our [contributing guide][gh-guides-contributing]. If your +question persists, reach us in the `#dev-ember-learning` channel on the [Ember Community Discord][discord]. + +Good luck! + +[ember-cli]: https://ember-cli.com/ + +[mdn]: https://developer.mozilla.org/en-US/docs/Web +[promises]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise +[js-modules]: http://jsmodules.io/ +[babeljs]: https://babeljs.io/docs/learn-es2015/ +[es6]: https://leanpub.com/understandinges6/read + +[gh-guides]: https://github.com/ember-learn/guides-source/ +[gh-guides-issues]: https://github.com/ember-learn/guides-source/issues +[gh-guides-contributing]: https://github.com/ember-learn/guides-source/blob/master/CONTRIBUTING.md + +[discord]: https://discordapp.com/invite/zT3asNS diff --git a/guides/v3.6.0/models/creating-updating-and-deleting-records.md b/guides/v3.6.0/models/creating-updating-and-deleting-records.md new file mode 100644 index 0000000000..4eca94340e --- /dev/null +++ b/guides/v3.6.0/models/creating-updating-and-deleting-records.md @@ -0,0 +1,168 @@ +## Creating Records + +You can create records by calling the +[`createRecord()`](https://www.emberjs.com/api/ember-data/release/classes/DS.Store/methods/createRecord?anchor=createRecord) +method on the store. + +```javascript +store.createRecord('post', { + title: 'Rails is Omakase', + body: 'Lorem ipsum' +}); +``` + +The store object is available in controllers and routes using `this.store`. + +## Updating Records + +Making changes to Ember Data records is as simple as setting the attribute you +want to change: + +```javascript +this.store.findRecord('person', 1).then(function(tyrion) { + // ...after the record has loaded + tyrion.set('firstName', 'Yollo'); +}); +``` + +All of the Ember.js conveniences are available for +modifying attributes. For example, you can use `Ember.Object`'s +[`incrementProperty`](https://emberjs.com/api/ember/2.15/classes/Ember.Object/methods/incrementProperty?anchor=incrementProperty) helper: + +```javascript +person.incrementProperty('age'); // Happy birthday! +``` + +## Persisting Records + +Records in Ember Data are persisted on a per-instance basis. +Call [`save()`](https://www.emberjs.com/api/ember-data/release/classes/DS.Model/methods/save?anchor=save) +on any instance of `DS.Model` and it will make a network request. + +Ember Data takes care of tracking the state of each record for +you. This allows Ember Data to treat newly created records differently +from existing records when saving. + +By default, Ember Data will `POST` newly created records to their type url. + +```javascript +let post = store.createRecord('post', { + title: 'Rails is Omakase', + body: 'Lorem ipsum' +}); + +post.save(); // => POST to '/posts' +``` + +Records that already exist on the backend are updated using the HTTP `PATCH` verb. + +```javascript +store.findRecord('post', 1).then(function(post) { + post.get('title'); // => "Rails is Omakase" + + post.set('title', 'A new post'); + + post.save(); // => PATCH to '/posts/1' +}); +``` + +You can tell if a record has outstanding changes that have not yet been +saved by checking its +[`hasDirtyAttributes`](https://www.emberjs.com/api/ember-data/release/classes/DS.Model/properties/hasDirtyAttributes?anchor=hasDirtyAttributes) +property. You can also see what parts of +the record were changed and what the original value was using the +[`changedAttributes()`](https://www.emberjs.com/api/ember-data/release/classes/DS.Model/methods/changedAttributes?anchor=changedAttributes) +method. `changedAttributes` returns an object, whose keys are the changed +properties and values are an array of values `[oldValue, newValue]`. + +```javascript +person.get('isAdmin'); // => false +person.get('hasDirtyAttributes'); // => false +person.set('isAdmin', true); +person.get('hasDirtyAttributes'); // => true +person.changedAttributes(); // => { isAdmin: [false, true] } +``` + +At this point, you can either persist your changes via `save()` or you can roll +back your changes. Calling +[`rollbackAttributes()`](https://www.emberjs.com/api/ember-data/release/classes/DS.Model/methods/rollbackAttributes?anchor=rollbackAttributes) +for a saved record reverts all the `changedAttributes` to their original value. +If the record `isNew` it will be removed from the store. + +```javascript +person.get('hasDirtyAttributes'); // => true +person.changedAttributes(); // => { isAdmin: [false, true] } + +person.rollbackAttributes(); + +person.get('hasDirtyAttributes'); // => false +person.get('isAdmin'); // => false +person.changedAttributes(); // => {} +``` + +## Handling Validation Errors + +If the backend server returns validation errors after trying to save, they will +be available on the `errors` property of your model. Here's how you might display +the errors from saving a blog post in your template: + +```handlebars +{{#each post.errors.title as |error|}} +
    {{error.message}}
    +{{/each}} +{{#each post.errors.body as |error|}} +
    {{error.message}}
    +{{/each}} +``` + +## Promises + +[`save()`](https://www.emberjs.com/api/ember-data/release/classes/DS.Model/methods/save?anchor=save) returns +a promise, which makes it easy to asynchronously handle success and failure +scenarios. Here's a common pattern: + +```javascript +let post = store.createRecord('post', { + title: 'Rails is Omakase', + body: 'Lorem ipsum' +}); + +let self = this; + +function transitionToPost(post) { + self.transitionToRoute('posts.show', post); +} + +function failure(reason) { + // handle the error +} + +post.save().then(transitionToPost).catch(failure); + +// => POST to '/posts' +// => transitioning to posts.show route +``` + +## Deleting Records + +Deleting records is as straightforward as creating records. Call [`deleteRecord()`](https://www.emberjs.com/api/ember-data/release/classes/DS.Model/methods/deleteRecord?anchor=deleteRecord) +on any instance of `DS.Model`. This flags the record as `isDeleted`. The +deletion can then be persisted using `save()`. Alternatively, you can use +the [`destroyRecord`](https://www.emberjs.com/api/ember-data/release/classes/DS.Model/methods/deleteRecord?anchor=destroyRecord) method to delete and persist at the same time. + +```javascript +store.findRecord('post', 1, { backgroundReload: false }).then(function(post) { + post.deleteRecord(); + post.get('isDeleted'); // => true + post.save(); // => DELETE to /posts/1 +}); + +// OR +store.findRecord('post', 2, { backgroundReload: false }).then(function(post) { + post.destroyRecord(); // => DELETE to /posts/2 +}); +``` + +The `backgroundReload` option is used to prevent the fetching of the destroyed record, since [`findRecord()`][findRecord] automatically schedules a fetch of the record from the adapter. + +[findRecord]: diff --git a/guides/v3.6.0/models/customizing-adapters.md b/guides/v3.6.0/models/customizing-adapters.md new file mode 100644 index 0000000000..2c3b161bb9 --- /dev/null +++ b/guides/v3.6.0/models/customizing-adapters.md @@ -0,0 +1,295 @@ +In Ember Data, an Adapter determines how data is persisted to a +backend data store. Things such as the backend host, URL format + and headers used to talk to a REST API can all be configured + in an adapter. You can even switch to storing data in local storage + using a [local storage adapter](https://github.com/locks/ember-localstorage-adapter). + +Ember Data's default Adapter has some built-in assumptions about +how a [REST API should look](http://jsonapi.org/). If your backend conventions +differ from those assumptions, Ember Data allows either slight adjustments +or you can switch to a different adapter if your backend works noticeably +differently. + +_(If you're looking to adjust how the data sent to the backend is formatted, +check the [serializer](../customizing-serializers/) page.)_ + +Extending Adapters is a natural process in Ember Data. Ember takes the +position that you should extend an adapter to add different +functionality. This results in code that is +more testable, easier to understand and reduces bloat for people who +may want to subclass your adapter. + +If your backend has some consistent rules you can define an +`adapter:application`. The `adapter:application` will get priority over +the default Adapter, however it will still be superseded by model +specific Adapters. + +```javascript {data-filename=app/adapters/application.js} +import DS from 'ember-data'; + +export default DS.JSONAPIAdapter.extend({ + // Application specific overrides go here +}); +``` + +If you have one model that has exceptional rules for communicating +with its backend than the others you can create a Model specific +Adapter by running the command `ember generate adapter adapter-name`. +For example, running `ember generate adapter post` will create the +following file: + +```javascript {data-filename=app/adapters/post.js} +import DS from 'ember-data'; + +export default DS.JSONAPIAdapter.extend({ + namespace: 'api/v1' +}); +``` + +Ember Data comes with several built-in adapters. +Feel free to use these adapters as a starting point for creating your own custom adapter. + +- [DS.Adapter](https://www.emberjs.com/api/ember-data/release/classes/DS.Adapter) is the basic adapter +with no functionality. It is generally a good starting point if you +want to create an adapter that is radically different from the other +Ember adapters. + +- [DS.JSONAPIAdapter](https://www.emberjs.com/api/ember-data/release/classes/DS.JSONAPIAdapter) +The `JSONAPIAdapter` is the default adapter and follows JSON API +conventions to communicate with an HTTP server by transmitting JSON +via XHR. + +- [DS.RESTAdapter](https://www.emberjs.com/api/ember-data/release/classes/DS.RESTAdapter) +The `RESTAdapter` allows your store to communicate with an HTTP server +by transmitting JSON via XHR. Before Ember Data 2.0 this adapter was the default. + + +## Customizing the JSONAPIAdapter + +The +[DS.JSONAPIAdapter](https://www.emberjs.com/api/ember-data/release/classes/DS.JSONAPIAdapter) +has a handful of hooks that are commonly used to extend it to work +with non-standard backends. + +### URL Conventions + +The `JSONAPIAdapter` is smart enough to determine the URLs it +communicates with based on the name of the model. For example, if you +ask for a `Post` by ID: + +```javascript +store.findRecord('post', 1).then(function(post) { +}); +``` + +The JSON API adapter will automatically send a `GET` request to `/posts/1`. + +The actions you can take on a record map onto the following URLs in the +JSON API adapter: + + + + + + + + + + + + +
    ActionHTTP VerbURL
    FindGET/posts/123
    Find AllGET/posts
    UpdatePATCH/posts/123
    CreatePOST/posts
    DeleteDELETE/posts/123
    + +#### Pluralization Customization + +To facilitate pluralizing model names when generating route urls Ember +Data comes bundled with +[Ember Inflector](https://github.com/stefanpenner/ember-inflector), an +ActiveSupport::Inflector compatible library for inflecting words +between plural and singular forms. Irregular or uncountable +pluralizations can be specified via `Ember.Inflector.inflector`. +To do this, create a file containing your customizations and import it +in `app.js`: + +```javascript {data-filename=app/app.js} +// sets up Ember.Inflector +import './models/custom-inflector-rules'; +``` + +```javascript {data-filename=app/models/custom-inflector-rules.js} +import Inflector from 'ember-inflector'; + +const inflector = Inflector.inflector; + +// Tell the inflector that the plural of "campus" is "campuses" +inflector.irregular('campus', 'campuses'); + +// Tell the inflector that the plural of "advice" is "advice" +inflector.uncountable('advice'); + +// Modules must have an export, so we just export an empty object here +export default {}; +``` + +The JSON API adapter will now make requests for `Campus` models to +`/campuses` and `/campuses/1` (instead of `/campus/` and `/campus/1`), +and requests for `advice` to `/advice` and `/advice/1` (instead of +`/advices/` and `/advices/1`). + +When specifying irregular inflection rules for compound words, only the final word or phrase should be specified. For example, to specify the plural of `redCow` as `redKine` or `red-cow` as `red-kine`, only the final word segments `cow` and `kine` should be specified: + +```javascript +inflector.irregular('cow', 'kine'); +``` + +#### Endpoint Path Customization + +The `namespace` property can be used to prefix requests with a +specific url namespace. + +```javascript {data-filename=app/adapters/application.js} +import DS from 'ember-data'; + +export default DS.JSONAPIAdapter.extend({ + namespace: 'api/1' +}); +``` + +Requests for `person` would now target `http://emberjs.com/api/1/people/1`. + + +#### Host Customization + +By default, the adapter will target the current domain. If you would +like to specify a new domain you can do so by setting the `host` +property on the adapter. + +```javascript {data-filename=app/adapters/application.js} +import DS from 'ember-data'; + +export default DS.JSONAPIAdapter.extend({ + host: 'https://api.example.com' +}); +``` + +Requests for `person` would now target `https://api.example.com/people/1`. + + +#### Path Customization + +By default, the `JSONAPIAdapter` will attempt to pluralize and dasherize +the model name to generate the path name. If this convention does not +conform to your backend you can override the `pathForType` method. + +For example, if you did not want to pluralize model names and needed +underscore_case instead of dash-case you could override the +`pathForType` method like this: + +```javascript {data-filename=app/adapters/application.js} +import DS from 'ember-data'; +import { underscore } from '@ember/string'; + +export default DS.JSONAPIAdapter.extend({ + pathForType(type) { + return underscore(type); + } +}); +``` + +Requests for `person` would now target `/person/1`. +Requests for `user-profile` would now target `/user_profile/1`. + +#### Headers customization + +Some APIs require HTTP headers, e.g. to provide an API key. Arbitrary +headers can be set as key/value pairs on the `JSONAPIAdapter`'s `headers` +object and Ember Data will send them along with each ajax request. +(Note that we set headers in `init()` because default property values +should not be arrays or objects.) + +```javascript {data-filename=app/adapters/application.js} +import DS from 'ember-data'; + +export default DS.JSONAPIAdapter.extend({ + init() { + this._super(...arguments); + + this.set('headers', { + 'API_KEY': 'secret key', + 'ANOTHER_HEADER': 'Some header value' + }); + } +}); +``` + +`headers` can also be used as a computed property to support dynamic +headers. In the example below, the headers are generated with a computed +property dependent on the `session` service. + +```javascript {data-filename=app/adapters/application.js} +import DS from 'ember-data'; +import { computed } from '@ember/object'; +import { inject as service } from '@ember/service'; + + +export default DS.JSONAPIAdapter.extend({ + session: service('session'), + headers: computed('session.authToken', function() { + return { + 'API_KEY': this.session.authToken, + 'ANOTHER_HEADER': 'Some header value' + }; + }) +}); +``` + +In some cases, your dynamic headers may require data from some +object outside of Ember's observer system (for example +`document.cookie`). You can use the +[volatile](https://www.emberjs.com/api/ember/release/classes/@ember%2Fobject%2Fcomputed/methods/property?anchor=volatile) +function to set the property into a non-cached mode causing the headers to +be recomputed with every request. + +```javascript {data-filename=app/adapters/application.js} +import DS from 'ember-data'; +import { computed } from '@ember/object'; +import { get } from '@ember/object'; + +export default DS.JSONAPIAdapter.extend({ + headers: computed(function() { + return { + 'API_KEY': get(document.cookie.match(/apiKey\=([^;]*)/), '1'), + 'ANOTHER_HEADER': 'Some header value' + }; + }).volatile() +}); +``` + +#### Authoring Adapters + +The `defaultSerializer` property can be used to specify the serializer +that will be used by this adapter. This is only used when a model +specific serializer or `serializer:application` are not defined. + +In an application, it is often easier to specify an +`serializer:application`. However, if you are the author of a +community adapter it is important to remember to set this property to +ensure Ember does the right thing in the case a user of your adapter +does not specify an `serializer:application`. + +```javascript {data-filename=app/adapters/my-custom-adapter.js} +import DS from 'ember-data'; + +export default DS.JSONAPIAdapter.extend({ + defaultSerializer: '-default' +}); +``` + +## Community Adapters + +If none of the built-in Ember Data Adapters work for your backend, +be sure to check out some of the community maintained Ember Data +Adapters. Some good places to look for Ember Data Adapters include: + +- [Ember Observer](http://emberobserver.com/categories/data) +- [GitHub](https://github.com/search?q=ember+data+adapter&ref=cmdform) diff --git a/guides/v3.6.0/models/customizing-serializers.md b/guides/v3.6.0/models/customizing-serializers.md new file mode 100644 index 0000000000..dcfedbb512 --- /dev/null +++ b/guides/v3.6.0/models/customizing-serializers.md @@ -0,0 +1,784 @@ +In Ember Data, serializers format the data sent to and received from +the backend store. By default, Ember Data serializes data using the +[JSON API](http://jsonapi.org/) format. If your backend uses a different +format, Ember Data allows you to customize the serializer or use a +different serializer entirely. + +Ember Data ships with 3 serializers. The +[`JSONAPISerializer`](https://www.emberjs.com/api/ember-data/release/classes/DS.JSONAPISerializer) +is the default serializer and works with JSON API backends. The +[`JSONSerializer`](https://www.emberjs.com/api/ember-data/release/classes/DS.JSONSerializer) +is a simple serializer for working with single json object or arrays of records. The +[`RESTSerializer`](https://www.emberjs.com/api/ember-data/release/classes/DS.RESTSerializer) +is a more complex serializer that supports sideloading and was the default +serializer before 2.0. + +## JSONAPISerializer Conventions + +When requesting a record, the `JSONAPISerializer` expects your server +to return a JSON representation of the record that conforms to the +following conventions. + + +### JSON API Document + +The `JSONAPISerializer` expects the backend to return a JSON API +Document that follows the JSON API specification and the conventions +of the examples found on [http://jsonapi.org/format](http://jsonapi.org/format/). This means all +type names should be pluralized and attribute and relationship names +should be dash-cased. For example, if you request a record from +`/people/123`, the response should look like this: + +```json +{ + "data": { + "type": "people", + "id": "123", + "attributes": { + "first-name": "Jeff", + "last-name": "Atwood" + } + } +} +``` + +A response that contains multiple records may have an array in its +`data` property. + +```json +{ + "data": [{ + "type": "people", + "id": "123", + "attributes": { + "first-name": "Jeff", + "last-name": "Atwood" + } + }, { + "type": "people", + "id": "124", + "attributes": { + "first-name": "Yehuda", + "last-name": "Katz" + } + }] +} +``` + +### Sideloaded Data + +Data that is not a part of the primary request but includes linked +relationships should be placed in an array under the `included` +key. For example, if you request `/articles/1` and the backend also +returned any comments associated with that person the response +should look like this: + +```json +{ + "data": { + "type": "articles", + "id": "1", + "attributes": { + "title": "JSON API paints my bikeshed!" + }, + "links": { + "self": "http://example.com/articles/1" + }, + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "5" }, + { "type": "comments", "id": "12" } + ] + } + } + }, + "included": [{ + "type": "comments", + "id": "5", + "attributes": { + "body": "First!" + }, + "links": { + "self": "http://example.com/comments/5" + } + }, { + "type": "comments", + "id": "12", + "attributes": { + "body": "I like XML better" + }, + "links": { + "self": "http://example.com/comments/12" + } + }] +} +``` + +## Customizing Serializers + +Ember Data uses the `JSONAPISerializer` by default, but you can +override this default by defining a custom serializer. There are two +ways to define a custom serializer. First, you can define a custom +serializer for your entire application by defining an "application" +serializer. + +```javascript {data-filename=app/serializers/application.js} +import DS from 'ember-data'; + +export default DS.JSONAPISerializer.extend({}); +``` + +You can also define a serializer for a specific model. For example, if +you had a `post` model you could also define a `post` serializer: + +```javascript {data-filename=app/serializers/post.js} +import DS from 'ember-data'; + +export default DS.JSONAPISerializer.extend({}); +``` + +To change the format of the data that is sent to the backend store, you can use +the [`serialize()`](https://www.emberjs.com/api/ember-data/release/classes/DS.JSONAPISerializer/methods/serialize?anchor=serialize) +hook. Let's say that we have this JSON API response from Ember Data: + +```json +{ + "data": { + "id": "1", + "type": "product", + "attributes": { + "name": "My Product", + "amount": 100, + "currency": "SEK" + } + } +} +``` + +But our server expects data in this format: + +```json +{ + "data": { + "id": "1", + "type": "product", + "attributes": { + "name": "My Product", + "cost": { + "amount": 100, + "currency": "SEK" + } + } + } +} +``` + +Here's how you can change the data: + +```javascript {data-filename=app/serializers/application.js} +import DS from 'ember-data'; + +export default DS.JSONAPISerializer.extend({ + serialize(snapshot, options) { + let json = this._super(...arguments); + + json.data.attributes.cost = { + amount: json.data.attributes.amount, + currency: json.data.attributes.currency + }; + + delete json.data.attributes.amount; + delete json.data.attributes.currency; + + return json; + }, +}); +``` + +Similarly, if your backend store provides data in a format other than JSON API, +you can use the +[`normalizeResponse()`](https://www.emberjs.com/api/ember-data/release/classes/DS.JSONAPISerializer/methods/serialize?anchor=normalizeResponse) +hook. Using the same example as above, if the server provides data that looks +like: + +```json +{ + "data": { + "id": "1", + "type": "product", + "attributes": { + "name": "My Product", + "cost": { + "amount": 100, + "currency": "SEK" + } + } + } +} +``` + +And so we need to change it to look like: + +```json +{ + "data": { + "id": "1", + "type": "product", + "attributes": { + "name": "My Product", + "amount": 100, + "currency": "SEK" + } + } +} +``` + +Here's how we could do it: + +```javascript {data-filename=app/serializers/application.js} +import DS from 'ember-data'; + +export default DS.JSONAPISerializer.extend({ + normalizeResponse(store, primaryModelClass, payload, id, requestType) { + payload.data.attributes.amount = payload.data.attributes.cost.amount; + payload.data.attributes.currency = payload.data.attributes.cost.currency; + + delete payload.data.attributes.cost; + + return this._super(...arguments); + }, +}); +``` + +To normalize only a single model, you can use the +[`normalize()`](https://www.emberjs.com/api/ember-data/release/classes/DS.JSONAPISerializer/methods/serialize?anchor=normalize) +hook similarly. + +For more hooks to customize the serializer with, see the [Ember Data serializer +API documentation](https://www.emberjs.com/api/ember-data/release/classes/DS.JSONAPISerializer). + +### IDs + +In order to keep track of unique records in the store Ember Data +expects every record to have an `id` property in the payload. Ids +should be unique for every unique record of a specific type. If your +backend uses a key other than `id` you can use the +serializer's `primaryKey` property to correctly transform the id +property to `id` when serializing and deserializing data. + +```javascript {data-filename=app/serializers/application.js} +import DS from 'ember-data'; + +export default DS.JSONAPISerializer.extend({ + primaryKey: '_id' +}); +``` + +### Attribute Names + +In Ember Data the convention is to camelize attribute names on a +model. For example: + +```javascript {data-filename=app/models/person.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + firstName: DS.attr('string'), + lastName: DS.attr('string'), + isPersonOfTheYear: DS.attr('boolean') +}); +``` + +However, the `JSONAPISerializer` expects attributes to be dasherized +in the document payload returned by your server: + +```javascript +{ + "data": { + "id": "44", + "type": "people", + "attributes": { + "first-name": "Zaphod", + "last-name": "Beeblebrox", + "is-person-of-the-year": true + } + } +} +``` + +If the attributes returned by your server use a different convention +you can use the serializer's +[`keyForAttribute()`](https://www.emberjs.com/api/ember-data/release/classes/DS.JSONAPISerializer/methods/keyForAttribute?anchor=keyForAttribute) +method to convert an attribute name in your model to a key in your JSON +payload. For example, if your backend returned attributes that are +`under_scored` instead of `dash-cased` you could override the `keyForAttribute` +method like this. + +```javascript {data-filename=app/serializers/application.js} +import { underscore } from '@ember/string'; +import DS from 'ember-data'; + +export default DS.JSONAPISerializer.extend({ + keyForAttribute(attr) { + return underscore(attr); + } +}); +``` + +Irregular keys can be mapped with a custom serializer. The `attrs` +object can be used to declare a simple mapping between property names +on DS.Model records and payload keys in the serialized JSON object +representing the record. An object with the property key can also be +used to designate the attribute's key on the response payload. + + +If the JSON for `person` has a key of `lastNameOfPerson`, and the +desired attribute name is simply `lastName`, then create a custom +Serializer for the model and override the `attrs` property. + +```javascript {data-filename=app/models/person.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + lastName: DS.attr('string') +}); +``` + +```javascript {data-filename=app/serializers/person.js} +import DS from 'ember-data'; + +export default DS.JSONAPISerializer.extend({ + attrs: { + lastName: 'lastNameOfPerson' + } +}); +``` + +### Relationships + +References to other records should be done by ID. For example, if you +have a model with a `hasMany` relationship: + +```javascript {data-filename=app/models/post.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + comments: DS.hasMany('comment', { async: true }) +}); +``` + +The JSON should encode the relationship as an array of IDs and types: + +```javascript +{ + "data": { + "type": "posts", + "id": "1", + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "1" }, + { "type": "comments", "id": "2" }, + { "type": "comments", "id": "3" } + ] + } + } + } +} +``` + +`Comments` for a `post` can be loaded by `post.get('comments')`. The +JSON API adapter will send 3 `GET` requests to `/comments/1/`, +`/comments/2/` and `/comments/3/`. + +Any `belongsTo` relationships in the JSON representation should be the +dasherized version of the property's name. For example, if you have +a model: + +```javascript {data-filename=app/models/comment.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + originalPost: DS.belongsTo('post') +}); +``` + +The JSON should encode the relationship as an ID to another record: + +```javascript +{ + "data": { + "type": "comment", + "id": "1", + "relationships": { + "original-post": { + "data": { "type": "post", "id": "5" }, + } + } + } +} +``` +If needed these naming conventions can be overwritten by implementing +the +[`keyForRelationship()`](https://www.emberjs.com/api/ember-data/release/classes/DS.JSONAPISerializer/methods/keyForAttribute?anchor=keyForRelationship) +method. + +```javascript {data-filename=app/serializers/application.js} +import DS from 'ember-data'; + +export default DS.JSONAPISerializer.extend({ + keyForRelationship(key, relationship) { + return key + 'Ids'; + } +}); +``` + + +## Creating Custom Transformations + +In some circumstances, the built-in attribute types of `string`, +`number`, `boolean`, and `date` may be inadequate. For example, a +server may return a non-standard date format. + +Ember Data can have new JSON transforms +registered for use as attributes: + +```javascript {data-filename=app/transforms/coordinate-point.js} +import DS from 'ember-data'; +import EmberObject from '@ember/object'; + +export default DS.Transform.extend({ + serialize(value) { + return [value.get('x'), value.get('y')]; + }, + deserialize(value) { + return EmberObject.create({ x: value[0], y: value[1] }); + } +}); +``` + +```javascript {data-filename=app/models/cursor.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + position: DS.attr('coordinate-point') +}); +``` + +When `coordinatePoint` is received from the API, it is +expected to be an array: + +```javascript +{ + cursor: { + position: [4,9] + } +} +``` + +But once loaded on a model instance, it will behave as an object: + +```javascript +let cursor = store.findRecord('cursor', 1); +cursor.get('position.x'); //=> 4 +cursor.get('position.y'); //=> 9 +``` + +If `position` is modified and saved, it will pass through the +`serialize` function in the transform and again be presented as +an array in JSON. + +## JSONSerializer + +Not all APIs follow the conventions that the `JSONAPISerializer` uses +with a data namespace and sideloaded relationship records. Some +legacy APIs may return a simple JSON payload that is just the requested +resource or an array of serialized records. The `JSONSerializer` is a +serializer that ships with Ember Data that can be used alongside the +`RESTAdapter` to serialize these simpler APIs. + +To use it in your application you will need to define a +`serializer:application` that extends the `JSONSerializer`. + +```javascript {data-filename=app/serializers/application.js} +import DS from 'ember-data'; + +export default DS.JSONSerializer.extend({ + // ... +}); +``` + +For requests that are only expected to return 1 record +(e.g. `store.findRecord('post', 1)`) the `JSONSerializer` expects the response +to be a JSON object that looks similar to this: + +```json +{ + "id": "1", + "title": "Rails is omakase", + "tag": "rails", + "comments": ["1", "2"] +} +``` + +For requests that are only expected to return 0 or more records +(e.g. `store.findAll('post')` or `store.query('post', { filter: { status: 'draft' } })`) +the `JSONSerializer` expects the response to be a JSON array that +looks similar to this: + +```json +[{ + "id": "1", + "title": "Rails is omakase", + "tag": "rails", + "comments": ["1", "2"] +}, { + "id": "2", + "title": "I'm Running to Reform the W3C's Tag", + "tag": "w3c", + "comments": ["3"] +}] +``` + +The `JSONAPISerializer` is built on top of the `JSONSerializer` so they share +many of the same hooks for customizing the behavior of the +serialization process. Be sure to check out the +[API docs](https://www.emberjs.com/api/ember-data/release/classes/DS.JSONSerializer) +for a full list of methods and properties. + + +## EmbeddedRecordMixin + +Although Ember Data encourages you to sideload your relationships, +sometimes when working with legacy APIs you may discover you need to +deal with JSON that contains relationships embedded inside other +records. The `EmbeddedRecordsMixin` is meant to help with this problem. + +To set up embedded records, include the mixin when extending a +serializer then define and configure embedded relationships. + +For example, if your `post` model contained an embedded `author` record +that looks similar to this: + + +```json +{ + "id": "1", + "title": "Rails is omakase", + "tag": "rails", + "authors": [ + { + "id": "2", + "name": "Steve" + } + ] +} +``` + +You would define your relationship like this: + +```javascript {data-filename=app/serializers/post.js} +import DS from 'ember-data'; + +export default DS.JSONSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + authors: { + serialize: 'records', + deserialize: 'records' + } + } +}); +``` + +If you find yourself needing to both serialize and deserialize the +embedded relationship you can use the shorthand option of `{ embedded: +'always' }`. The example above could therefore be expressed as such: + +```javascript {data-filename=app/serializers/post.js} +import DS from 'ember-data'; + +export default DS.JSONSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + authors: { embedded: 'always' } + } +}); +``` + + +The `serialize` and `deserialize` keys support 3 values: + +* `records` is used to signal that the entire record is expected +* `ids` is used to signal that only the id of the record is expected +* `false` is used to signal that the record is not expected + +For example you may find that you want to read an embedded record when +extracting a JSON payload but only include the relationship's id when +serializing the record. This is possible by using the `serialize: +'ids'` option. You can also opt out of serializing a relationship by +setting `serialize: false`. + +```javascript {data-filename=app/serializers/post.js} +import DS from 'ember-data'; + +export default DS.JSONSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + author: { + serialize: false, + deserialize: 'records' + }, + comments: { + deserialize: 'records', + serialize: 'ids' + } + } +}); +``` + +### EmbeddedRecordsMixin Defaults + +If you do not overwrite `attrs` for a specific relationship, the +`EmbeddedRecordsMixin` will behave in the following way: + +BelongsTo: `{ serialize: 'id', deserialize: 'id' }` +HasMany: `{ serialize: false, deserialize: 'ids' }` + + +There is an option of not embedding JSON in the serialized payload by +using serialize: 'ids'. If you do not want the relationship sent at +all, you can use `serialize: false`. + +## Authoring Serializers + +If you would like to create a custom serializer its recommend that you +start with the `JSONAPISerializer` or `JSONSerializer` and extend one of +those to match your needs. +However, if your payload is extremely different from one of these +serializers you can create your own by extending the `DS.Serializer` +base class. + +A serializer has two main roles in Ember Data. +First, it is responsible for taking a response from an adapter and +serializing it into the normalized JSON format that Ember Data +understands. +Secondly, it transforms snapshots of records into a payload the +adapter will send to the server when creating, updating, or deleting a +record. + +#### Ember Data's Normalized JSON Format + +The normalized JSON format that Ember Data expects is a +[JSON API](http://jsonapi.org/) document with a couple of additional +restrictions. + +First, it is important to make sure that the `type` name of a record +in the normalized JSON object exactly matches the filename of the +model defined for this record type. +By convention Model names are singular in Ember Data, however, the +example type names shown in the +[JSON API spec](http://jsonapi.org/format/) are pluralized. +The JSON API spec itself is agnostic about inflection rules, however, +Ember Data's own `JSONAPISerializer` assumes types are plural and it +will automatically singularize the types. + +Second, attribute and relationship names in the JSON API document +should exactly match the name and casing of the `DS.attr()`, +`DS.belongsTo()` and `DS.hasMany()`, properties defined on the +Model. + +By convention these property names are camelCase in Ember Data models. +As with the `type` names, this is different from the example attribute +and relationship names shown in the +[JSON API spec](http://jsonapi.org/format/). +The examples in the spec use dash-case for attribute and relationship +names. However, the spec does not require attribute or relationship +names to follow any specific casing convention. +If you are using Ember Data's own `JSONAPISerializer` it will assume +the attribute and relationship names from your API are dash-case and +automatically transform them to camelCase when it creates the +normalized JSON object. + +Other than these two restrictions, Ember Data's normalized JSON object +follows the [JSON API](http://jsonapi.org/) specification. + +Example: given this `post` model. + +```javascript {data-filename=app/models/post.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + title: DS.attr('string'), + tag: DS.attr('string'), + comments: hasMany('comment', { async: false }), + relatedPosts: hasMany('post') +}); +``` + +The normalized JSON object that Ember Data expects a serializer to +return looks like this: + +```javascript +{ + data: { + id: "1", + type: "post", + attributes: { + title: "Rails is omakase", + tag: "rails", + }, + relationships: { + comments: { + data: [{ id: "1", type: 'comment' }, + { id: "2", type: 'comment' }], + }, + relatedPosts: { + links: { + related: "/api/v1/posts/1/related-posts/" + } + } + } +} +``` + +Note that the type is `"post"` to match the post model and the +`relatedPosts` relationship in the document matches the +`relatedPosts: hasMany('post')` on the model. + +#### Normalizing adapter responses + +When creating a custom serializer you will need to define a +[normalizeResponse](https://www.emberjs.com/api/ember-data/release/classes/DS.Serializer/methods/normalizeResponse?anchor=normalizeResponse) +method to transform the response from the adapter into the normalized +JSON object described above. + +This method receives the `store`, the Model class for the request, the +payload, the id of the record request (or `null` if there is +no id associated with the request), and the request type (a string with +the possible values of: `'findRecord'`, `'queryRecord'`, `'findAll'`, +`'findBelongsTo'`, `'findHasMany'`, `'findMany'`, `'query'`, +`'createRecord'`, `'deleteRecord'`, and `'updateRecord'`) as arguments. + +A custom serializer will also need to define a +[normalize](http://emberjs.com/api/data/classes/DS.Serializer.html#method_normalize) +method. +This method is called by `store.normalize(type, payload)` and is often +used for normalizing requests made outside of Ember Data because they +do not fall into the normal CRUD flow that the adapter provides. + +#### Serializing records + +Finally a serializer will need to implement a +[serialize](https://www.emberjs.com/api/ember-data/release/classes/DS.Serializer/methods/serialize?anchor=serialize) +method. +Ember Data will provide a record snapshot and an options hash and this +method should return an object that the adapter will send to the +server when creating, updating or deleting a record. + + +## Community Serializers + +If none of the built-in Ember Data Serializers work for your backend, +be sure to check out some of the community maintained Ember Data +Adapters and Serializers. +A good place to search for them is +[Ember Observer](http://emberobserver.com/categories/data). diff --git a/guides/v3.6.0/models/defining-models.md b/guides/v3.6.0/models/defining-models.md new file mode 100644 index 0000000000..32bef53938 --- /dev/null +++ b/guides/v3.6.0/models/defining-models.md @@ -0,0 +1,160 @@ +A model is a class that defines the properties and behavior of the +data that you present to the user. Anything that the user expects to see +if they leave your app and come back later (or if they refresh the page) +should be represented by a model. + +When you want a new model for your application you need to create a new file +under the models folder and extend from `DS.Model`. This is more conveniently +done by using one of Ember CLI's generator commands. For instance, let's create +a `person` model: + +```bash +ember generate model person +``` + +This will generate the following file: + +```javascript {data-filename=app/models/person.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ +}); +``` + +After you have defined a model class, you can start [finding](../finding-records/) +and [working with records](../creating-updating-and-deleting-records/) of that type. + + +## Defining Attributes + +The `person` model we generated earlier didn't have any attributes. Let's +add first and last name, as well as the birthday, using [`DS.attr`](https://www.emberjs.com/api/ember-data/release/classes/DS/methods/attr?anchor=attr): + +```javascript {data-filename=app/models/person.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + firstName: DS.attr(), + lastName: DS.attr(), + birthday: DS.attr() +}); +``` + +Attributes are used when turning the JSON payload returned from your +server into a record, and when serializing a record to save back to the +server after it has been modified. + +You can use attributes like any other property, including as part of a +computed property. Frequently, you will want to define computed +properties that combine or transform primitive attributes. + +```javascript {data-filename=app/models/person.js} +import DS from 'ember-data'; +import { computed } from '@ember/object'; + +export default DS.Model.extend({ + firstName: DS.attr(), + lastName: DS.attr(), + + fullName: computed('firstName', 'lastName', function() { + return `${this.firstName} ${this.lastName}`; + }) +}); +``` + +For more about adding computed properties to your classes, see [Computed +Properties](../../object-model/computed-properties/). + +### Transforms + +You may find the type of an attribute returned by the server does not +match the type you would like to use in your JavaScript code. Ember +Data allows you to define simple serialization and deserialization +methods for attribute types called transforms. You can specify that +you would like a transform to run for an attribute by providing the +transform name as the first argument to the `DS.attr` method. Ember Data +supports attribute types of `string`, `number`, `boolean`, and `date`, +which coerce the value to the JavaScript type that matches its name. + +```javascript {data-filename=app/models/person.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + name: DS.attr('string'), + age: DS.attr('number'), + admin: DS.attr('boolean'), + birthday: DS.attr('date') +}); +``` + +The `date` transform will transform an +[ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) string to a JavaScript +date object. + +The `boolean` transform can handle values other than `true` or +`false`. The strings `"true"` or `"t"` in any casing, `"1"`, and the number +`1` will all coerce to `true`, and `false` otherwise. + +Transforms are not required. If you do not specify a transform name +Ember Data will do no additional processing of the value. + +#### Custom Transforms + +You can also create custom transforms with Ember CLI's `transform` generator: + +```bash +ember generate transform dollars +``` + +Here is a simple transform that converts values between cents and US dollars. + +```javascript {data-filename=app/transforms/dollars.js} +import DS from 'ember-data'; + +export default DS.Transform.extend({ + deserialize(serialized) { + return serialized / 100; // returns dollars + }, + + serialize(deserialized) { + return deserialized * 100; // returns cents + } +}); +``` + +A transform has two functions: `serialize` and `deserialize`. Deserialization +converts a value to a format that the client expects. Serialization does the +reverse and converts a value to the format expected by the persistence layer. + +You would use the custom `dollars` transform like this: + +```javascript {data-filename=app/models/product.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + spent: DS.attr('dollars') +}); +``` + +### Options + +`DS.attr` can also take a hash of options as a second parameter. At the moment +the only option available is `defaultValue`, which can use a value or a function +to set the default value of the attribute if one is not supplied. + +In the following example we define that `verified` has a default value of +`false` and `createdAt` defaults to the current date at the time of the model's +creation: + +```javascript {data-filename=app/models/user.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + username: DS.attr('string'), + email: DS.attr('string'), + verified: DS.attr('boolean', { defaultValue: false }), + createdAt: DS.attr('date', { + defaultValue() { return new Date(); } + }) +}); +``` diff --git a/guides/v3.6.0/models/finding-records.md b/guides/v3.6.0/models/finding-records.md new file mode 100644 index 0000000000..3b4700131c --- /dev/null +++ b/guides/v3.6.0/models/finding-records.md @@ -0,0 +1,124 @@ +The Ember Data store provides an interface for retrieving records of a single type. + +### Retrieving a Single Record + +Use [`store.findRecord()`](https://www.emberjs.com/api/ember-data/release/classes/DS.Store/methods/findRecord?anchor=findRecord) to retrieve a record by its type and ID. +This will return a promise that fulfills with the requested record: + +```javascript +// GET /blog-posts/1 +this.store.findRecord('blog-post', 1) // => GET /blog-posts/1 + .then(function(blogPost) { + // Do something with `blogPost` + }); +``` + +Use [`store.peekRecord()`](https://www.emberjs.com/api/ember-data/release/classes/DS.Store/methods/findRecord?anchor=peekRecord) to retrieve a record by its type and ID, without making a network request. +This will return the record only if it is already present in the store: + +```javascript +let blogPost = this.store.peekRecord('blog-post', 1); // => no network request +``` + +### Retrieving Multiple Records + +Use [`store.findAll()`](https://www.emberjs.com/api/ember-data/release/classes/DS.Store/methods/findAll?anchor=findAll) to retrieve all of the records for a given type: + +```javascript +// GET /blog-posts +this.store.findAll('blog-post') // => GET /blog-posts + .then(function(blogPosts) { + // Do something with `blogPosts` + }); +``` + +Use [`store.peekAll()`](https://www.emberjs.com/api/ember-data/release/classes/DS.Store/methods/findAll?anchor=peekAll) to retrieve all of the records for a given type that are already loaded into the store, without making a network request: + +```javascript +let blogPosts = this.store.peekAll('blog-post'); // => no network request +``` + +`store.findAll()` returns a `DS.PromiseArray` that fulfills to a `DS.RecordArray` and `store.peekAll` directly returns a `DS.RecordArray`. + +It's important to note that `DS.RecordArray` is not a JavaScript array, it's an object that implements [`Ember.Enumerable`](https://emberjs.com/api/ember/release/classes/Ember.Enumerable). +This is important because, for example, if you want to retrieve records by index, +the `[]` notation will not work--you'll have to use `objectAt(index)` instead. + +### Querying for Multiple Records + +Ember Data provides the ability to query for records that meet certain criteria. +Calling [`store.query()`](https://www.emberjs.com/api/ember-data/release/classes/DS.Store/methods/query?anchor=query) will make a `GET` request with the passed object serialized as query params. +This method returns a `DS.PromiseArray` in the same way as `findAll`. + +For example, we could search for all `person` models who have the name of +`Peter`: + +```javascript +// GET to /persons?filter[name]=Peter +this.store.query('person', { + filter: { + name: 'Peter' + } +}).then(function(peters) { + // Do something with `peters` +}); +``` + +### Querying for A Single Record + +If you are using an adapter that supports server requests capable of returning a single model object, +Ember Data provides a convenience method [`store.queryRecord()`](https://www.emberjs.com/api/ember-data/release/classes/DS.Store/methods/query?anchor=queryRecord)that will return a promise that resolves with that single record. +The request is made via a method `queryRecord()` defined by the adapter. + +For example, if your server API provides an endpoint for the currently logged in user: + +```text +// GET /api/current_user +{ + user: { + id: 1234, + username: 'admin' + } +} +``` + +and the adapter for the `User` model defines a `queryRecord()` method that targets that endpoint: + +```javascript {data-filename=app/adapters/user.js} +import DS from 'ember-data'; +import $ from 'jquery'; + +export default DS.Adapter.extend({ + queryRecord(store, type, query) { + return $.getJSON('/api/current_user'); + } +}); +``` + +then calling [`store.queryRecord()`](https://www.emberjs.com/api/ember-data/release/classes/DS.Store/methods/query?anchor=queryRecord) will retrieve that object from the server: + +```javascript +store.queryRecord('user', {}).then(function(user) { + let username = user.get('username'); + console.log(`Currently logged in as ${username}`); +}); +``` + +As in the case of `store.query()`, a query object can also be passed to `store.queryRecord()` and is available for the adapter's `queryRecord()` to use to qualify the request. +However the adapter must return a single model object, not an array containing one element, +otherwise Ember Data will throw an exception. + +Note that Ember's default [JSON API adapter](https://www.emberjs.com/api/ember-data/release/classes/DS.JSONAPIAdapter) does not provide the functionality needed to support `queryRecord()` directly as it relies on REST request definitions that return result data in the form of an array. + +If your server API or your adapter only provides array responses but you wish to retrieve just a single record, you can alternatively use the `query()` method as follows: + +```javascript +// GET to /users?filter[email]=tomster@example.com +tom = store.query('user', { + filter: { + email: 'tomster@example.com' + } +}).then(function(users) { + return users.get("firstObject"); +}); +``` diff --git a/guides/v3.6.0/models/handling-metadata.md b/guides/v3.6.0/models/handling-metadata.md new file mode 100644 index 0000000000..4d7cf1c5b2 --- /dev/null +++ b/guides/v3.6.0/models/handling-metadata.md @@ -0,0 +1,87 @@ +Along with the records returned from your store, you'll likely need to handle some kind of metadata. *Metadata* is data that goes along with a specific *model* or *type* instead of a record. + +Pagination is a common example of using metadata. Imagine a blog with far more posts than you can display at once. You might query it like so: + +```javascript +let result = this.store.query('post', { + limit: 10, + offset: 0 +}); +``` + +To get different *pages* of data, you'd simply change your offset in increments of 10. So far, so good. But how do you know how many pages of data you have? Your server would need to return the total number of records as a piece of metadata. + +Each serializer will expect the metadata to be returned differently. For example, Ember Data's JSON deserializer looks for a `meta` key: + +```javascript +{ + "post": { + "id": 1, + "title": "Progressive Enhancement is Dead", + "comments": ["1", "2"], + "links": { + "user": "/people/tomdale" + }, + // ... + }, + + "meta": { + "total": 100 + } +} +``` + +Regardless of the serializer used, this metadata is extracted from the response. You can then read it with `.get('meta')`. + +This can be done on the result of a `store.query()` call: + +```javascript +store.query('post').then((result) => { + let meta = result.get('meta'); +}); +``` + +On a belongsTo relationship: + +```javascript +let post = store.peekRecord('post', 1); + +post.get('author').then((author) => { + let meta = author.get('meta'); +}); +``` + +Or on a hasMany relationship: + +```javascript +let post = store.peekRecord('post', 1); + +post.get('comments').then((comments) => { + let meta = comments.get('meta'); +}); +``` + +After reading it, `meta.total` can be used to calculate how many pages of posts you'll have. + +To use the `meta` data outside of the `model` hook, you need to return it: + +```javascript {data-filename=app/routes/users.js} +import Router from '@ember/routing/route'; + +export default Route.extend({ + model() { + return this.store.query('user', {}).then((results) => { + return { + users: results, + meta: results.get('meta') + }; + }); + }, + setupController(controller, { users, meta }) { + this._super(controller, users); + controller.set('meta', meta); + } +}); +``` + +To customize metadata extraction, check out the documentation for your serializer. diff --git a/guides/v3.6.0/models/index.md b/guides/v3.6.0/models/index.md new file mode 100644 index 0000000000..18f79e44e1 --- /dev/null +++ b/guides/v3.6.0/models/index.md @@ -0,0 +1,366 @@ +Ember developers have great options for how they handle data from +back end APIs. Ember itself works with any type of back end: REST, +JSON API, GraphQL, or anything else. + +Many developers choose to use Ember Data, a powerful set of tools +for formatting requests, normalizing responses, and efficiently +managing a local cache of data. The Ember Data library is included +by default for applications generated with the Ember CLI; however, +if you do not wish to use it, it can easily be removed by +removing the `ember-data` entry from `package.json`. +Some developers write all their own code to handle API requests, +using native JavaScript methods like +[fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) +or third-party libraries. Many apps use a combination of approaches. + +This section of the Guides describes the essential features of Ember +Data. To learn about other ways to handle data and to find extensions, +check out [Ember Observer](https://www.emberobserver.com/) +or search for community-made tutorials. + +## What are Ember Data models? + +In Ember Data, models are objects that represent the underlying data +that your application presents to the user. +Note that Ember Data models are a different concept than the +[`model`](../routing/specifying-a-routes-model/) method on Routes, +although they share the same name. + +Different apps may have very +different models, depending on what problems they're trying to solve. +For example, a photo sharing application might have a `Photo` +model to represent a particular photo, and a `PhotoAlbum` that +represents a group of photos. In contrast, an online shopping app would +probably have different models, like `ShoppingCart`, `Invoice`, or +`LineItem`. + +Models tend to be _persistent_. That means the user does not expect +model data to be lost when they close their browser window. To make sure +no data is lost, if the user makes changes to a model, you need to store +the model data somewhere that it will not be lost. + +Typically, most models are loaded from and saved to a server that uses a +database to store data. Usually you will send JSON representations of +models back and forth to an HTTP server that you have written. However, +Ember makes it easy to use other durable storage, such as saving to the +user's hard disk with [IndexedDB][indexeddb], or hosted storage solutions that let you +avoid writing and hosting your own servers. + +[indexeddb]: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API + +Once you've loaded your models from storage, components know how to +translate model data into a UI that your user can interact with. For +more information about how components get model data, see the +[Specifying a Route's Model](../routing/specifying-a-routes-model/) +guide. + +Thanks to its use of the _adapter pattern_, Ember Data can be configured +to work with many different kinds of backends. There is [an entire +ecosystem of adapters][adapters] that allow your Ember app to talk to different +types of servers without you writing any networking code. + +[adapters]: http://emberobserver.com/categories/ember-data-adapters + +If you need to integrate your Ember.js app with a server that does not +have an adapter available (for example, you hand-rolled an API server +that does not adhere to any JSON specification/), Ember Data is designed +to be configurable to work with whatever data your server returns. + +Ember Data is also designed to work with streaming servers, like those +powered by WebSockets. You can open a socket to your server and push +changes into Ember Data whenever they occur, giving your app a real-time +user interface that is always up-to-date. + +At first, using Ember Data may feel different than the way you're used +to writing JavaScript applications. Many developers are familiar with +using AJAX to fetch raw JSON data from an endpoint, which may appear +easy at first. Over time, however, complexity leaks out into your +application code, making it hard to maintain. + +With Ember Data, managing models as your application grows becomes both +simple _and_ easy. + +Once you have an understanding of Ember Data, you will have a much +better way to manage the complexity of data loading in your application. +This will allow your code to evolve without becoming a mess. + +## The Store and a Single Source of Truth + +One common way of building web applications is to tightly couple user +interface elements to data fetching. For example, imagine you are +writing the admin section of a blogging app, which has a feature that +lists the drafts for the currently logged in user. + +You might be tempted to make the component responsible for fetching that +data and storing it: + +```javascript {data-filename=app/components/list-of-drafts.js} +import Component from '@ember/component'; + +export default Component.extend({ + willRender() { + $.getJSON('/drafts').then(data => { + this.set('drafts', data); + }); + } +}); +``` + +You could then show the list of drafts in your component's template like +this: + +```handlebars {data-filename=app/templates/components/list-of-drafts.hbs} +
      + {{#each drafts key="id" as |draft|}} +
    • {{draft.title}}
    • + {{/each}} +
    +``` + +This works great for the `list-of-drafts` component. However, your app +is likely made up of many different components. On another page you +may want a component to display the number of drafts. You may be +tempted to copy and paste your existing `willRender` code into the new +component. + +```javascript {data-filename=app/components/drafts-button.js} +import Component from '@ember/component'; + +export default Component.extend({ + willRender() { + $.getJSON('/drafts').then(data => { + this.set('drafts', data); + }); + } +}); +``` + +```handlebars {data-filename=app/templates/components/drafts-button.hbs} +{{#link-to "drafts" tagName="button"}} + Drafts ({{drafts.length}}) +{{/link-to}} +``` + +Unfortunately, the app will now make two separate requests for the +same information. Not only is the redundant data fetching costly in +terms of wasted bandwidth and affecting the perceived speed of your +app, it's easy for the two values to get out-of-sync. You yourself +have probably used a web application where the list of items gets out +of sync with the counter in a toolbar, leading to a frustrating and +inconsistent experience. + +There is also a _tight coupling_ between your application's UI and the +network code. If the url or the format of the JSON payload changes, it +is likely to break all of your UI components in ways that are hard to +track down. + +The SOLID principles of good design tell us that objects should have a +single responsibility. The responsibility of a component should be +presenting model data to the user, not fetching the model. + +Good Ember apps take a different approach. Ember Data gives you a single +**store** that is the central repository of models in your application. +Routes and their corresponding controllers can ask the store for models, and the store is +responsible for knowing how to fetch them. + +It also means that the store can detect that two different components +are asking for the same model, allowing your app to only fetch the data +from the server once. You can think of the store as a read-through cache +for your app's models. Both routes and their corresponding controllers have access to +this shared store; when they need to display or modify a model, they +first ask the store for it. + +## Convention Over Configuration with JSON API + +You can significantly reduce the amount of code you need to write and +maintain by relying on Ember's conventions. Since these conventions +will be shared among developers on your team, following them leads +to code that is easier to maintain and understand. + +Rather than creating an arbitrary set of conventions, Ember Data is +designed to work out of the box with [JSON API][json-api]. JSON API is a +formal specification for building conventional, robust, and performant +APIs that allow clients and servers to communicate model data. + +[json-api]: http://jsonapi.org + +JSON API standardizes how JavaScript applications talk to servers, so +you decrease the coupling between your frontend and backend, and have +more freedom to change pieces of your stack. + +As an analogy, JSON API is to JavaScript apps and API servers what SQL is +to server-side frameworks and databases. Popular frameworks like Ruby on +Rails, Laravel, Django, Spring and more work out of the box with many +different databases, like MySQL, PostgreSQL, SQL Server, and more. + +Frameworks (or apps built on those frameworks) don't need to write +lots of custom code to add support for a new database; as long as that +database supports SQL, adding support for it is relatively easy. + +So too with JSON API. By using JSON API to interop between your Ember +app and your server, you can entirely change your backend stack without +breaking your frontend. And as you add apps for other platforms, such as +iOS and Android, you will be able to leverage JSON API libraries for +those platforms to easily consume the same API your Ember app uses. + +## Models + +In Ember Data, each model is represented by a subclass of `Model` that +defines the attributes, relationships, and behavior of the data that you +present to the user. + +Models define the type of data that will be provided by your server. For +example, a `Person` model might have a `firstName` attribute that is a +string, and a `birthday` attribute that is a date: + +```javascript {data-filename=app/models/person.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + firstName: DS.attr('string'), + birthday: DS.attr('date') +}); +``` + +A model also describes its relationships with other objects. For +example, an `order` may have many `line-items`, and a +`line-item` may belong to a particular `order`. + +```javascript {data-filename=app/models/order.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + lineItems: DS.hasMany('line-item') +}); +``` + +```javascript {data-filename=app/models/line-item.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + order: DS.belongsTo('order') +}); +``` + +Models don't have any data themselves, they define the attributes, +relationships and behavior of specific instances, which are called +**records**. + +## Records + +A **record** is an instance of a model that contains data loaded from a +server. Your application can also create new records and save them back +to the server. + +A record is uniquely identified by its model **type** and **ID**. + +For example, if you were writing a contact management app, you might +have a `Person` model. An individual record in your app might +have a type of `person` and an ID of `1` or `steve-buscemi`. + +```javascript +this.store.findRecord('person', 1); // => { id: 1, name: 'steve-buscemi' } +``` + +An ID is usually assigned to a record by the server when you save it for +the first time, but you can also generate IDs client-side. + +## Adapter + +An **adapter** is an object that translates requests from Ember (such as +"find the user with an ID of 1") into requests to a server. + +For example, if your application asks for a `Person` with an ID of +`1`, how should Ember load it? Over HTTP or a WebSocket? If +it's HTTP, is the URL `/person/1` or `/resources/people/1`? + +The adapter is responsible for answering all of these questions. +Whenever your app asks the store for a record that it doesn't have +cached, it will ask the adapter for it. If you change a record and save +it, the store will hand the record to the adapter to send the +appropriate data to your server and confirm that the save was +successful. + +Adapters let you completely change how your API is implemented without +impacting your Ember application code. + +## Caching + +The store will automatically cache records for you. If a record had already +been loaded, asking for it a second time will always return the same +object instance. This minimizes the number of round-trips to the +server, and allows your application to render its UI to the user as fast as +possible. + +For example, the first time your application asks the store for a +`person` record with an ID of `1`, it will fetch that information from +your server. + +However, the next time your app asks for a `person` with ID `1`, the +store will notice that it had already retrieved and cached that +information from the server. Instead of sending another request for the +same information, it will give your application the same record it had +provided it the first time. This feature—always returning the same +record object, no matter how many times you look it up—is sometimes +called an _identity map_. + +Using an identity map is important because it ensures that changes you +make in one part of your UI are propagated to other parts of the UI. It +also means that you don't have to manually keep records in sync—you can +ask for a record by ID and not have to worry about whether other parts +of your application have already asked for and loaded it. + +One downside to returning a cached record is you may find the state of +the data has changed since it was first loaded into the store's +identity map. In order to prevent this stale data from being a problem +for long, Ember Data will automatically make a request in the +background each time a cached record is returned from the store. When +the new data comes in, the record is updated, and if there have been +changes to the record since the initial render, the template is +re-rendered with the new information. + +## Architecture Overview + +The first time your application asks the store for a record, the store +sees that it doesn't have a local copy and requests it from your +adapter. Your adapter will go and retrieve the record from your +persistence layer; typically, this will be a JSON representation of the +record served from an HTTP server. + +![Diagram showing process for finding an unloaded record](/images/guides/models/finding-unloaded-record-step1-diagram.png) + +As illustrated in the diagram above, the adapter cannot always return the +requested record immediately. In this case, the adapter must make an +_asynchronous_ request to the server, and only when that request finishes +loading can the record be created with its backing data. + +Because of this asynchronicity, the store immediately returns a +_promise_ from the `findRecord()` method. Similarly, any request that the +store makes to the adapter also returns promises. + +Once the request to the server returns with a JSON payload for the +requested record, the adapter resolves the promise it returned to the +store with the JSON. + +The store then takes that JSON, initializes the record with the +JSON data, and resolves the promise returned to your application +with the newly-loaded record. + +![Diagram showing process for finding an unloaded record after the payload has returned from the server](/images/guides/models/finding-unloaded-record-step2-diagram.png) + +Let's look at what happens if you request a record that the store +already has in its cache. + +![Diagram showing process for finding an unloaded record after the payload has returned from the server](/images/guides/models/finding-loaded-record-diagram.png) + +In this case, because the store already knew about the record, it +returns a promise that it resolves with the record immediately. It does +not need to ask the adapter (and, therefore, the server) for a copy +since it already has it saved locally. + +--- + +Models, records, adapters and the store are the core concepts you +should understand to get the most out of Ember Data. The following +sections go into more depth about each of these concepts, and how to +use them together. diff --git a/guides/v3.6.0/models/pushing-records-into-the-store.md b/guides/v3.6.0/models/pushing-records-into-the-store.md new file mode 100644 index 0000000000..a4703688e9 --- /dev/null +++ b/guides/v3.6.0/models/pushing-records-into-the-store.md @@ -0,0 +1,152 @@ +One way to think about the store is as a cache of all of the records +that have been loaded by your application. If a route or a controller in +your app asks for a record, the store can return it immediately if it is +in the cache. Otherwise, the store must ask the adapter to load it, +which usually means a trip over the network to retrieve it from the +server. + +Instead of waiting for the app to request a record, however, you can +push records into the store's cache ahead of time. + +This is useful if you have a good sense of what records the user +will need next. When they click on a link, instead of waiting for a +network request to finish, Ember.js can render the new template +immediately. It feels instantaneous. + +Another use case for pushing in records is if your application has a +streaming connection to a backend. If a record is created or modified, +you want to update the UI immediately. + +### Pushing Records + +To push a record into the store, call the store's [`push()`](https://www.emberjs.com/api/ember-data/release/classes/DS.Store/methods/push?anchor=push) method. + +For example, imagine we want to preload some data into the store when +the application boots for the first time. + +We can use the `route:application` to do so. The `route:application` is +the top-most route in the route hierarchy, and its `model` hook gets +called once when the app starts up. + +```javascript {data-filename=app/models/album.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + title: DS.attr(), + artist: DS.attr(), + songCount: DS.attr() +}); +``` + +```javascript {data-filename=app/routes/application.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model() { + this.store.push({ + data: [{ + id: 1, + type: 'album', + attributes: { + title: 'Fewer Moving Parts', + artist: 'David Bazan', + songCount: 10 + }, + relationships: {} + }, { + id: 2, + type: 'album', + attributes: { + title: 'Calgary b/w I Can\'t Make You Love Me/Nick Of Time', + artist: 'Bon Iver', + songCount: 2 + }, + relationships: {} + }] + }); + } +}); +``` + +The store's `push()` method is a low level API which accepts a JSON +API document with a few important differences from the JSON API +document that the JSONAPISerializer accepts. The type name in the JSON +API document must match the type name of the model exactly (In the +example above the type is `album` because the model is defined in +`app/models/album.js`). Attributes and relationship names must match +the casing of the properties defined on the Model class. + +If you would like the data to be normalized by the model's default +serializer before pushing it into the store, you can use the +[`store.pushPayload()`](https://www.emberjs.com/api/ember-data/release/classes/DS.Store/methods/push?anchor=pushPayload) method. + +```javascript {data-filename=app/serializers/album.js} +import DS from 'ember-data'; + +export default DS.RestSerializer.extend({ + normalize(typeHash, hash) { + hash['songCount'] = hash['song_count'] + delete hash['song_count'] + return this._super(typeHash, hash); + } +}); +``` + +```javascript {data-filename=app/routes/application.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model() { + this.store.pushPayload({ + albums: [ + { + id: 1, + title: 'Fever Moving Parts', + artist: 'David Bazan', + song_count: 10 + }, + { + id: 2, + title: 'Calgary b/w I Can\'t Make You Love Me/Nick Of Time', + artist: 'Bon Iver', + song_count: 2 + } + ] + }); + } +}); +``` + +The `push()` method is also important when working with complex +endpoints. You may find your application has an endpoint that performs +some business logic then creates several records. This likely does not +map cleanly to Ember Data's existing `save()` API which is structured +around persisting a single record. Instead you should make your own +custom AJAX request and push the resulting model data into the store +so it can be accessed by other parts of your application. + + +```javascript {data-filename=app/routes/confirm-payment.js} +import Route from '@ember/routing/route'; +import $ from 'jquery'; + +export default Route.extend({ + actions: { + confirm(data) { + $.ajax({ + data: data, + method: 'POST', + url: 'process-payment' + }).then((digitalInventory) => { + this.store.push(digitalInventory); + this.transitionTo('thank-you'); + }); + } + } +}); +``` + +Properties that are defined on the model but are omitted in the +normalized JSON API document object will not be updated. Properties +that are included in the normalized JSON API document object but not +defined on the Model will be ignored. diff --git a/guides/v3.6.0/models/relationships.md b/guides/v3.6.0/models/relationships.md new file mode 100644 index 0000000000..32e08cee56 --- /dev/null +++ b/guides/v3.6.0/models/relationships.md @@ -0,0 +1,482 @@ +Ember Data includes several built-in relationship types to help you +define how your models relate to each other. + +### One-to-One + +To declare a one-to-one relationship between two models, use +`DS.belongsTo`: + +```javascript {data-filename=app/models/user.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + profile: DS.belongsTo('profile') +}); +``` + +```javascript {data-filename=app/models/profile.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + user: DS.belongsTo('user') +}); +``` + +### One-to-Many + +To declare a one-to-many relationship between two models, use +`DS.belongsTo` in combination with `DS.hasMany`, like this: + +```javascript {data-filename=app/models/blog-post.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + comments: DS.hasMany('comment') +}); +``` + +```javascript {data-filename=app/models/comment.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + blogPost: DS.belongsTo('blog-post') +}); +``` + +### Many-to-Many + +To declare a many-to-many relationship between two models, use +`DS.hasMany`: + +```javascript {data-filename=app/models/blog-post.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + tags: DS.hasMany('tag') +}); +``` + +```javascript {data-filename=app/models/tag.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + blogPosts: DS.hasMany('blog-post') +}); +``` + +### Explicit Inverses + +Ember Data will do its best to discover which relationships map to one +another. In the one-to-many code above, for example, Ember Data can figure out that +changing the `comments` relationship should update the `blogPost` +relationship on the inverse because `blogPost` is the only relationship to +that model. + +However, sometimes you may have multiple `belongsTo`/`hasMany`s for +the same type. You can specify which property on the related model is +the inverse using `DS.belongsTo` or `DS.hasMany`'s `inverse` +option. Relationships without an inverse can be indicated as such by +including `{ inverse: null }`. + + +```javascript {data-filename=app/models/comment.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + onePost: DS.belongsTo('blog-post', { inverse: null }), + twoPost: DS.belongsTo('blog-post'), + redPost: DS.belongsTo('blog-post'), + bluePost: DS.belongsTo('blog-post') +}); +``` + +```javascript {data-filename=app/models/blog-post.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + comments: DS.hasMany('comment', { + inverse: 'redPost' + }) +}); +``` + +### Reflexive Relations + +When you want to define a reflexive relation (a model that has a relationship to +itself), you must explicitly define the inverse relationship. If there +is no inverse relationship then you can set the inverse to `null`. + +Here's an example of a one-to-many reflexive relationship: + +```javascript {data-filename=app/models/folder.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + children: DS.hasMany('folder', { inverse: 'parent' }), + parent: DS.belongsTo('folder', { inverse: 'children' }) +}); +``` + +Here's an example of a one-to-one reflexive relationship: + +```javascript {data-filename=app/models/user.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + name: DS.attr('string'), + bestFriend: DS.belongsTo('user', { inverse: 'bestFriend' }), +}); +``` + +You can also define a reflexive relationship that doesn't have an inverse: + +```javascript {data-filename=app/models/folder.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + parent: DS.belongsTo('folder', { inverse: null }) +}); +``` + +### Polymorphism + +Polymorphism is a powerful concept which allows a developer +to abstract common functionality into a base class. Consider the +following example: a user with multiple payment methods. They +could have a linked PayPal account, and a couple credit cards on +file. + +Note that, for polymorphism to work, Ember Data expects a +"type" declaration polymorphic type via the reserved `type` +property on the model. Confused? See the API response below. + +First, let's look at the model definitions: + +```javascript {data-filename=app/models/user.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + paymentMethods: DS.hasMany('payment-method', { polymorphic: true }) +}); +``` + +```javascript {data-filename=app/models/payment-method.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + user: DS.belongsTo('user', { inverse: 'paymentMethods' }), +}); +``` + +```javascript {data-filename=app/models/payment-method-cc.js} +import { computed } from '@ember/object'; +import PaymentMethod from './payment-method'; + +export default PaymentMethod.extend({ + last4: DS.attr(), + + obfuscatedIdentifier: computed('last4', function () { + return `**** **** **** ${this.last4}`; + }) +}); +``` + +```javascript {data-filename=app/models/payment-method-paypal.js} +import { computed } from '@ember/object'; +import DS from 'ember-data'; +import PaymentMethod from './payment-method' + +export default PaymentMethod.extend({ + linkedEmail: DS.attr(), + + obfuscatedIdentifier: computed('linkedEmail', function () { + let last5 = this.linkedEmail.split('').reverse().slice(0, 5).reverse().join(''); + + return `••••${last5}`; + }) +}); +``` + +And our API might setup these relationships like so: + +```json +{ + "data": { + "id": "8675309", + "type": "user", + "attributes": { + "name": "Anfanie Farmeo" + }, + "relationships": { + "payment-methods": { + "data": [{ + "id": "1", + "type": "payment-method-paypal" + }, { + "id": "2", + "type": "payment-method-cc" + }, { + "id": "3", + "type": "payment-method-apple-pay" + }] + } + } + }, + "included": [{ + "id": "1", + "type": "payment-method-paypal", + "attributes": { + "linked-email": "ryan@gosling.io" + } + }, { + "id": "2", + "type": "payment-method-cc", + "attributes": { + "last4": "1335" + } + }, { + "id": "3", + "type": "payment-method-apple-pay", + "attributes": { + "last4": "5513" + } + }] +} +``` + +### Readonly Nested Data + +Some models may have properties that are deeply nested objects of +readonly data. The naïve solution would be to define models for each +nested object and use `hasMany` and `belongsTo` to recreate the nested +relationship. However, since readonly data will never need to be +updated and saved this often results in the creation of a great deal +of code for very little benefit. An alternate approach is to define +these relationships using an attribute with no transform +(`DS.attr()`). This makes it easy to access readonly values in +computed properties and templates without the overhead of defining +extraneous models. + +### Creating Records + +Let's assume that we have a `blog-post` and a `comment` model. A single blog post can have several comments linked to it. The correct relationship is shown below: + +```javascript {data-filename=app/models/blog-post.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + comments: DS.hasMany('comment') +}); +``` + +```javascript {data-filename=app/models/comment.js} +import DS from 'ember-data'; + +export default DS.Model.extend({ + blogPost: DS.belongsTo('blog-post') +}); +``` + +Now, suppose we want to add comments to an existing blogPost. We can do this in two ways, but for both of them, we first need to look up a blog post that is already loaded in the store, using its id: + +```javascript +let myBlogPost = this.store.peekRecord('blog-post', 1); +``` +Now we can either set the `belongsTo` relationship in our new comment, or, update the blogPost's `hasMany` relationship. As you might observe, we don't need to set both `hasMany` and `belongsTo` for a record. Ember Data will do that for us. + +First, let's look at setting the `belongsTo` relationship in our new comment: + +```javascript +let comment = this.store.createRecord('comment', { + blogPost: myBlogPost +}); +comment.save(); +``` + +In the above snippet, we have referenced `myBlogPost` while creating the record. This will let Ember know that the newly created comment belongs to `myBlogPost`. +This will create a new `comment` record and save it to the server. Ember Data will also update `myBlogPost` to include our newly created comment in its `comments` relationship. + +The second way of doing the same thing is to link the two records together by updating the blogPost's `hasMany` relationship as shown below: + +```javascript +let comment = this.store.createRecord('comment', { +}); +myBlogPost.get('comments').pushObject(comment); +comment.save().then(function () { + myBlogPost.save(); +}); +``` + +In this above case, the new comment's `belongsTo` relationship will be automatically set to the parent blogPost. + +Although `createRecord` is fairly straightforward, the only thing to watch out for +is that you cannot assign a promise as a relationship, currently. + +For example, if you want to set the `author` property of a blogPost, this would **not** work +if the `user` with id isn't already loaded into the store: + +```javascript +this.store.createRecord('blog-post', { + title: 'Rails is Omakase', + body: 'Lorem ipsum', + author: this.store.findRecord('user', 1) +}); +``` + +However, you can easily set the relationship after the promise has fulfilled: + +```javascript +let blogPost = this.store.createRecord('blog-post', { + title: 'Rails is Omakase', + body: 'Lorem ipsum' +}); + +this.store.findRecord('user', 1).then(function(user) { + blogPost.set('author', user); +}); +``` + +### Retrieving Related Records + +When you request data from the server for a model that has relationships with one or more others, +you may want to retrieve records corresponding to those related models at the same time. +For example, when retrieving a blog post, you may need to access the comments associated +with the post as well. +The [JSON API specification allows](http://jsonapi.org/format/#fetching-includes) +servers to accept a query parameter with the key `include` as a request to +include those related records in the response returned to the client. +The value of the parameter should be a comma-separated list of names of the +relationships required. + +If you are using an adapter that supports JSON API, such as Ember's default [`JSONAPIAdapter`](https://www.emberjs.com/api/ember-data/release/classes/DS.JSONAPIAdapter), +you can easily add the `include` parameter to the server requests created by +the `findRecord()`, `findAll()`, +`query()` and `queryRecord()` methods. + +`findRecord()` and `findAll()` each take an `options` argument in which you can +specify the `include` parameter. +For example, given a `post` model that has a `hasMany` relationship with a `comment` model, +when retrieving a specific post we can have the server also return that post's comments +as follows: + +```javascript {data-filename=app/routes/post.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model(params) { + return this.store.findRecord('post', params.post_id, {include: 'comments'}); + } +}); +``` +The post's comments would then be available in your template as `model.comments`. + +Nested relationships can be specified in the `include` parameter as a dot-separated sequence of relationship names. +So to request both the post's comments and the authors of those comments the request +would look like this: + +```javascript {data-filename=app/routes/post.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model(params) { + return this.store.findRecord('post', params.post_id, {include: 'comments,comments.author'}); + } +}); +``` +The `query()` and `queryRecord()` methods each take a `query` argument that is +serialized directly into the URL query string and the `include` parameter may +form part of that argument. +For example: + +```javascript {data-filename=app/routes/adele.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model() { + // GET to /artists?filter[name]=Adele&include=albums + this.store.query('artist', { + filter: {name: 'Adele'}, + include: 'albums' + }).then(function(artists) { + return artists.get('firstObject'); + }); + } +}); +``` + +### Updating Existing Records + +Sometimes we want to set relationships on already existing records. We can simply set a `belongsTo` relationship: + +```javascript +let blogPost = this.store.peekRecord('blog-post', 1); +let comment = this.store.peekRecord('comment', 1); +comment.set('blogPost', blogPost); +comment.save(); +``` + +Alternatively, we could update the `hasMany` relationship by pushing a record into the relationship: + +```javascript +let blogPost = this.store.peekRecord('blog-post', 1); +let comment = this.store.peekRecord('comment', 1); +blogPost.get('comments').pushObject(comment); +blogPost.save(); +``` + +### Removing Relationships + +To remove a `belongsTo` relationship, we can set it to `null`, which will also remove it from the `hasMany` side: + +```javascript +let comment = this.store.peekRecord('comment', 1); +comment.set('blogPost', null); +comment.save(); +``` + +It is also possible to remove a record from a `hasMany` relationship: + +```javascript +let blogPost = this.store.peekRecord('blog-post', 1); +let comment = this.store.peekRecord('comment', 1); +blogPost.get('comments').removeObject(comment); +blogPost.save(); +``` + +As in the earlier examples, the comment's `belongsTo` relationship will also be cleared by Ember Data. + +### Relationships as Promises + +While working with relationships it is important to remember that they return promises. + +For example, if we were to work on a blogPost's asynchronous comments, we would have to wait until the promise has fulfilled: + +```javascript +let blogPost = this.store.peekRecord('blog-post', 1); + +blogPost.get('comments').then((comments) => { + // now we can work with the comments +}); +``` + +The same applies to `belongsTo` relationships: + +```javascript +let comment = this.store.peekRecord('comment', 1); + +comment.get('blogPost').then((blogPost) => { + // the blogPost is available here +}); +``` + +Handlebars templates will automatically be updated to reflect a resolved promise. We can display a list of comments in a blogPost like so: + +```handlebars +
      + {{#each blogPost.comments as |comment|}} +
    • {{comment.id}}
    • + {{/each}} +
    +``` + +Ember Data will query the server for the appropriate records and re-render the template once the data is received. diff --git a/guides/v3.6.0/object-model/bindings.md b/guides/v3.6.0/object-model/bindings.md new file mode 100644 index 0000000000..9c0652ddf3 --- /dev/null +++ b/guides/v3.6.0/object-model/bindings.md @@ -0,0 +1,74 @@ +Unlike most other frameworks that include some sort of binding implementation, +bindings in Ember.js can be used with any object. That said, bindings are most +often used within the Ember framework itself, and for most problems Ember app +developers face, computed properties are the appropriate solution. + + +The easiest way to create a two-way binding is to use a [`computed.alias()`](https://www.emberjs.com/api/ember/release/classes/@ember%2Fobject%2Fcomputed/methods/alias?anchor=alias&show=inherited%2Cprotected%2Cprivate%2Cdeprecated), +that specifies the path to another object. + +```javascript +import EmberObject from '@ember/object'; +import { alias } from '@ember/object/computed'; + +husband = EmberObject.create({ + pets: 0 +}); + +Wife = EmberObject.extend({ + pets: alias('husband.pets') +}); + +wife = Wife.create({ + husband: husband +}); + +wife.get('pets'); // 0 + +// Someone gets a pet. +husband.set('pets', 1); +wife.get('pets'); // 1 +``` + +Note that bindings don't update immediately. Ember waits until all of your +application code has finished running before synchronizing changes, so you can +change a bound property as many times as you'd like without worrying about the +overhead of syncing bindings when values are transient. + +## One-Way Bindings + +A one-way binding only propagates changes in one direction, using +[`computed.oneWay()`](https://www.emberjs.com/api/ember/release/classes/@ember%2Fobject%2Fcomputed/methods/alias?anchor=oneWay&show=inherited%2Cprotected%2Cprivate%2Cdeprecated). Often, one-way bindings are a performance +optimization and you can safely use a two-way binding (which are de facto one-way bindings if you only ever change one side). +Sometimes one-way bindings are useful to achieve specific behaviour such as a +default that is the same as another property but can be overridden (e.g. a +shipping address that starts the same as a billing address but can later be +changed) + +```javascript +import EmberObject, { computed } from '@ember/object'; +import Component from '@ember/component'; +import { oneWay } from '@ember/object/computed'; + +user = EmberObject.create({ + fullName: 'Kara Gates' +}); + +UserComponent = Component.extend({ + userName: oneWay('user.fullName') +}); + +userComponent = UserComponent.create({ + user: user +}); + +// Changing the name of the user object changes +// the value on the view. +user.set('fullName', 'Krang Gates'); +// userComponent.userName will become "Krang Gates" + +// ...but changes to the view don't make it back to +// the object. +userComponent.set('userName', 'Truckasaurus Gates'); +user.get('fullName'); // "Krang Gates" +``` diff --git a/guides/v3.6.0/object-model/classes-and-instances.md b/guides/v3.6.0/object-model/classes-and-instances.md new file mode 100644 index 0000000000..65450b46f3 --- /dev/null +++ b/guides/v3.6.0/object-model/classes-and-instances.md @@ -0,0 +1,267 @@ +As you learn about Ember, you'll see code like `Component.extend()` and +`DS.Model.extend()`. Here, you'll learn about this `extend()` method, as well +as other major features of the Ember object model. + +### Defining Classes + +To define a new Ember _class_, call the [`extend()`][1] method on +[`EmberObject`][2]: + +[1]: https://www.emberjs.com/api/ember/release/classes/@ember%2Fobject/methods/extend?anchor=extend +[2]: https://www.emberjs.com/api/ember/release/modules/@ember%2Fobject + +```javascript +import EmberObject from '@ember/object'; + +const Person = EmberObject.extend({ + say(thing) { + alert(thing); + } +}); +``` + +This defines a new `Person` class with a `say()` method. + +You can also create a _subclass_ from any existing class by calling +its `extend()` method. For example, you might want to create a subclass +of Ember's built-in [`Component`][3] class: + +[3]: https://www.emberjs.com/api/ember/release/classes/Component + +```javascript {data-filename=app/components/todo-item.js} +import Component from '@ember/component'; + +export default Component.extend({ + classNameBindings: ['isUrgent'], + isUrgent: true +}); +``` + +### Overriding Parent Class Methods + +When defining a subclass, you can override methods but still access the +implementation of your parent class by calling the special `_super()` +method: + +```javascript +import EmberObject from '@ember/object'; + +const Person = EmberObject.extend({ + say(thing) { + alert(`${this.name} says: ${thing}`); + } +}); + +const Soldier = Person.extend({ + say(thing) { + // this will call the method in the parent class (Person#say), appending + // the string ', sir!' to the variable `thing` passed in + this._super(`${thing}, sir!`); + } +}); + +let yehuda = Soldier.create({ + name: 'Yehuda Katz' +}); + +yehuda.say('Yes'); // alerts "Yehuda Katz says: Yes, sir!" +``` + +In certain cases, you will want to pass arguments to `_super()` before or after overriding. + +This allows the original method to continue operating as it normally would. + +One common example is when overriding the [`normalizeResponse()`][4] hook in one of Ember-Data's serializers. + +A handy shortcut for this is to use a "spread operator", like `...arguments`: + +[4]: https://www.emberjs.com/api/ember-data/release/classes/DS.JSONAPISerializer/methods/normalizeResponse?anchor=normalizeResponse + +```javascript +normalizeResponse(store, primaryModelClass, payload, id, requestType) { + // Customize my JSON payload for Ember-Data + return this._super(...arguments); +} +``` + +The above example returns the original arguments (after your customizations) back to the parent class, so it can continue with its normal operations. + +### Creating Instances + +Once you have defined a class, you can create new _instances_ of that +class by calling its [`create()`][5] method. Any methods, properties and +computed properties you defined on the class will be available to +instances: + +[5]: https://www.emberjs.com/api/ember/release/classes/@ember%2Fobject/methods/create?anchor=create + +```javascript +import EmberObject from '@ember/object'; + +const Person = EmberObject.extend({ + say(thing) { + alert(`${this.name} says: ${thing}`); + } +}); + +let person = Person.create(); + +person.say('Hello'); // alerts " says: Hello" +``` + +When creating an instance, you can initialize the values of its properties +by passing an optional hash to the `create()` method: + +```javascript +import EmberObject from '@ember/object'; + +const Person = EmberObject.extend({ + helloWorld() { + alert(`Hi, my name is ${this.name}`); + } +}); + +let tom = Person.create({ + name: 'Tom Dale' +}); + +tom.helloWorld(); // alerts "Hi, my name is Tom Dale" +``` + +Note that for performance reasons, while calling `create()` you cannot redefine an instance's +computed properties and should not redefine existing or define new methods. You should only set simple properties when calling +`create()`. If you need to define or redefine methods or computed +properties, create a new subclass and instantiate that. + +By convention, properties or variables that hold classes are +PascalCased, while instances are not. So, for example, the variable +`Person` would point to a class, while `person` would point to an instance +(usually of the `Person` class). You should stick to these naming +conventions in your Ember applications. + +### Initializing Instances + +When a new instance is created, its [`init()`][6] method is invoked +automatically. This is the ideal place to implement setup required on new +instances: + +[6]: https://www.emberjs.com/api/ember/release/classes/EmberObject/methods/init?anchor=init + +```javascript +import EmberObject from '@ember/object'; + +const Person = EmberObject.extend({ + init() { + alert(`${this.name}, reporting for duty!`); + } +}); + +Person.create({ + name: 'Stefan Penner' +}); + +// alerts "Stefan Penner, reporting for duty!" +``` + +If you are subclassing a framework class, like `Ember.Component`, and you +override the `init()` method, make sure you call `this._super(...arguments)`! +If you don't, a parent class may not have an opportunity to do important +setup work, and you'll see strange behavior in your application. + +Arrays and objects defined directly on any `Ember.Object` are shared across all instances of that class. + +```javascript +import EmberObject from '@ember/object'; + +const Person = EmberObject.extend({ + shoppingList: ['eggs', 'cheese'] +}); + +Person.create({ + name: 'Stefan Penner', + addItem() { + this.shoppingList.pushObject('bacon'); + } +}); + +Person.create({ + name: 'Robert Jackson', + addItem() { + this.shoppingList.pushObject('sausage'); + } +}); + +// Stefan and Robert both trigger their addItem. +// They both end up with: ['eggs', 'cheese', 'bacon', 'sausage'] +``` + +To avoid this behavior, it is encouraged to initialize those arrays and object properties during `init()`. Doing so ensures each instance will be unique. + +```javascript +import EmberObject from '@ember/object'; + +const Person = EmberObject.extend({ + init() { + this.set('shoppingList', ['eggs', 'cheese']); + } +}); + +Person.create({ + name: 'Stefan Penner', + addItem() { + this.shoppingList.pushObject('bacon'); + } +}); + +Person.create({ + name: 'Robert Jackson', + addItem() { + this.shoppingList.pushObject('sausage'); + } +}); + +// Stefan ['eggs', 'cheese', 'bacon'] +// Robert ['eggs', 'cheese', 'sausage'] +``` + +### Accessing Object Properties + +When reading a property value of an object, you can in most cases use the common Javascript dot notation, e.g. `myObject.myProperty`. + +[Ember proxy objects][9] are the one big exception to this rule. If you're working with Ember proxy objects, including promise proxies for async relationships in Ember Data, you have to use Ember's [`get()`][7] accessor method to read values. + +Let's look at the following `blogPost` Ember Data model: + +```javascript {data-filename=app/models/blog-post.js} +import Model from 'ember-data/model'; +import attr from 'ember-data/attr'; +import { hasMany } from 'ember-data/relationships'; + +export default Model.extend({ + title: attr('string'), + body: attr('string'), + comments: hasMany('comment', { async: true }), +}); +``` + +To access the blog post's title you can simply write `blogPost.title`, whereas only the syntax `blogPost.get('comments')` will return the post's comments. + +Always use Ember's [`set()`][8] method to update property values. It will propagate the value change to computed properties, observers, templates, etc. If you "just" use Javascript's dot notation to update a property value, computed properties won't recalculate, observers won't fire and templates won't update. + +```javascript +import EmberObject from '@ember/object'; + +const Person = EmberObject.extend({ + name: 'Robert Jackson' +}); + +let person = Person.create(); + +person.name; // 'Robert Jackson' +person.set('name', 'Tobias Fünke'); +person.name; // 'Tobias Fünke' +``` + +[7]: https://www.emberjs.com/api/ember/release/classes/@ember%2Fobject/methods/get?anchor=get +[8]: https://www.emberjs.com/api/ember/release/classes/@ember%2Fobject/methods/set?anchor=set +[9]: https://emberjs.com/api/ember/3.3/classes/ObjectProxy diff --git a/guides/v3.6.0/object-model/computed-properties-and-aggregate-data.md b/guides/v3.6.0/object-model/computed-properties-and-aggregate-data.md new file mode 100644 index 0000000000..889d1170cc --- /dev/null +++ b/guides/v3.6.0/object-model/computed-properties-and-aggregate-data.md @@ -0,0 +1,237 @@ +When a computed property depends on the contents of an array, there are a few +extra methods you'll need to use in order to correctly recognize when the +contents of the array change. Arrays have two special keys you can append to +array properties to track changes on them, `[]` and `@each`. + +## `[]` + +Sometimes a computed property needs to update when items are added to, removed from, or replaced in an array. +In those cases we can use the `[]` array key to tell the property to update at the right time. +We'll use the familiar todo list for our examples: + +```javascript {data-filename=app/components/todo-list.js} +import EmberObject, { computed } from '@ember/object'; +import Component from '@ember/component'; + +export default Component.extend({ + todos: null, + + init() { + this.set('todos', [ + EmberObject.create({ title: 'Buy food', isDone: true }), + EmberObject.create({ title: 'Eat food', isDone: false }), + EmberObject.create({ title: 'Catalog Tomster collection', isDone: true }), + ]); + }, + + titles: computed('todos.[]', function() { + return this.todos.mapBy('title'); + }) +}); +``` + +The dependent key `todos.[]` instructs Ember.js to update bindings +and fire observers when any of the following events occurs: + +1. The `todos` property of the component is changed to a different array. +2. An item is added to the `todos` array. +3. An item is removed from the `todos` array. +4. An item is replaced in the `todos` array. + +Notably, the computed property will not update if an individual todo is mutated. +For that to happen, we need to use the special `@each` key. + +## `@each` + +Sometimes you have a computed property whose value depends on the properties of +items in an array. For example, you may have an array of todo items, and want +to calculate the incomplete todo's based on their `isDone` property. + +To facilitate this, Ember provides the `@each` key illustrated below: + +```javascript {data-filename=app/components/todo-list.js} +import EmberObject, { computed } from '@ember/object'; +import Component from '@ember/component'; + +export default Component.extend({ + todos: null, + + init() { + this.set('todos', [ + EmberObject.create({ isDone: true }), + EmberObject.create({ isDone: false }), + EmberObject.create({ isDone: true }), + ]); + }, + + incomplete: computed('todos.@each.isDone', function() { + let todos = this.todos; + return todos.filterBy('isDone', false); + }) +}); +``` + +Here, the dependent key `todos.@each.isDone` instructs Ember.js to update bindings +and fire observers when any of the following events occurs: + +1. The `todos` property of the component is changed to a different array. +2. An item is added to the `todos` array. +3. An item is removed from the `todos` array. +4. An item is replaced in the `todos` array. +5. The `isDone` property of any of the objects in the `todos` array changes. + +### Multiple Dependent Keys + +It's important to note that the `@each` key can be dependent on more than one key. +For example, if you are using `Ember.computed` to sort an array by multiple keys, +you would declare the dependency with braces: `todos.@each.{priority,title}` + +### When to use `[]` and `@each` + +Both `[]` and `@each` will update bindings when the array is replaced and when the members of the +array are changed. If you're using `@each` on a particular property, you don't also need to use `[]`: + +```javascript + //specifying both '[]' and '@each' is redundant here + incomplete: computed('todos.[]', 'todos.@each.isDone', function() { + ... + }) +``` + +Using `@each` is more expensive than `[]`, so default to `[]` if you don't actually have to observe property +changes on individual members of the array. + +### Computed Property Macros + +Ember also provides a computed property macro +[`computed.filterBy`](https://www.emberjs.com/api/ember/release/classes/@ember%2Fobject%2Fcomputed/methods/alias?anchor=filterBy), +which is a shorter way of expressing the above computed property: + +```javascript {data-filename=app/components/todo-list.js} +import EmberObject, { computed } from '@ember/object'; +import { filterBy } from '@ember/object/computed'; +import Component from '@ember/component'; + +export default Component.extend({ + todos: null, + + init() { + this.set('todos', [ + EmberObject.create({ isDone: true }), + EmberObject.create({ isDone: false }), + EmberObject.create({ isDone: true }), + ]); + }, + + incomplete: filterBy('todos', 'isDone', false) +}); +``` + +In both of the examples above, `incomplete` is an array containing the single incomplete todo: + +```javascript +import TodoListComponent from 'app/components/todo-list'; + +let todoListComponent = TodoListComponent.create(); +todoListComponent.get('incomplete.length'); +// 1 +``` + +If we change the todo's `isDone` property, the `incomplete` property is updated +automatically: + +```javascript +import EmberObject from '@ember/object'; + +let todos = todoListComponent.get('todos'); +let todo = todos.objectAt(1); +todo.set('isDone', true); + +todoListComponent.get('incomplete.length'); +// 0 + +todo = EmberObject.create({ isDone: false }); +todos.pushObject(todo); + +todoListComponent.get('incomplete.length'); +// 1 +``` + +Note that `@each` only works one level deep. You cannot use nested forms like +`todos.@each.owner.name` or `todos.@each.owner.@each.name`. + +## `[]` vs `@each` + +Sometimes you don't care if properties of individual array items change. In this +case use the `[]` key instead of `@each`. Computed properties dependent on an array +using the `[]` key will only update if items are added to or removed from the array, +or if the array property is set to a different array. For example: + +```javascript {data-filename=app/components/todo-list.js} +import EmberObject, { computed } from '@ember/object'; +import Component from '@ember/component'; + +export default Component.extend({ + todos: null, + + init() { + this.set('todos', [ + EmberObject.create({ isDone: true }), + EmberObject.create({ isDone: false }), + EmberObject.create({ isDone: true }), + ]); + }, + + selectedTodo: null, + indexOfSelectedTodo: computed('selectedTodo', 'todos.[]', function() { + return this.todos.indexOf(this.selectedTodo); + }) +}); +``` + +Here, `indexOfSelectedTodo` depends on `todos.[]`, so it will update if we add an item +to `todos`, but won't update if the value of `isDone` on a `todo` changes. + +Several of the [Ember.computed](https://www.emberjs.com/api/ember/release/classes/@ember%2Fobject%2Fcomputed) macros +utilize the `[]` key to implement common use-cases. For instance, to +create a computed property that mapped properties from an array, you could use +[Ember.computed.map](https://www.emberjs.com/api/ember/release/classes/@ember%2Fobject%2Fcomputed/methods/map?anchor=map) +or build the computed property yourself: + +```javascript +import EmberObject, { computed } from '@ember/object'; + +const Hamster = EmberObject.extend({ + excitingChores: computed('chores.[]', function() { + return this.chores.map(function(chore, index) { + return `CHORE ${index + 1}: ${chore.toUpperCase()}!`; + }); + }) +}); + +const hamster = Hamster.create({ + chores: ['clean', 'write more unit tests'] +}); + +hamster.excitingChores; // ['CHORE 1: CLEAN!', 'CHORE 2: WRITE MORE UNIT TESTS!'] +hamster.chores.pushObject('review code'); +hamster.excitingChores; // ['CHORE 1: CLEAN!', 'CHORE 2: WRITE MORE UNIT TESTS!', 'CHORE 3: REVIEW CODE!'] +``` + +By comparison, using the computed macro abstracts some of this away: + +```javascript +import EmberObject from '@ember/object'; +import { map } from '@ember/object/computed'; + +const Hamster = EmberObject.extend({ + excitingChores: map('chores', function(chore, index) { + return `CHORE ${index + 1}: ${chore.toUpperCase()}!`; + }) +}); +``` + +The computed macros expect you to use an array, so there is no need to use the +`[]` key in these cases. However, building your own custom computed property +requires you to tell Ember.js that it is watching for array changes, which is +where the `[]` key comes in handy. diff --git a/guides/v3.6.0/object-model/computed-properties.md b/guides/v3.6.0/object-model/computed-properties.md new file mode 100644 index 0000000000..5db295e0ac --- /dev/null +++ b/guides/v3.6.0/object-model/computed-properties.md @@ -0,0 +1,254 @@ +## What are Computed Properties? + +In a nutshell, computed properties let you declare functions as properties. +You create one by defining a computed property as a function, which Ember will automatically call when you ask for the property. +You can then use it the same way you would any normal, static property. + +It's super handy for taking one or more normal properties and transforming or manipulating their data to create a new value. + +### Computed properties in action + +We'll start with a simple example. +We have a `Person` object with `firstName` and `lastName` properties, but we also want a `fullName` property that joins the two names when either of them changes: + +```javascript +import EmberObject, { computed } from '@ember/object'; + +Person = EmberObject.extend({ + // these will be supplied by `create` + firstName: null, + lastName: null, + + fullName: computed('firstName', 'lastName', function() { + return `${this.firstName} ${this.lastName}`; + }) +}); + +let ironMan = Person.create({ + firstName: 'Tony', + lastName: 'Stark' +}); + +ironMan.fullName; // "Tony Stark" +``` + +This declares `fullName` to be a computed property, with `firstName` and `lastName` as the properties it depends on. +The first time you access the `fullName` property, the function will be called and the results will be cached. +Subsequent access of `fullName` will read from the cache without calling the function. +Changing any of the dependent properties causes the cache to invalidate, so that the computed function runs again on the next access. + +### Computed properties only recompute when they are consumed + +A computed property will only recompute its value when it is _consumed._ Properties are consumed in two ways: + +1. By being accessed, for example `ironMan.fullName` +2. By being referenced in a handlebars template that is currently being rendered, for example `{{ironMan.fullName}}` + +Outside of those two circumstances the code in the property will not run, even if one of the property's dependencies are changed. + +We'll modify the `fullName` property from the previous example to log to the console: + +```javascript +import Ember from 'ember'; + +… + fullName: computed('firstName', 'lastName', function() { + console.log('compute fullName'); // track when the property recomputes + return `${this.firstName} ${this.lastName}`; + }) +… +``` + +Using the new property, it will only log after a `fullName` is accessed, and then only if either the `firstName` or `lastName` has been previously changed: + +```javascript + +let ironMan = Person.create({ + firstName: 'Tony', + lastName: 'Stark' +}); + +ironMan.fullName; // 'compute fullName' +ironMan.set('firstName', 'Bruce') // no console output + +ironMan.fullName; // 'compute fullName' +ironMan.fullName; // no console output since dependencies have not changed +``` + + +### Multiple dependents on the same object + +In the previous example, the `fullName` computed property depends on two other properties of the same object. +However, you may find that you have to observe the properties of a different object. + +For example, look at this computed property: + +```javascript +import EmberObject, { computed } from '@ember/object'; + +let home = EmberObject.extend({ + location: { + streetName: 'Evergreen Terrace', + streetNumber: 742 + }, + + address: computed('location.streetName', 'location.streetNumber', function() { + return `${this.location.streetNumber} ${this.location.streetName}`; + }) +}); + +home.address // 742 Evergreen Terrace +home.set('location.streetNumber', 744) +home.address // 744 Evergreen Terrace +``` + +It is important to observe an object's properties, not the object itself that has properties nested inside. If the object reference `location` is used as a dependent key, the computed property will not recalculate when the `streetName` or `streetNumber` properties change. + +```javascript +import EmberObject, { computed } from '@ember/object'; + +let home = EmberObject.extend({ + location: { + streetName: 'Evergreen Terrace', + streetNumber: 742 + }, + + address: computed('location', function() { + return `${this.location.streetNumber} ${this.location.streetName}`; + }) +}); + +home.address // 742 Evergreen Terrace +home.set('location.streetNumber', 744) +home.address // 742 Evergreen Terrace +home.set('location', { + streetName: 'Evergreen Terrace', + streetNumber: 744 +}) +home.address // 744 Evergreen Terrace +``` + +Since both `streetName` and `streetNumber` are properties on the `location` object, we can use a short-hand syntax called _brace expansion_ to declare the dependents keys. +You surround the dependent properties with braces (`{}`), and separate with commas, like so: + +```javascript +import EmberObject, { computed } from '@ember/object'; + +let home = EmberObject.extend({ + location: { + streetName: 'Evergreen Terrace', + streetNumber: 742 + }, + + address: computed('location.{streetName,streetNumber}', function() { + return `${this.location.streetNumber} ${this.location.streetName}`; + }) +}); +``` + +### Chaining computed properties + +You can use computed properties as values to create new computed properties. +Let's add a `description` computed property to the previous example, +and use the existing `fullName` property and add in some other properties: + +```javascript +import EmberObject, { computed } from '@ember/object'; + +Person = EmberObject.extend({ + firstName: null, + lastName: null, + age: null, + country: null, + + fullName: computed('firstName', 'lastName', function() { + return `${this.firstName} ${this.lastName}`; + }), + + description: computed('fullName', 'age', 'country', function() { + return `${this.fullName}; Age: ${this.age}; Country: ${this.country}`; + }) +}); + +let captainAmerica = Person.create({ + firstName: 'Steve', + lastName: 'Rogers', + age: 80, + country: 'USA' +}); + +captainAmerica.get('description'); // "Steve Rogers; Age: 80; Country: USA" +``` + +### Dynamic updating + +Computed properties, by default, observe any changes made to the properties they depend on and are dynamically updated when they're called. +Let's use computed properties to dynamically update. + +```javascript +captainAmerica.set('firstName', 'William'); + +captainAmerica.description; // "William Rogers; Age: 80; Country: USA" +``` + +So this change to `firstName` was observed by `fullName` computed property, which was itself observed by the `description` property. + +Setting any dependent property will propagate changes through any computed properties that depend on them, all the way down the chain of computed properties you've created. + +### Setting Computed Properties + +You can also define what Ember should do when setting a computed property. +If you try to set a computed property, it will be invoked with the key (property name), and the value you want to set it to. +You must return the new intended value of the computed property from the setter function. + +```javascript +import EmberObject, { computed } from '@ember/object'; + +Person = EmberObject.extend({ + firstName: null, + lastName: null, + + fullName: computed('firstName', 'lastName', { + get(key) { + return `${this.firstName} ${this.lastName}`; + }, + set(key, value) { + let [firstName, lastName] = value.split(/\s+/); + this.set('firstName', firstName); + this.set('lastName', lastName); + return value; + } + }) +}); + + +let captainAmerica = Person.create(); +captainAmerica.set('fullName', 'William Burnside'); +captainAmerica.firstName; // William +captainAmerica.lastName; // Burnside +``` + +### Computed property macros + +Some types of computed properties are very common. +Ember provides a number of computed property macros, which are shorter ways of expressing certain types of computed property. + +In this example, the two computed properties are equivalent: + +```javascript +import EmberObject, { computed } from '@ember/object'; +import { equal } from '@ember/object/computed'; + +Person = EmberObject.extend({ + fullName: 'Tony Stark', + + isIronManLongWay: computed('fullName', function() { + return this.fullName === 'Tony Stark'; + }), + + isIronManShortWay: equal('fullName', 'Tony Stark') +}); +``` + +To see the full list of computed property macros, have a look at +[the API documentation](https://www.emberjs.com/api/ember/release/modules/@ember%2Fobject) diff --git a/guides/v3.6.0/object-model/enumerables.md b/guides/v3.6.0/object-model/enumerables.md new file mode 100644 index 0000000000..f9691b6b53 --- /dev/null +++ b/guides/v3.6.0/object-model/enumerables.md @@ -0,0 +1,209 @@ +In Ember.js, an enumerable is any object that contains a number of child +objects, and which allows you to work with those children using the +[`MutableArray`](https://emberjs.com/api/ember/release/classes/MutableArray) API. The most common +enumerable in the majority of apps is the native JavaScript array, which +Ember.js extends to conform to the enumerable interface. + +By providing a standardized interface for dealing with enumerables, +Ember.js allows you to completely change the way your underlying data is +stored without having to modify the other parts of your application that +access it. + +The enumerable API follows ECMAScript specifications as much as +possible. This minimizes incompatibility with other libraries, and +allows Ember.js to use the native browser implementations in arrays +where available. + +## Use of Observable Methods and Properties + +In order for Ember to observe when you make a change to an enumerable, you need +to use special methods that `MutableArray` provides. For example, if you add +an element to an array using the standard JavaScript method `push()`, Ember will +not be able to observe the change, but if you use the enumerable method +`pushObject()`, the change will propagate throughout your application. + +Here is a list of standard JavaScript array methods and their observable +enumerable equivalents: + + + + + + + + + + + + +
    Standard MethodObservable Equivalent
    poppopObject
    pushpushObject
    reversereverseObjects
    shiftshiftObject
    unshiftunshiftObject
    + +Additionally, to retrieve the first and last objects in an array +in an observable fashion, you should use `myArray.get('firstObject')` and +`myArray.get('lastObject')`, respectively. + +## API Overview + +In the rest of this guide, we'll explore some of the most common enumerable +conveniences. For the full list, please see the [`MutableArray API +reference documentation`](https://emberjs.com/api/ember/release/classes/MutableArray). + +### Iterating Over an Enumerable + +To enumerate all the values of an enumerable object, use the [`forEach()`](https://emberjs.com/api/ember/release/classes/MutableArray/methods/forEach?anchor=forEach) +method: + + +```javascript +let food = ['Poi', 'Ono', 'Adobo Chicken']; + +food.forEach((item, index) => { + console.log(`Menu Item ${index+1}: ${item}`); +}); + +// Menu Item 1: Poi +// Menu Item 2: Ono +// Menu Item 3: Adobo Chicken +``` + +### First and Last Objects + +All enumerables expose [`firstObject`](https://emberjs.com/api/ember/release/classes/MutableArray/properties/firstObject?anchor=firstObject) and [`lastObject`](https://emberjs.com/api/ember/release/classes/MutableArray/properties/lastObject?anchor=lastObject) properties +that you can bind to. + + + +```javascript +let animals = ['rooster', 'pig']; + +animals.get('lastObject'); +//=> "pig" + +animals.pushObject('peacock'); + +animals.get('lastObject'); +//=> "peacock" +``` + +### Map + +You can easily transform each item in an enumerable using the +[`map()`](https://emberjs.com/api/ember/release/classes/MutableArray/methods/map?anchor=map) method, which creates a new array with results of calling a +function on each item in the enumerable. + + +```javascript +let words = ['goodbye', 'cruel', 'world']; + +let emphaticWords = words.map(item => `${item}!`); +//=> ["goodbye!", "cruel!", "world!"] +``` + +If your enumerable is composed of objects, there is a [`mapBy()`](https://emberjs.com/api/ember/release/classes/MutableArray/methods/mapBy?anchor=mapBy) +method that will extract the named property from each of those objects +in turn and return a new array: + + +```javascript +import EmberObject from '@ember/object'; + +let hawaii = EmberObject.create({ + capital: 'Honolulu' +}); + +let california = EmberObject.create({ + capital: 'Sacramento' +}); + +let states = [hawaii, california]; + +states.mapBy('capital'); +//=> ["Honolulu", "Sacramento"] +``` + +### Filtering + +Another common task to perform on an enumerable is to take the +enumerable as input, and return an Array after filtering it based on +some criteria. + +For arbitrary filtering, use the [`filter()`](https://emberjs.com/api/ember/release/classes/MutableArray/methods/filter?anchor=filter) method. The filter method +expects the callback to return `true` if Ember should include it in the +final Array, and `false` or `undefined` if Ember should not. + + +```javascript +let arr = [1, 2, 3, 4, 5]; + +arr.filter((item, index, self) => item < 4); + +//=> [1, 2, 3] +``` + +When working with a collection of Ember objects, you will often want to filter a set of objects based upon the value of some property. The [`filterBy()`](https://emberjs.com/api/ember/release/classes/MutableArray/methods/filterBy?anchor=filterBy) method provides a shortcut. + + +```javascript +import EmberObject from '@ember/object'; + +Todo = EmberObject.extend({ + title: null, + isDone: false +}); + +let todos = [ + Todo.create({ title: 'Write code', isDone: true }), + Todo.create({ title: 'Go to sleep' }) +]; + +todos.filterBy('isDone', true); + +// returns an Array containing only items with `isDone == true` +``` + +If you only want to return the first matched value, rather than an Array +containing all of the matched values, you can use [`find()`](https://emberjs.com/api/ember/release/classes/MutableArray/methods/find?anchor=find) and [`findBy()`](https://emberjs.com/api/ember/release/classes/MutableArray/methods/findBy?anchor=findBy), +which work like `filter()` and `filterBy()`, but return only one item. + + +### Aggregate Information (Every or Any) + +To find out whether every item in an enumerable matches some condition, you can +use the [`every()`](https://emberjs.com/api/ember/release/classes/MutableArray/methods/every?anchor=every) method: + + +```javascript +import EmberObject from '@ember/object'; + +Person = EmberObject.extend({ + name: null, + isHappy: false +}); + +let people = [ + Person.create({ name: 'Yehuda', isHappy: true }), + Person.create({ name: 'Majd', isHappy: false }) +]; + +people.every((person, index, self) => person.get('isHappy')); + +//=> false +``` + +To find out whether at least one item in an enumerable matches some condition, +you can use the [`any()`](https://emberjs.com/api/ember/release/classes/MutableArray/methods/any?anchor=any) method: + + +```javascript +people.any((person, index, self) => person.get('isHappy')); + +//=> true +``` + +Like the filtering methods, the `every()` and `any()` methods have +analogous `isEvery()` and `isAny()` methods. + +```javascript +people.isEvery('isHappy', true); // false +people.isAny('isHappy', true); // true +``` diff --git a/guides/v3.6.0/object-model/index.md b/guides/v3.6.0/object-model/index.md new file mode 100644 index 0000000000..02498c5a75 --- /dev/null +++ b/guides/v3.6.0/object-model/index.md @@ -0,0 +1,21 @@ +One of the first things you'll notice when you generate JavaScript files in Ember is that most of the code you will write goes inside of an object. While an Ember Object might look a lot like an ES2015 JavaScript class, it has some special properties. + +### Introducing: Ember Objects + +The [Ember Object](https://www.emberjs.com/api/ember/release/classes/EmberObject) class extends plain JavaScript objects to provide core framework functions such as participating in Ember's [binding system](../object-model/bindings/) or how changes to an object automatically trigger updates to the user interface. + +Most objects in Ember, including Routes, Models, Services, Mixins, Controllers, and Components extend the `EmberObject` class. + +### Why Ember Objects? + +The most important reason is that an Ember Object can be watched for changes – or _observed_. For example, being [observable](https://www.emberjs.com/api/ember/release/classes/Observable) is important for [computed properties](../object-model/computed-properties/). It is one of the fundamental ways that models, controllers and views communicate with each other in an Ember application. + +### More on Ember Objects + +The [@ember/object](https://www.emberjs.com/api/ember/release/modules/@ember%2Fobject) package also provides a class system, supporting features like mixins and constructor methods, and being observable. + +Some features in Ember's object model are not present in JavaScript classes or common patterns, but all are aligned as much as possible with the language and proposed additions. + +Ember also extends the JavaScript `Array` prototype with its [@ember/enumerable](https://emberjs.com/api/ember/release/classes/Enumerable) interface to provide change observation for arrays. + +Finally, Ember extends the `String` prototype with a few [formatting and localization methods](https://www.emberjs.com/api/ember/release/classes/String). diff --git a/guides/v3.6.0/object-model/observers.md b/guides/v3.6.0/object-model/observers.md new file mode 100644 index 0000000000..f6733f5a34 --- /dev/null +++ b/guides/v3.6.0/object-model/observers.md @@ -0,0 +1,155 @@ +*__Note:__ Observers are often over-used by new Ember developers. Observers are used +heavily within the Ember framework itself, but for most problems Ember app +developers face, computed properties are the appropriate solution.* + +Ember supports observing any property, including computed properties. + +Observers should contain behavior that reacts to changes in another property. +Observers are especially useful when you need to perform some behavior after a +binding has finished synchronizing. + +You can set up an observer on an object by using `observer`: + +```javascript +import EmberObject, { + computed, + observer +} from '@ember/object'; + +Person = EmberObject.extend({ + // these will be supplied by `create` + firstName: null, + lastName: null, + + fullName: computed('firstName', 'lastName', function() { + return `${this.firstName} ${this.lastName}`; + }), + + fullNameChanged: observer('fullName', function() { + // deal with the change + console.log(`fullName changed to: ${this.fullName}`); + }) +}); + +let person = Person.create({ + firstName: 'Yehuda', + lastName: 'Katz' +}); + +// observer won't fire until `fullName` is consumed first +person.get('fullName'); // "Yehuda Katz" +person.set('firstName', 'Brohuda'); // fullName changed to: Brohuda Katz +``` + +Because the `fullName` computed property depends on `firstName`, +updating `firstName` will fire observers on `fullName` as well. + +### Observers and asynchrony + +Observers in Ember are currently synchronous. This means that they will fire +as soon as one of the properties they observe changes. Because of this, it +is easy to introduce bugs where properties are not yet synchronized: + +```javascript +import { observer } from '@ember/object'; + +Person.reopen({ + lastNameChanged: observer('lastName', function() { + // The observer depends on lastName and so does fullName. Because observers + // are synchronous, when this function is called the value of fullName is + // not updated yet so this will log the old value of fullName + console.log(this.fullName); + }) +}); +``` + +This synchronous behavior can also lead to observers being fired multiple +times when observing multiple properties: + +```javascript +import { observer } from '@ember/object'; + +Person.reopen({ + partOfNameChanged: observer('firstName', 'lastName', function() { + // Because both firstName and lastName were set, this observer will fire twice. + }) +}); + +person.set('firstName', 'John'); +person.set('lastName', 'Smith'); +``` + +To get around these problems, you should make use of [`Ember.run.once()`](https://www.emberjs.com/api/ember/release/classes/@ember%2Frunloop/methods/once?anchor=once). +This will ensure that any processing you need to do only happens once, and +happens in the next run loop once all bindings are synchronized: + + +```javascript +import { observer } from '@ember/object'; +import { once } from '@ember/runloop'; + +Person.reopen({ + partOfNameChanged: observer('firstName', 'lastName', function() { + once(this, 'processFullName'); + }), + + processFullName() { + // This will only fire once if you set two properties at the same time, and + // will also happen in the next run loop once all properties are synchronized + console.log(this.fullName); + } +}); + +person.set('firstName', 'John'); +person.set('lastName', 'Smith'); +``` + +### Observers and object initialization + +Observers never fire until after the initialization of an object is complete. + +If you need an observer to fire as part of the initialization process, you +cannot rely on the side effect of `set`. Instead, specify that the observer +should also run after `init` by using [`Ember.on()`](https://emberjs.com/api/ember/2.15/namespaces/Ember/methods/on?anchor=on): + + +```javascript +import EmberObject, { observer } from '@ember/object'; +import { on } from '@ember/object/evented'; + +Person = EmberObject.extend({ + init() { + this.set('salutation', 'Mr/Ms'); + }, + + salutationDidChange: on('init', observer('salutation', function() { + // some side effect of salutation changing + })) +}); +``` + +### Unconsumed Computed Properties Do Not Trigger Observers + +If you never `get()` a computed property, its observers will not fire even if +its dependent keys change. You can think of the value changing from one unknown +value to another. + +This doesn't usually affect application code because computed properties are +almost always observed at the same time as they are fetched. For example, you get +the value of a computed property, put it in DOM (or draw it with D3), and then +observe it so you can update the DOM once the property changes. + +If you need to observe a computed property but aren't currently retrieving it, +get it in your `init()` method. + +### Outside of class definitions + +You can also add observers to an object outside of a class definition +using [`addObserver()`](https://www.emberjs.com/api/ember/release/classes/@ember%2Fobject%2Fobservers/methods/addObserver?anchor=addObserver): + + +```javascript +person.addObserver('fullName', function() { + // deal with the change +}); +``` diff --git a/guides/v3.6.0/object-model/reopening-classes-and-instances.md b/guides/v3.6.0/object-model/reopening-classes-and-instances.md new file mode 100644 index 0000000000..e7727b697f --- /dev/null +++ b/guides/v3.6.0/object-model/reopening-classes-and-instances.md @@ -0,0 +1,46 @@ +You don't need to define a class all at once. You can reopen a class and +define new properties using the +[`reopen()`](https://emberjs.com/api/ember/2.15/classes/Ember.Object/methods/reopen?anchor=reopen) +method. + +```javascript +Person.reopen({ + isPerson: true +}); + +Person.create().get('isPerson'); // true +``` + +When using `reopen()`, you can also override existing methods and +call `this._super`. + + +```javascript +Person.reopen({ + // override `say` to add an ! at the end + say(thing) { + this._super(thing + '!'); + } +}); +``` + +`reopen()` is used to add instance methods and properties that are shared +across all instances of a class. It does not add +methods and properties to a particular instance of a class as in vanilla JavaScript (without using prototype). + +But when you need to add static methods or static properties to the class itself +you can use [`reopenClass()`](https://emberjs.com/api/ember/2.15/classes/Ember.Object/methods/reopenClass?anchor=reopenClass). + +```javascript +// add static property to class +Person.reopenClass({ + isPerson: false +}); +// override property of existing and future Person instances +Person.reopen({ + isPerson: true +}); + +Person.isPerson; // false - because it is static property created by `reopenClass` +Person.create().get('isPerson'); // true +``` diff --git a/guides/v3.6.0/pages.yml b/guides/v3.6.0/pages.yml new file mode 100644 index 0000000000..480ca62896 --- /dev/null +++ b/guides/v3.6.0/pages.yml @@ -0,0 +1,268 @@ +- title: "Guides and Tutorials" + url: 'index' + skip_toc: true + pages: + - title: "Ember.js Guides" + url: "" + +- title: "Getting Started" + url: 'getting-started' + pages: + - title: "Quick Start" + url: "quick-start" + - title: "Installing Ember" + url: "index" + - title: "Core Concepts" + url: "core-concepts" + - title: "JavaScript Primer" + url: "js-primer" + +- title: "Tutorial" + url: 'tutorial' + pages: + - title: "Creating Your App" + url: "ember-cli" + - title: "Planning Your App" + url: 'acceptance-test' + - title: "Routes and Templates" + url: "routes-and-templates" + - title: "The Model Hook" + url: "model-hook" + - title: "Installing Addons" + url: "installing-addons" + - title: "Building a Simple Component" + url: "simple-component" + - title: "Creating a Handlebars Helper" + url: "hbs-helper" + - title: "Using Ember Data" + url: "ember-data" + - title: "Building a Complex Component" + url: "autocomplete-component" + - title: "Services and Utilities" + url: "service" + - title: "Adding Nested Routes" + url: "subroutes" + - title: "Deploying" + url: "deploying" + +- title: "The Object Model" + url: 'object-model' + pages: + - title: "Objects in Ember" + url: "index" + - title: "Classes and Instances" + url: "classes-and-instances" + - title: "Reopening Classes and Instances" + url: "reopening-classes-and-instances" + - title: "Computed Properties" + url: "computed-properties" + - title: "Computed Properties and Aggregate Data" + url: "computed-properties-and-aggregate-data" + - title: "Observers" + url: "observers" + - title: "Bindings" + url: "bindings" + - title: "Enumerables" + url: 'enumerables' + +- title: "Routing" + url: 'routing' + pages: + - title: "Introduction" + url: "index" + - title: "Defining Your Routes" + url: "defining-your-routes" + - title: "Specifying a Route's Model" + url: "specifying-a-routes-model" + - title: "Rendering a Template" + url: "rendering-a-template" + - title: "Redirecting" + url: "redirection" + - title: "Preventing and Retrying Transitions" + url: "preventing-and-retrying-transitions" + - title: "Loading / Error Substates" + url: "loading-and-error-substates" + - title: "Query Parameters" + url: "query-params" + - title: "Asynchronous Routing" + url: "asynchronous-routing" + +- title: "Templates" + url: 'templates' + pages: + - title: "Handlebars Basics" + url: "handlebars-basics" + - title: "Built-in Helpers" + url: "built-in-helpers" + - title: "Conditionals" + url: "conditionals" + - title: "Displaying a List of Items" + url: "displaying-a-list-of-items" + - title: "Displaying the Keys in an Object" + url: "displaying-the-keys-in-an-object" + - title: "Binding Element Attributes" + url: "binding-element-attributes" + - title: "Links" + url: "links" + - title: "Actions" + url: "actions" + - title: "Input Helpers" + url: "input-helpers" + - title: "Development Helpers" + url: "development-helpers" + - title: "Writing Helpers" + url: "writing-helpers" + +- title: "Components" + url: 'components' + pages: + - title: "Defining a Component" + url: "defining-a-component" + - title: "The Component Lifecycle" + url: "the-component-lifecycle" + - title: "Passing Properties to a Component" + url: "passing-properties-to-a-component" + - title: "Wrapping Content in a Component" + url: "wrapping-content-in-a-component" + - title: "Customizing a Component's Element" + url: "customizing-a-components-element" + - title: "Using Block Params" + url: "block-params" + - title: "Handling Events" + url: "handling-events" + - title: "Triggering Changes with Actions" + url: "triggering-changes-with-actions" + +- title: "Controllers" + url: 'controllers' + pages: + - title: "Introduction" + url: "index" + +- title: "Models" + url: 'models' + pages: + - title: "Introduction" + url: "index" + - title: "Defining Models" + url: "defining-models" + - title: "Finding Records" + url: "finding-records" + - title: "Creating, Updating and Deleting" + url: "creating-updating-and-deleting-records" + - title: "Relationships" + url: "relationships" + - title: "Pushing Records into the Store" + url: "pushing-records-into-the-store" + - title: "Handling Metadata" + url: "handling-metadata" + - title: "Customizing Adapters" + url: "customizing-adapters" + - title: "Customizing Serializers" + url: "customizing-serializers" + +- title: "Application Concerns" + url: 'applications' + pages: + - title: "Applications and Instances" + url: "applications-and-instances" + - title: "Dependency Injection" + url: "dependency-injection" + - title: "Initializers" + url: "initializers" + - title: "Services" + url: "services" + - title: "The Run Loop" + url: "run-loop" + +- title: "Testing" + url: 'testing' + pages: + - title: "Introduction" + url: "index" + - title: "Application Tests" + url: "acceptance" + - title: "Testing Basics" + url: "unit-testing-basics" + - title: "Testing Components" + url: "testing-components" + - title: "Testing Helpers" + url: "testing-helpers" + - title: "Testing Controllers" + url: "testing-controllers" + - title: "Testing Routes" + url: "testing-routes" + - title: "Testing Models" + url: "testing-models" + +- title: "Ember Inspector" + url: "ember-inspector" + pages: + - title: "Introduction" + url: "index" + - title: "Installing the Inspector" + url: "installation" + - title: "Object Inspector" + url: "object-inspector" + - title: "The Component Tree" + url: "component-tree" + - title: "The View Tree" + url: "view-tree" + - title: "Inspecting Routes" + url: "routes" + - title: "Data Tab" + url: "data" + - title: "Tackling Deprecations" + url: "deprecations" + - title: "Library Info" + url: "info" + - title: "Debugging Promises" + url: "promises" + - title: "Inspecting Objects via the Container" + url: "container" + - title: "Rendering Performance" + url: "render-performance" + - title: "Troubleshooting" + url: "troubleshooting" + +- title: "Addons and Dependencies" + url: "addons-and-dependencies" + pages: + - title: "Managing Dependencies" + url: "managing-dependencies" + +- title: "Configuring Ember.js" + url: 'configuring-ember' + pages: + - title: "Configuring Your App" + url: "configuring-your-app" + - title: "Configuring Ember CLI" + url: "configuring-ember-cli" + - title: "Handling Deprecations" + url: "handling-deprecations" + - title: "Disabling Prototype Extensions" + url: "disabling-prototype-extensions" + - title: "Specifying the URL Type" + url: "specifying-url-type" + - title: "Embedding Applications" + url: "embedding-applications" + - title: "Feature Flags" + url: "feature-flags" + - title: "Build targets" + url: "build-targets" + - title: "Debugging" + url: "debugging" + +- title: "Contributing to Ember.js" + url: 'contributing' + pages: + - title: "Adding New Features" + url: "adding-new-features" + - title: "Repositories" + url: "repositories" + +- title: "Glossary" + url: "glossary" + pages: + - title: "Web Development" + url: "web-development" diff --git a/guides/v3.6.0/routing/asynchronous-routing.md b/guides/v3.6.0/routing/asynchronous-routing.md new file mode 100644 index 0000000000..fb1c14c158 --- /dev/null +++ b/guides/v3.6.0/routing/asynchronous-routing.md @@ -0,0 +1,173 @@ +This section covers some more advanced features of the router and its +capability for handling complex async logic within your app. + +### A Word on Promises... + +Ember's approach to handling asynchronous logic in the router makes +heavy use of the concept of Promises. In short, promises are objects that +represent an eventual value. A promise can either _fulfill_ +(successfully resolve the value) or _reject_ (fail to resolve the +value). The way to retrieve this eventual value, or handle the cases +when the promise rejects, is via the promise's [`then()`](https://www.emberjs.com/api/ember/release/classes/Promise/methods/then?anchor=then) method, which +accepts two optional callbacks, one for fulfillment and one for +rejection. If the promise fulfills, the fulfillment handler gets called +with the fulfilled value as its sole argument, and if the promise rejects, +the rejection handler gets called with a reason for the rejection as its +sole argument. For example: + + +```javascript +let promise = fetchTheAnswer(); + +promise.then(fulfill, reject); + +function fulfill(answer) { + console.log(`The answer is ${answer}`); +} + +function reject(reason) { + console.log(`Couldn't get the answer! Reason: ${reason}`); +} +``` + +Much of the power of promises comes from the fact that they can be +chained together to perform sequential asynchronous operations: + +```javascript +// Note: jQuery AJAX methods return promises +let usernamesPromise = Ember.$.getJSON('/usernames.json'); + +usernamesPromise.then(fetchPhotosOfUsers) + .then(applyInstagramFilters) + .then(uploadTrendyPhotoAlbum) + .then(displaySuccessMessage, handleErrors); +``` + +In the above example, if any of the methods +`fetchPhotosOfUsers`, `applyInstagramFilters`, or +`uploadTrendyPhotoAlbum` returns a promise that rejects, +`handleErrors` will be called with +the reason for the failure. In this manner, promises approximate an +asynchronous form of try-catch statements that prevent the rightward +flow of nested callback after nested callback and facilitate a saner +approach to managing complex asynchronous logic in your applications. + +This guide doesn't intend to fully delve into all the different ways +promises can be used, but if you'd like a more thorough introduction, +take a look at the readme for [RSVP](https://github.com/tildeio/rsvp.js), +the promise library that Ember uses. + +### The Router Pauses for Promises + +When transitioning between routes, the Ember router collects all of the +models (via the `model` hook) that will be passed to the route's +controllers at the end of the transition. If the `model` hook (or the related +`beforeModel` or `afterModel` hooks) return normal (non-promise) objects or +arrays, the transition will complete immediately. But if the `model` hook +(or the related `beforeModel` or `afterModel` hooks) returns a promise (or +if a promise was provided as an argument to `transitionTo`), the transition +will pause until that promise fulfills or rejects. + +The router considers any object with a `then()` method +defined on it to be a promise. + +If the promise fulfills, the transition will pick up where it left off and +begin resolving the next (child) route's model, pausing if it too is a +promise, and so on, until all destination route models have been +resolved. The values passed to the [`setupController()`](https://www.emberjs.com/api/ember/release/classes/Route/methods/setupController?anchor=setupController) hook for each route +will be the fulfilled values from the promises. + + +A basic example: + +```javascript {data-filename=app/routes/tardy.js} +import Route from '@ember/routing/route'; +import RSVP from 'rsvp'; +import { later } from '@ember/runloop'; + +export default Route.extend({ + model() { + return new RSVP.Promise(function(resolve) { + later(function() { + resolve({ msg: 'Hold Your Horses' }); + }, 3000); + }); + }, + + setupController(controller, model) { + console.log(model.msg); // "Hold Your Horses" + } +}); +``` + +When transitioning into `route:tardy`, the `model()` hook will be called and +return a promise that won't resolve until 3 seconds later, during which time +the router will be paused in mid-transition. When the promise eventually +fulfills, the router will continue transitioning and eventually call +`route:tardy`'s `setupController()` hook with the resolved object. + +This pause-on-promise behavior is extremely valuable for when you need +to guarantee that a route's data has fully loaded before displaying a +new template. + +### When Promises Reject... + +We've covered the case when a model promise fulfills, but what if it rejects? + +By default, if a model promise rejects during a transition, the transition is +aborted, no new destination route templates are rendered, and an error +is logged to the console. + +You can configure this error-handling logic via the `error` handler on +the route's `actions` hash. When a promise rejects, an `error` event +will be fired on that route and bubble up to `route:application`'s +default error handler unless it is handled by a custom error handler +along the way, e.g.: + +```javascript {data-filename=app/routes/good-for-nothing.js} +import Route from '@ember/routing/route'; +import RSVP from 'rsvp'; + +export default Route.extend({ + model() { + return RSVP.reject("FAIL"); + }, + + actions: { + error(reason) { + alert(reason); // "FAIL" + + // Can transition to another route here, e.g. + // this.transitionTo('index'); + + // Uncomment the line below to bubble this error event: + // return true; + } + } +}); +``` + +In the above example, the error event would stop right at +`route:good-for-nothing`'s error handler and not continue to bubble. To +make the event continue bubbling up to `route:application`, you can +return true from the error handler. + +### Recovering from Rejection + +Rejected model promises halt transitions, but because promises are chainable, +you can catch promise rejects within the `model` hook itself and convert +them into fulfills that won't halt the transition. + +```javascript {data-filename=app/routes/funky.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model() { + return iHopeThisWorks().catch(function() { + // Promise rejected, fulfill with some default value to + // use as the route's model and continue on with the transition + return { msg: 'Recovered from rejected promise' }; + }); + } +}); +``` diff --git a/guides/v3.6.0/routing/defining-your-routes.md b/guides/v3.6.0/routing/defining-your-routes.md new file mode 100644 index 0000000000..9bbb48f6af --- /dev/null +++ b/guides/v3.6.0/routing/defining-your-routes.md @@ -0,0 +1,288 @@ +When your application starts, the router matches the current URL to the _routes_ +that you've defined. The routes, in turn, are responsible for displaying +templates, loading data, and setting up application state. + +To define a route, run + +```bash +ember generate route route-name +``` + +This creates a route file at `app/routes/route-name.js`, a template for the route at `app/templates/route-name.hbs`, +and a unit test file at `tests/unit/routes/route-name-test.js`. +It also adds the route to the router. + +## Basic Routes + +The [`map()`](https://www.emberjs.com/api/ember/release/classes/EmberRouter/methods/map?anchor=map) method +of your Ember application's router can be invoked to define URL mappings. When +calling `map()`, you should pass a function that will be invoked with the value +`this` set to an object which you can use to create routes. + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('about', { path: '/about' }); + this.route('favorites', { path: '/favs' }); +}); +``` + +Now, when the user visits `/about`, Ember will render the `about` +template. Visiting `/favs` will render the `favorites` template. + +You can leave off the path if it is the same as the route +name. In this case, the following is equivalent to the above example: + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('about'); + this.route('favorites', { path: '/favs' }); +}); +``` + +Inside your templates, you can use [`{{link-to}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/link-to?anchor=link-to) to navigate between +routes, using the name that you provided to the `route` method. + +```handlebars +{{#link-to "index"}}{{/link-to}} + + +``` + +The `{{link-to}}` helper will also add an `active` class to the link that +points to the currently active route. + +Multi-word route names are conventionally dasherized, such as: + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('blog-post', { path: '/blog-post' }); +}); +``` + +The route defined above will by default use the `blog-post.js` route handler, +the `blog-post.hbs` template, and be referred to as `blog-post` in any +`{{link-to}}` helpers. + +Multi-word route names that break this convention, such as: + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('blog_post', { path: '/blog-post' }); +}); +``` + +will still by default use the `blog-post.js` route handler and the +`blog-post.hbs` template, but will be referred to as `blog_post` in any +`{{link-to}}` helpers. + +## Nested Routes + +Often you'll want to have a template that displays inside another template. +For example, in a blogging application, instead of going from a list of blog +posts to creating a new post, you might want to have the post creation page +display next to the list. + +In these cases, you can use nested routes to display one template inside +of another. + +You can define nested routes by passing a callback to `this.route`: + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('posts', function() { + this.route('new'); + }); +}); +``` + +Assuming you have already generated the `posts` route, to generate the above nested route you would run: + +```bash +ember generate route posts/new +``` + +And then add the `{{outlet}}` helper to your template where you want the nested +template to display: + +```handlebars {data-filename=templates/posts.hbs} +

    Posts

    + +{{outlet}} +``` + +This router creates a route for `/posts` and for `/posts/new`. When a user +visits `/posts`, they'll simply see the `posts.hbs` template. (Below, [index +routes](#toc_index-routes) explains an important addition to this.) When the +user visits `posts/new`, they'll see the `posts/new.hbs` template rendered into +the `{{outlet}}` of the `posts` template. + +A nested route name includes the names of its ancestors. +If you want to transition to a route (either +via `transitionTo` or `{{#link-to}}`), make sure to use the full route +name (`posts.new`, not `new`). + +## The application route + +The `application` route is entered when your app first boots up. Like other +routes, it will load a template with the same name (`application` in +this case) by default. +You should put your header, footer, and any other decorative content +here. All other routes will render +their templates into the `application.hbs` template's `{{outlet}}`. + +This route is part of every application, so you don't need to +specify it in your `app/router.js`. + +## Index Routes + +At every level of nesting (including the top level), Ember +automatically provides a route for the `/` path named `index`. +To see when a new level of nesting occurs, check the router, +whenever you see a `function`, that's a new level. + +For example, if you write a simple router like this: + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('favorites'); +}); +``` + +It is the equivalent of: + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('index', { path: '/' }); + this.route('favorites'); +}); +``` + +The `index` template will be rendered into the `{{outlet}}` in the +`application` template. If the user navigates to `/favorites`, +Ember will replace the `index` template with the `favorites` +template. + +A nested router like this: + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('posts', function() { + this.route('favorites'); + }); +}); +``` + +Is the equivalent of: + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('index', { path: '/' }); + this.route('posts', function() { + this.route('index', { path: '/' }); + this.route('favorites'); + }); +}); +``` + +If the user navigates to `/posts`, the current route will be +`posts.index`, and the `posts/index` template +will be rendered into the `{{outlet}}` in the `posts` template. + +If the user then navigates to `/posts/favorites`, Ember will +replace the `{{outlet}}` in the `posts` template with the +`posts/favorites` template. + +## Dynamic Segments + +One of the responsibilities of a route is to load a model. + +For example, if we have the route `this.route('posts');`, our +route might load all of the blog posts for the app. + +Because `/posts` represents a fixed model, we don't need any +additional information to know what to retrieve. However, if we want a route +to represent a single post, we would not want to have to hardcode every +possible post into the router. + +Enter _dynamic segments_. + +A dynamic segment is a portion of a URL that starts with a `:` and is followed by an identifier. + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('posts'); + this.route('post', { path: '/post/:post_id' }); +}); +``` + +If the user navigates to `/post/5`, the route will then have the `post_id` of +`5` to use to load the correct post. +Ember follows the convention of `:model-name_id` for two reasons. +The first reason is that Routes know how to fetch the right model by default, if you follow the convention. +The second is that `params` is an object, and can only have one value associated with a key. +To put it in code, the following will *not* work properly: + +```javascript {data-filename=app/router.js} +// This won't work! The dynamic segments will collide. +Router.map(function() { + this.route('photo', { path: '/photo/:id' }, function() { + this.route('comment', { path: '/comment/:id' }); + }); +}); +``` + +But the following will: + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('photo', { path: '/photo/:photo_id' }, function() { + this.route('comment', { path: '/comment/:comment_id' }); + }); +}); +``` + +In the next section, [Specifying a Route's Model](../specifying-a-routes-model/), you will learn more about how to load a model. + +## Wildcard / globbing routes + +You can define wildcard routes that will match multiple URL segments. +This could be used, for example, if you'd like a catch-all route which is useful when the user enters an incorrect URL not managed by your app. +Wildcard routes begin with an asterisk. + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('not-found', { path: '/*path' }); +}); +``` + +```handlebars {data-filename=app/templates/not-found.hbs} +

    Oops, the page you're looking for wasn't found

    +``` + +In the above example we have successfully used a wildcard route to handle all routes not managed by our application +so that when a user navigates to `/a/non-existent/path` they will be shown a message that says the page they're looking for wasn't found. + +Note that if you want to manually transition to this wildcard route, you need to pass an arbitrary (not empty) argument. For example: + +```javascript {data-filename=app/routes/some-route.js} +this.transitionTo('not-found', 404); +``` + +## Route Handlers + +To have your route do something beyond render a template with the same name, you'll +need to create a route handler. The following guides will explore the different +features of route handlers. For more information on routes, see the API documentation +for [the router](https://www.emberjs.com/api/ember/release/classes/EmberRouter) and for [route +handlers](https://www.emberjs.com/api/ember/release/classes/Route). + +## Transitioning Between Routes +Once the routes are defined, how do we go about transitioning between them within our application? It depends on where the transition needs to take place: + +- From a template, use [`{{link-to}}`](https://emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/link-to?anchor=link-to) as mentioned above +- From a route, use the [`transitionTo()`](https://emberjs.com/api/ember/release/classes/Route/methods/transitionTo?anchor=transitionTo) method +- From a controller, use the [`transitionToRoute()`](https://emberjs.com/api/ember/release/classes/Controller/methods/transitionToRoute?anchor=transitionToRoute) method +- From anywhere else in your application, such as a component, inject the [Router Service](https://emberjs.com/api/ember/release/classes/RouterService) and use the [`transitionTo()`](https://emberjs.com/api/ember/release/classes/RouterService/methods/transitionTo?anchor=transitionTo) method diff --git a/guides/v3.6.0/routing/index.md b/guides/v3.6.0/routing/index.md new file mode 100644 index 0000000000..7af20abb64 --- /dev/null +++ b/guides/v3.6.0/routing/index.md @@ -0,0 +1,21 @@ +Imagine we are writing a web app for managing a blog. At any given time, we +should be able to answer questions like _What post are they looking at?_ and +_Are they editing it?_ In Ember.js, the answer to these questions is determined +by the URL. + +The URL can be set in a few ways: + +* The user loads the app for the first time. +* The user changes the URL manually, such as by clicking the back button or by +editing the address bar. +* The user clicks a link within the app. +* Some other event in the app causes the URL to change. + +Regardless of how the URL becomes set, the Ember router then maps the current +URL to one or more route handlers. A route handler can do several things: + +* It can render a template. +* It can load a model that is then available to the template. +* It can redirect to a new route, such as if the user isn't allowed to visit +that part of the app. +* It can handle actions that involve changing a model or transitioning to a new route. diff --git a/guides/v3.6.0/routing/loading-and-error-substates.md b/guides/v3.6.0/routing/loading-and-error-substates.md new file mode 100644 index 0000000000..041cbe22aa --- /dev/null +++ b/guides/v3.6.0/routing/loading-and-error-substates.md @@ -0,0 +1,252 @@ +The Ember Router allows you to provide feedback that a route is loading, as well +as when an error occurs in loading a route. + +The `error` and `loading` substates exist as a part of each route, so they +should not be added to your `router.js` file. To utilize a substate, the route, controller, +and template may be optionally defined as desired. + +## `loading` substates + +During the `beforeModel`, `model`, and `afterModel` hooks, data may take some +time to load. Technically, the router pauses the transition until the promises +returned from each hook fulfill. + +Consider the following: + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('slow-model'); +}); +``` + +```javascript {data-filename=app/routes/slow-model.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model() { + return this.store.findAll('slow-model'); + } +}); +``` + +If you navigate to `slow-model`, in the `model` hook, +the query may take a long time to complete. +During this time, your UI isn't really giving you any feedback as to +what's happening. If you're entering this route after a full page +refresh, your UI will be entirely blank, as you have not actually +finished fully entering any route and haven't yet displayed any +templates. If you're navigating to `slow-model` from another +route, you'll continue to see the templates from the previous route +until the model finish loading, and then, boom, suddenly all the +templates for `slow-model` load. + +So, how can we provide some visual feedback during the transition? + +Simply define a template called `loading` (and optionally a corresponding route) +that Ember will transition to. The +intermediate transition into the loading substate happens immediately +(synchronously), the URL won't be updated, and, unlike other transitions, the +currently active transition won't be aborted. + +Once the main transition into `slow-model` completes, the `loading` +route will be exited and the transition to `slow-model` will continue. + +For nested routes, like: + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('foo', function() { + this.route('bar', function() { + this.route('slow-model'); + }); + }); +}); +``` + +When accessing `foo.bar.slow-model` route then Ember will alternate trying to +find a `routeName-loading` or `loading` template in the hierarchy starting with +`foo.bar.slow-model-loading`: + +1. `foo.bar.slow-model-loading` +2. `foo.bar.loading` or `foo.bar-loading` +3. `foo.loading` or `foo-loading` +4. `loading` or `application-loading` + +It's important to note that for `slow-model` itself, Ember will not try to +find a `slow-model.loading` template but for the rest of the hierarchy either +syntax is acceptable. This can be useful for creating a custom loading screen +for a leaf route like `slow-model`. + +When accessing `foo.bar` route then Ember will search for: + +1. `foo.bar-loading` +2. `foo.loading` or `foo-loading` +3. `loading` or `application-loading` + +It's important to note that `foo.bar.loading` is not considered now. + +### The `loading` event + +If the various `beforeModel`/`model`/`afterModel` hooks +don't immediately resolve, a [`loading`](https://www.emberjs.com/api/ember/release/classes/Route/events/loading?anchor=loading) event will be fired on that route. + +```javascript {data-filename=app/routes/foo-slow-model.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model() { + return this.store.findAll('slow-model'); + }, + + actions: { + loading(transition, originRoute) { + let controller = this.controllerFor('foo'); + controller.set('currentlyLoading', true); + + return true; // allows the loading template to be shown + } + } +}); +``` + +If the `loading` handler is not defined at the specific route, +the event will continue to bubble above a transition's parent +route, providing the `application` route the opportunity to manage it. + +When using the `loading` handler, we can make use of the transition promise to know when the loading event is over: + +```javascript {data-filename=app/routes/foo-slow-model.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + … + + actions: { + loading(transition, originRoute) { + let controller = this.controllerFor('foo'); + controller.set('currentlyLoading', true); + transition.promise.finally(function() { + controller.set('currentlyLoading', false); + }); + } + } +}); +``` + +In case we want both custom logic and the default behaviour for the loading substate, +we can implement the `loading` action and let it bubble by returning `true`. + +```javascript {data-filename=app/routes/foo-slow-model.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + ... + actions: { + loading(transition) { + let start = new Date(); + transition.promise.finally(() => { + this.notifier.notify(`Took ${new Date() - start}ms to load`); + }); + + return true; + } + } +}); +``` + +## `error` substates + +Ember provides an analogous approach to `loading` substates in +the case of errors encountered during a transition. + +Similar to how the default `loading` event handlers are implemented, +the default `error` handlers will look for an appropriate error substate to +enter, if one can be found. + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('articles', function() { + this.route('overview'); + }); +}); +``` + +As with the `loading` substate, on a thrown error or rejected promise returned +from the `articles.overview` route's `model` hook (or `beforeModel` or +`afterModel`) Ember will look for an error template or route in the following +order: + +1. `articles.overview-error` +2. `articles.error` or `articles-error` +3. `error` or `application-error` + +If one of the above is found, the router will immediately transition into +that substate (without updating the URL). The "reason" for the error +(i.e. the exception thrown or the promise reject value) will be passed +to that error state as its `model`. + +The model hooks (`beforeModel`, `model`, and `afterModel`) of an error substate +are not called. Only the `setupController` method of the error substate is +called with the `error` as the model. See example below: + +```javascript +setupController(controller, error) { + console.log(error.message); + this._super(...arguments); +} +``` + +If no viable error substates can be found, an error message will be +logged. + +### The `error` event + +If the `articles.overview` route's `model` hook returns a promise that rejects +(for instance the server returned an error, the user isn't logged in, +etc.), an [`error`](https://www.emberjs.com/api/ember/release/classes/Route/events/error?anchor=error) event will fire from that route and bubble upward. +This `error` event can be handled and used to display an error message, +redirect to a login page, etc. + +```javascript {data-filename=app/routes/articles-overview.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model(params) { + return this.store.findAll('privileged-model'); + }, + + actions: { + error(error, transition) { + if (error.status === '403') { + this.replaceWith('login'); + } else { + // Let the route above this handle the error. + return true; + } + } + } +}); +``` + +Analogous to the `loading` event, you could manage the `error` event +at the application level to avoid writing the same code for multiple routes. + +In case we want to run some custom logic and have the default behaviour of rendering the error template, +we can handle the `error` event and let it bubble by returning `true`. + +```javascript {data-filename=app/routes/articles-overview.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model(params) { + return this.get('store').findAll('privileged-model'); + }, + actions: { + error(error) { + this.notifier.error(error); + + return true; + } + } +}); +``` diff --git a/guides/v3.6.0/routing/preventing-and-retrying-transitions.md b/guides/v3.6.0/routing/preventing-and-retrying-transitions.md new file mode 100644 index 0000000000..6218d5dc32 --- /dev/null +++ b/guides/v3.6.0/routing/preventing-and-retrying-transitions.md @@ -0,0 +1,108 @@ +During a route transition, the Ember Router passes a transition +object to the various hooks on the routes involved in the transition. +Any hook that has access to this transition object has the ability +to immediately abort the transition by calling `transition.abort()`, +and if the transition object is stored, it can be re-attempted at a +later time by calling `transition.retry()`. + +### Preventing Transitions via `willTransition` + +When a transition is attempted, whether via `{{link-to}}`, `transitionTo`, +or a URL change, a `willTransition` action is fired on the currently +active routes. This gives each active route, starting with the leaf-most +route, the opportunity to decide whether or not the transition should occur. + +Imagine your app is in a route that's displaying a complex form for the user +to fill out and the user accidentally navigates backwards. Unless the +transition is prevented, the user might lose all of the progress they +made on the form, which can make for a pretty frustrating user experience. + +Here's one way this situation could be handled: + +```javascript {data-filename=app/routes/form.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + actions: { + willTransition(transition) { + if (this.controller.get('userHasEnteredData') && + !confirm('Are you sure you want to abandon progress?')) { + transition.abort(); + } else { + // Bubble the `willTransition` action so that + // parent routes can decide whether or not to abort. + return true; + } + } + } +}); +``` + +When the user clicks on a `{{link-to}}` helper, or when the app initiates a +transition by using `transitionTo`, the transition will be aborted and the URL +will remain unchanged. However, if the browser back button is used to +navigate away from `route:form`, or if the user manually changes the URL, the +new URL will be navigated to before the `willTransition` action is +called. This will result in the browser displaying the new URL, even if +`willTransition` calls `transition.abort()`. + +### Aborting Transitions Within `model`, `beforeModel`, `afterModel` + +The `model`, `beforeModel`, and `afterModel` hooks described in +[Asynchronous Routing](../asynchronous-routing/) +each get called with a transition object. This makes it possible for +destination routes to abort attempted transitions. + +```javascript {data-filename=app/routes/disco.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + beforeModel(transition) { + if (new Date() > new Date('January 1, 1980')) { + alert('Sorry, you need a time machine to enter this route.'); + transition.abort(); + } + } +}); +``` + +### Storing and Retrying a Transition + +Aborted transitions can be retried at a later time. A common use case +for this is having an authenticated route redirect the user to a login +page, and then redirecting them back to the authenticated route once +they've logged in. + +```javascript {data-filename=app/routes/some-authenticated.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + beforeModel(transition) { + if (!this.controllerFor('auth').get('userIsLoggedIn')) { + let loginController = this.controllerFor('login'); + loginController.set('previousTransition', transition); + this.transitionTo('login'); + } + } +}); +``` + +```javascript {data-filename=app/controllers/login.js} +import Controller from '@ember/controller'; + +export default Controller.extend({ + actions: { + login() { + // Log the user in, then reattempt previous transition if it exists. + let previousTransition = this.previousTransition; + if (previousTransition) { + this.set('previousTransition', null); + previousTransition.retry(); + } else { + // Default back to homepage + this.transitionToRoute('index'); + } + } + } +}); +``` diff --git a/guides/v3.6.0/routing/query-params.md b/guides/v3.6.0/routing/query-params.md new file mode 100644 index 0000000000..4bb7f140aa --- /dev/null +++ b/guides/v3.6.0/routing/query-params.md @@ -0,0 +1,345 @@ +Query parameters are optional key-value pairs that appear to the right of +the `?` in a URL. For example, the following URL has two query params, +`sort` and `page`, with respective values `ASC` and `2`: + +```text +http://example.com/articles?sort=ASC&page=2 +``` + +Query params allow for additional application state to be serialized +into the URL that can't otherwise fit into the _path_ of the URL (i.e. +everything to the left of the `?`). Common use cases for query params include +representing the current page number in a paginated collection, filter criteria, or sorting criteria. + +In web development, query parameters are used within a URL as described above but can also be used +in API requests that retrieve data. Ember treats these as _two_ different concepts. This section +describes how routing query parameters are used in Ember. See [finding records](../../models/finding-records/#toc_querying-for-multiple-records) to see how query parameters are +applied to API requests in Ember Data. + +### Specifying Query Parameters + +Query params are declared on route-driven controllers. For example, to +configure query params that are active within the `articles` route, +they must be declared on `controller:articles`. + +To add a `category` +query parameter that will filter out all the articles that haven't +been categorized as popular we'd specify `'category'` +as one of `controller:articles`'s `queryParams`: + +```javascript {data-filename=app/controllers/articles.js} +import Controller from '@ember/controller'; + +export default Controller.extend({ + queryParams: ['category'], + category: null +}); +``` + +This sets up a binding between the `category` query param in the URL, +and the `category` property on `controller:articles`. In other words, +once the `articles` route has been entered, any changes to the +`category` query param in the URL will update the `category` property +on `controller:articles`, and vice versa. +Note that you can't bind `queryParams` to computed properties, they +have to be values. + +Now we need to define a computed property of our category-filtered +array that the `articles` template will render: + +```javascript {data-filename=app/controllers/articles.js} +import Controller from '@ember/controller'; +import { computed } from '@ember/object'; + +export default Controller.extend({ + queryParams: ['category'], + category: null, + + filteredArticles: computed('category', 'model', function() { + let category = this.category; + let articles = this.model; + + if (category) { + return articles.filterBy('category', category); + } else { + return articles; + } + }) +}); +``` + +With this code, we have established the following behaviors: + +1. If the user navigates to `/articles`, `category` will be `null`, so + the articles won't be filtered. +2. If the user navigates to `/articles?category=recent`, + `category` will be set to `"recent"`, so articles will be filtered. +3. Once inside the `articles` route, any changes to the `category` + property on `controller:articles` will cause the URL to update the + query param. By default, a query param property change won't cause a + full router transition (i.e. it won't call `model` hooks and + `setupController`, etc.); it will only update the URL. + +### link-to Helper + +The `link-to` helper supports specifying query params using the +`query-params` subexpression helper. + +```handlebars +// Explicitly set target query params +{{#link-to "posts" (query-params direction="asc")}}Sort{{/link-to}} + +// Binding is also supported +{{#link-to "posts" (query-params direction=otherDirection)}}Sort{{/link-to}} +``` + +In the above examples, `direction` is presumably a query param property +on the `posts` controller, but it could also refer to a `direction` property +on any of the controllers associated with the `posts` route hierarchy, +matching the leaf-most controller with the supplied property name. + +The `link-to` helper takes into account query parameters when determining +its "active" state, and will set the class appropriately. The active state +is determined by calculating whether the query params end up the same after +clicking a link. You don't have to supply all of the current, +active query params for this to be true. + +### transitionTo + +`Route#transitionTo` and `Controller#transitionToRoute` +accept a final argument, which is an object with the key `queryParams`. + +```javascript {data-filename=app/routes/some-route.js} +this.transitionTo('post', object, { queryParams: { showDetails: true }}); +this.transitionTo('posts', { queryParams: { sort: 'title' }}); + +// if you want to transition the query parameters without changing the route +this.transitionTo({ queryParams: { direction: 'asc' }}); +``` + +You can also add query params to URL transitions: + +```javascript {data-filename=app/routes/some-route.js} +this.transitionTo('/posts/1?sort=date&showDetails=true'); +``` + +### Opting into a full transition + +When you change query params through a transition (`transitionTo` and `link-to`), +it is not considered a full transition. +This means that the controller properties associated with the query params will be updated, +as will the URL, but no `Route` method hook like `model` or `setupController` will be called. + +If you need a query param change to trigger a full transition, and thus the method hooks, +you can use the optional `queryParams` configuration hash on the `Route`. +If you have a `category` query param and you want it to trigger a model refresh, +you can set it as follows: + +```javascript {data-filename=app/routes/articles.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + queryParams: { + category: { + refreshModel: true + } + }, + + model(params) { + // This gets called upon entering 'articles' route + // for the first time, and we opt into refiring it upon + // query param changes by setting `refreshModel:true` above. + + // params has format of { category: "someValueOrJustNull" }, + // which we can forward to the server. + return this.store.query('article', params); + } +}); +``` + +```javascript {data-filename=app/controllers/articles.js} +import Controller from '@ember/controller'; + +export default Controller.extend({ + queryParams: ['category'], + category: null +}); +``` + +### Update URL with `replaceState` instead + +By default, Ember will use `pushState` to update the URL in the +address bar in response to a controller query param property change. +If you would like to use `replaceState` instead, which prevents an +additional item from being added to your browser's history, +you can specify this as follows: + +```javascript {data-filename=app/routes/articles.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + queryParams: { + category: { + replace: true + } + } +}); +``` + +This behaviour is similar to `link-to`, +which also lets you opt into a `replaceState` transition via `replace=true`. + +### Map a controller's property to a different query param key + +By default, specifying `foo` as a controller query param property will +bind to a query param whose key is `foo`, e.g. `?foo=123`. +You can also map a controller property to a different query param key using the following configuration syntax: + +```javascript {data-filename=app/controllers/articles.js} +import Controller from '@ember/controller'; + +export default Controller.extend({ + queryParams: { + category: 'articles_category' + }, + + category: null +}); +``` + +This will cause changes to the `controller:articles`'s `category` +property to update the `articles_category` query param, and vice versa. + +Query params that require additional customization can +be provided along with strings in the `queryParams` array. + +```javascript {data-filename=app/controllers/articles.js} +import Controller from '@ember/controller'; + +export default Controller.extend({ + queryParams: ['page', 'filter', { + category: 'articles_category' + }], + + category: null, + page: 1, + filter: 'recent' +}); +``` + +### Default values and deserialization + +In the following example, +the controller query param property `page` is considered to have a default value of `1`. + +```javascript {data-filename=app/controllers/articles.js} +import Controller from '@ember/controller'; + +export default Controller.extend({ + queryParams: 'page', + page: 1 +}); +``` + +This affects query param behavior in two ways: + +1. Query param values are cast to the same datatype as the default + value, e.g. a URL change from `/?page=3` to `/?page=2` will set + `controller:articles`'s `page` property to the number `2`, rather than + the string `"2"`. The same also applies to boolean default values. If the + default value is an array, the string will be parsed using `JSON.parse`. +2. When a controller's query param property is currently set to its + default value, this value won't be serialized into the URL. So in the + above example, if `page` is `1`, the URL might look like `/articles`, + but once someone sets the controller's `page` value to `2`, the URL + will become `/articles?page=2`. + +### Sticky Query Param Values + +By default, query param values in Ember are "sticky", +in that if you make changes to a query param and then leave and re-enter the route, +the new value of that query param will be preserved (rather than reset to its default). +This is a particularly handy default for preserving sort/filter parameters as you navigate back and forth between routes. + +Furthermore, these sticky query param values are remembered/restored according to the model loaded into the route. +So, given a `team` route with dynamic segment `/:team_name` and controller query param "filter", +if you navigate to `/badgers` and filter by `"rookies"`, +then navigate to `/bears` and filter by `"best"`, +and then navigate to `/potatoes` and filter by `"worst"`, +then given the following nav bar links: + +```handlebars +{{#link-to "team" "badgers"}}Badgers{{/link-to}} +{{#link-to "team" "bears"}}Bears{{/link-to}} +{{#link-to "team" "potatoes"}}Potatoes{{/link-to}} +``` + +the generated links would be: + +```html +Badgers +Bears +Potatoes +``` + +This illustrates that once you change a query param, +it is stored and tied to the model loaded into the route. + +If you wish to reset a query param, you have two options: + +1. explicitly pass in the default value for that query param into + `link-to` or `transitionTo`. +2. use the `Route.resetController` hook to set query param values back to + their defaults before exiting the route or changing the route's model. + +In the following example, the controller's `page` query param is reset to 1, +_while still scoped to the pre-transition `ArticlesRoute` model_. +The result of this is that all links pointing back into the exited route will use the newly reset value `1` as the value for the `page` query param. + +```javascript {data-filename=app/routes/articles.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + resetController(controller, isExiting, transition) { + if (isExiting) { + // isExiting would be false if only the route's model was changing + controller.set('page', 1); + } + } +}); +``` + +In some cases, you might not want the sticky query param value to be +scoped to the route's model but would rather reuse a query param's value +even as a route's model changes. This can be accomplished by setting the +`scope` option to `"controller"` within the controller's `queryParams` +config hash: + +```javascript {data-filename=app/controllers/articles.js} +import Controller from '@ember/controller'; + +export default Controller.extend({ + queryParams: [{ + showMagnifyingGlass: { + scope: 'controller' + } + }] +}); +``` + +The following demonstrates how you can override both the scope and the query param URL key of a single controller query param property: + +```javascript {data-filename=app/controllers/articles.js} +import Controller from '@ember/controller'; + +export default Controller.extend({ + queryParams: ['page', 'filter', + { + showMagnifyingGlass: { + scope: 'controller', + as: 'glass' + } + } + ] +}); +``` diff --git a/guides/v3.6.0/routing/redirection.md b/guides/v3.6.0/routing/redirection.md new file mode 100644 index 0000000000..51985f2bae --- /dev/null +++ b/guides/v3.6.0/routing/redirection.md @@ -0,0 +1,114 @@ +Sometimes you want to redirect a user to a different page than what they requested for. + +For example, if they're not logged in, you might want to prevent them from editing their profile, accessing private information, +or checking out items in their shopping cart. +Usually you want to redirect them to the login page, and after they have successfully logged in, take them back to the page they originally wanted to access. + +There are many other reasons you probably want to have the last word on whether a user can or cannot access a certain page. +Ember allows you to control that access with a combination of hooks and methods in your route. + +One of the methods is [`transitionTo()`](https://www.emberjs.com/api/ember/release/classes/Route/methods/transitionTo?anchor=transitionTo). +Calling `transitionTo()` from a route or +[`transitionToRoute()`](https://www.emberjs.com/api/ember/release/classes/Controller/methods/transitionToRoute?anchor=transitionToRoute) from a controller will stop any transitions currently in progress and start a new one, functioning as a redirect. +`transitionTo()` behaves exactly like the [link-to](../../templates/links/) helper. + +The other one is [`replaceWith()`](https://www.emberjs.com/api/ember/release/classes/Route/methods/transitionTo?anchor=replaceWith/) which works the same way as `transitionTo()`. +The only difference between them is how they manage history. +`replaceWith()` substitutes the current route entry and replaces it with that of the route we are redirecting to, +while `transitionTo()` leaves the entry for the current route and creates a new one for the redirection. + +If the new route has dynamic segments, you need to pass either a _model_ or an _identifier_ for each segment. +Passing a model will skip the route's `model()` hook since the model is already loaded. + +## Transitioning Before the Model is Known + +Since a route's [`beforeModel()`](https://www.emberjs.com/api/ember/release/classes/Route/methods/transitionTo?anchor=beforeModel) executes before the `model()` hook, +it's a good place to do a redirect if you don't need any information that is contained in the model. + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('posts'); +}); +``` + +```javascript {data-filename=app/routes/index.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + beforeModel(/* transition */) { + this.transitionTo('posts'); // Implicitly aborts the on-going transition. + } +}); +``` + +`beforeModel()` receives the current transition as an argument, which we can store and retry later. +This allows us to return the user back to the original route. +For example, we might redirect a user to the login page when they try to edit their profile, and immediately redirect +them back to the edit page once they have successfully logged in. +See [Storing and Retrying a Transition](../preventing-and-retrying-transitions/#toc_storing-and-retrying-a-transition) +for how to do that. + +If you need to examine some application state to figure out where to redirect, +you might use a [service](../../applications/services/). + +## Transitioning After the Model is Known + +If you need information about the current model in order to decide about redirection, you can use the [`afterModel()`](https://www.emberjs.com/api/ember/release/classes/Route/methods/transitionTo?anchor=afterModel) hook. +It receives the resolved model as the first parameter and the transition as the second one. +For example: + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('posts'); + this.route('post', { path: '/post/:post_id' }); +}); +``` + +```javascript {data-filename=app/routes/posts.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + afterModel(model, transition) { + if (model.get('length') === 1) { + this.transitionTo('post', model.get('firstObject')); + } + } +}); +``` + +When transitioning to the `posts` route if it turns out that there is only one post, +the current transition will be aborted in favor of redirecting to the `PostRoute` +with the single post object being its model. + +### Child Routes + +Let's change the router above to use a nested route, like this: + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('posts', function() { + this.route('post', { path: '/:post_id' }); + }); +}); +``` + +If we redirect to `posts.post` in the `afterModel` hook, `afterModel` +essentially invalidates the current attempt to enter this route. So the `posts` +route's `beforeModel`, `model`, and `afterModel` hooks will fire again within +the new, redirected transition. This is inefficient, since they just fired +before the redirect. + +Instead, we can use the [`redirect()`](https://www.emberjs.com/api/ember/release/classes/Route/methods/transitionTo?anchor=redirect) method, which will leave the original +transition validated, and not cause the parent route's hooks to fire again: + +```javascript {data-filename=app/routes/posts.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + redirect(model, transition) { + if (model.get('length') === 1) { + this.transitionTo('posts.post', model.get('firstObject')); + } + } +}); +``` diff --git a/guides/v3.6.0/routing/rendering-a-template.md b/guides/v3.6.0/routing/rendering-a-template.md new file mode 100644 index 0000000000..23bde1a92c --- /dev/null +++ b/guides/v3.6.0/routing/rendering-a-template.md @@ -0,0 +1,34 @@ +One job of a route handler is rendering the appropriate template to the screen. + +By default, a route handler will render the template with the same name as the +route. Take this router: + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('posts', function() { + this.route('new'); + }); +}); +``` + +Here, the `posts` route will render the `posts.hbs` template, and +the `posts.new` route will render `posts/new.hbs`. + +Each template will be rendered into the `{{outlet}}` of its parent route's +template. For example, the `posts.new` route will render its template into the +`posts.hbs`'s `{{outlet}}`, and the `posts` route will render its template into +the `application.hbs`'s `{{outlet}}`. + +If you want to render a template other than the default one, set the route's [`templateName`](https://www.emberjs.com/api/ember/release/classes/Route/properties/templateName?anchor=templateName) property to the name of +the template you want to render instead. + +```javascript {data-filename=app/routes/posts.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + templateName: 'posts/favorite-posts' +}); +``` + +You can override the [`renderTemplate()`](https://www.emberjs.com/api/ember/release/classes/Route/methods/renderTemplate?anchor=renderTemplate) hook if you want finer control over template rendering. +Among other things, it allows you to choose the controller used to configure the template and specific outlet to render it into. diff --git a/guides/v3.6.0/routing/specifying-a-routes-model.md b/guides/v3.6.0/routing/specifying-a-routes-model.md new file mode 100644 index 0000000000..fa78c24079 --- /dev/null +++ b/guides/v3.6.0/routing/specifying-a-routes-model.md @@ -0,0 +1,226 @@ +Often, you'll want a template to display data from a model. Loading the +appropriate model is one job of a route. + +For example, take this router: + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('favorite-posts'); +}); +``` + +To load a model for the `favorite-posts` route, you would use the [`model()`](https://www.emberjs.com/api/ember/release/classes/Route/methods/model?anchor=model) +hook in the `favorite-posts` route handler: + +```javascript {data-filename=app/routes/favorite-posts.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model() { + return this.store.query('post', { favorite: true }); + } +}); +``` + +Typically, the `model` [hook](../../getting-started/core-concepts/#toc_hooks) should return an [Ember Data](../../models/) record, +but it can also return any [promise](https://www.promisejs.org/) object (Ember Data records are promises/), +or a plain JavaScript object or array. +Ember will wait until the data finishes loading (until the promise is resolved/) before rendering the template. + +The route will then set the return value from the `model` hook as the `model` property of the controller. +You will then be able to access the controller's `model` property in your template: + +```handlebars {data-filename=app/templates/favorite-posts.hbs} +

    Favorite Posts

    +{{#each model as |post|}} +

    {{post.body}}

    +{{/each}} +``` + +## Dynamic Models + +Some routes always display the same model. For example, the `/photos` +route will always display the same list of photos available in the +application. If your user leaves this route and comes back later, the +model does not change. + +However, you will often have a route whose model will change depending +on user interaction. For example, imagine a photo viewer app. The +`/photos` route will render the `photos` template with the list of +photos as the model, which never changes. But when the user clicks on a +particular photo, we want to display that model with the `photo` +template. If the user goes back and clicks on a different photo, we want +to display the `photo` template again, this time with a different model. + +In cases like this, it's important that we include some information in +the URL about not only which template to display, but also which model. + +In Ember, this is accomplished by defining routes with [dynamic +segments](../defining-your-routes/#toc_dynamic-segments). + +Once you have defined a route with a dynamic segment, +Ember will extract the value of the dynamic segment from the URL for +you and pass them as a hash to the `model` hook as the first argument: + +```javascript {data-filename=app/router.js} +Router.map(function() { + this.route('photo', { path: '/photos/:photo_id' }); +}); +``` + +```javascript {data-filename=app/routes/photo.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model(params) { + return this.store.findRecord('photo', params.photo_id); + } +}); +``` + +In the `model` hook for routes with dynamic segments, it's your job to +turn the ID (something like `47` or `post-slug`) into a model that can +be rendered by the route's template. In the above example, we use the +photo's ID (`params.photo_id`) as an argument to Ember Data's `findRecord` +method. + +Note: A route with a dynamic segment will always have its `model` hook called when it is entered via the URL. +If the route is entered through a transition (e.g. when using the [link-to](../../templates/links/) Handlebars helper/), +and a model context is provided (second argument to `link-to`), then the hook is not executed. +If an identifier (such as an id or slug/) is provided instead then the model hook will be executed. + +For example, transitioning to the `photo` route this way won't cause the `model` hook to be executed (because `link-to` +was passed a model/): + +```handlebars {data-filename=app/templates/photos.hbs} +

    Photos

    +{{#each model as |photo|}} +

    + {{#link-to "photo" photo}} + {{photo.title}} + {{/link-to}} +

    +{{/each}} +``` + +while transitioning this way will cause the `model` hook to be executed (because `link-to` was passed `photo.id`, an +identifier, instead): + +```handlebars {data-filename=app/templates/photos.hbs} +

    Photos

    +{{#each model as |photo|}} +

    + {{#link-to "photo" photo.id}} + {{photo.title}} + {{/link-to}} +

    +{{/each}} +``` + +Routes without dynamic segments will always execute the model hook. + +## Multiple Models + +Multiple models can be returned through an +[RSVP.hash](https://www.emberjs.com/api/ember/release/classes/rsvp/methods/hash?anchor=hash). +The `RSVP.hash` method takes an object with promises or values as properties as an argument, and returns a single promise. +When all of the promises in the object resolve, the returned promise will resolve with an object of all of the promise values. For example: + +```javascript {data-filename=app/routes/songs.js} +import Route from '@ember/routing/route'; +import RSVP from 'rsvp'; + +export default Route.extend({ + model() { + return RSVP.hash({ + songs: this.store.findAll('song'), + albums: this.store.findAll('album') + }); + } +}); +``` + +In the `songs` template, we can specify both models and use the `{{#each}}` helper to display +each record in the song model and album model: + +```handlebars {data-filename=app/templates/songs.hbs} +

    Playlist

    + +
      + {{#each model.songs as |song|}} +
    • {{song.name}} by {{song.artist}}
    • + {{/each}} +
    + +

    Albums

    + +
      + {{#each model.albums as |album|}} +
    • {{album.title}} by {{album.artist}}
    • + {{/each}} +
    +``` + +If you use [Ember Data](../../models/) and you are building an `RSVP.hash` with the model's relationship, consider instead properly setting up your [relationships](../../models/relationships/) and letting Ember Data take care of loading them. + +## Reusing Route Context + +Sometimes you need to fetch a model, but your route doesn't have the parameters, because it's +a child route and the route directly above or a few levels above has the parameters that your route +needs. + +In this scenario, you can use the `paramsFor` method to get the parameters of a parent route. + +```javascript {data-filename=app/routes/album/index.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model() { + let { album_id } = this.paramsFor('album'); + + return this.store.query('song', { album: album_id }); + } +}); +``` + +This is guaranteed to work because the parent route is loaded. But if you tried to +do `paramsFor` on a sibling route, you wouldn't have the results you expected. + +This is a great way to use the parent context to load something that you want. +Using `paramsFor` will also give you the query params defined on that route's controller. +This method could also be used to look up the current route's parameters from an action +or another method on the route, and in that case we have a shortcut: `this.paramsFor(this.routeName)`. + +In our case, the parent route had already loaded its songs, so we would be writing unnecessary fetching logic. +Let's rewrite the same route, but use `modelFor`, which works the same way, but returns the model +from the parent route. + +```javascript {data-filename=app/routes/album/index.js} +import Route from '@ember/routing/route'; + +export default Route.extend({ + model() { + let { songs } = this.modelFor('album'); + + return songs; + } +}); +``` + +In the case above, the parent route looked something like this: + +```javascript {data-filename=app/routes/album.js} +import Route from '@ember/routing/route'; +import RSVP from 'rsvp'; + +export default Route.extend({ + model({ album_id }) { + return RSVP.hash({ + album: this.store.findRecord('album', album_id), + songs: this.store.query('songs', { album: album_id }) + }); + } +}); +``` + +And calling `modelFor` returned the result of the `model` hook. diff --git a/guides/v3.6.0/templates/actions.md b/guides/v3.6.0/templates/actions.md new file mode 100644 index 0000000000..e02c3e3740 --- /dev/null +++ b/guides/v3.6.0/templates/actions.md @@ -0,0 +1,174 @@ +Your app will often need a way to let users interact with controls that +change application state. For example, imagine that you have a template +that shows a blog title, and supports expanding the post to show the body. + +If you add the +[`{{action}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/action?anchor=action) +helper to any HTML DOM element, when a user clicks the element, the named event +will be sent to the template's corresponding component or controller. + +```handlebars {data-filename=app/templates/components/single-post.hbs} +

    +{{#if isShowingBody}} +

    {{{body}}}

    +{{/if}} +``` + +In the component or controller, you can then define what the action does within +the `actions` hook: + +```javascript {data-filename=app/components/single-post.js} +import Component from '@ember/component'; + +export default Component.extend({ + actions: { + toggleBody() { + this.toggleProperty('isShowingBody'); + } + } +}); +``` + +You will learn about more advanced usages in the Component's [Triggering Changes With Actions](../../components/triggering-changes-with-actions/) guide, +but you should familiarize yourself with the following basics first. + +## Action Parameters + +You can optionally pass arguments to the action handler. Any values +passed to the `{{action}}` helper after the action name will be passed to +the handler as arguments. + +For example, if the `post` argument was passed: + +```handlebars +

    {{post.title}}

    +``` + +The `select` action handler would be called with a single argument +containing the post model: + +```javascript {data-filename=app/components/single-post.js} +import Component from '@ember/component'; + +export default Component.extend({ + actions: { + select(post) { + console.log(post.get('title')); + } + } +}); +``` + +## Specifying the Type of Event + +By default, the +[`{{action}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/action?anchor=action) +helper listens for click events and triggers the action when the user clicks +on the element. + +You can specify an alternative event by using the `on` option. + +```handlebars +

    + + {{post.title}} +

    +``` + +You should use the camelCased event names, so two-word names like `keypress` +become `keyPress`. + +## Allowing Modifier Keys + +By default, the `{{action}}` helper will ignore click events with +pressed modifier keys. You can supply an `allowedKeys` option +to specify which keys should not be ignored. + +```handlebars + +``` + +This way the `{{action}}` will fire when clicking with the alt key +pressed down. + +## Allowing Default Browser Action + +By default, the `{{action}}` helper prevents the default browser action of the +DOM event. If you want to allow the browser action, you can stop Ember from +preventing it. + +For example, if you have a normal link tag and want the link to bring the user +to another page in addition to triggering an ember action when clicked, you can +use `preventDefault=false`: + +```handlebars +Go +``` + +With `preventDefault=false` omitted, if the user clicked on the link, Ember.js +will trigger the action, but the user will remain on the current page. + +With `preventDefault=false` present, if the user clicked on the link, Ember.js +will trigger the action *and* the user will be directed to the new page. + +## Modifying the action's first parameter + +If a `value` option for the +[`{{action}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/action?anchor=action) +helper is specified, its value will be considered a property path that will +be read off of the first parameter of the action. This comes very handy with +event listeners and enables to work with one-way bindings. + +```handlebars + + +``` + +Let's assume we have an action handler that prints its first parameter: + +```javascript +actions: { + bandDidChange(newValue) { + console.log(newValue); + } +} +``` + +By default, the action handler receives the first parameter of the event +listener, the event object the browser passes to the handler, so +`bandDidChange` prints `Event {}`. + +Using the `value` option modifies that behavior by extracting that property from +the event object: + +```handlebars + + +``` + +The `newValue` parameter thus becomes the `target.value` property of the event +object, which is the value of the input field the user typed. (e.g 'Foo Fighters') + +## Attaching Actions to Non-Clickable Elements + +Note that actions may be attached to any element of the DOM, but not all +respond to the `click` event. For example, if an action is attached to an `a` +link without an `href` attribute, or to a `div`, some browsers won't execute +the associated function. If it's really needed to define actions over such +elements, a CSS workaround exists to make them clickable, `cursor: pointer`. +For example: + +```css +[data-ember-action]:not(:disabled) { + cursor: pointer; +} +``` + +Keep in mind that even with this workaround in place, the `click` event will +not automatically trigger via keyboard driven `click` equivalents (such as +the `enter` key when focused). Browsers will trigger this on clickable +elements only by default. This also doesn't make an element accessible to +users of assistive technology. You will need to add additional things like +`role` and/or `tabindex` to make this accessible for your users. diff --git a/guides/v3.6.0/templates/binding-element-attributes.md b/guides/v3.6.0/templates/binding-element-attributes.md new file mode 100644 index 0000000000..078fcd81fd --- /dev/null +++ b/guides/v3.6.0/templates/binding-element-attributes.md @@ -0,0 +1,76 @@ +In addition to normal text, you may also want to have your templates +contain HTML elements whose attributes are bound to the controller. + +For example, imagine your controller has a property that contains a URL +to an image: + +```handlebars + +``` + +This generates the following HTML: + +```html + +``` + +If you use data binding with a Boolean value, it will add or remove +the specified attribute. For example, given this template: + +```handlebars + +``` + +If `isAdministrator` is `true`, Handlebars will produce the following +HTML element: + +```html + +``` + +If `isAdministrator` is `false`, Handlebars will produce the following: + +```html + +``` + +### Adding Other Attributes (Including Data Attributes) + +By default, components only accept a limited number of HTML attributes. +This means that some uncommon but perfectly valid attributes, such as `lang` or +custom `data-*` attributes must be specifically enabled. For example, this template: + +```handlebars +{{#link-to "photos" data-toggle="dropdown" lang="es"}}Fotos{{/link-to}} +``` + +Will render the following HTML: + +```html +Fotos +``` + +To enable support for these attributes, an attribute binding must be +added for each specific attribute on the component. +To do this, you can extend the appropriate components +in your application. For example, for `link-to` you would create your own version +of this component by extending +[`Ember.LinkComponent`](https://www.emberjs.com/api/ember/release/classes/LinkComponent) + +```javascript {data-filename="app/components/link-to/component.js"} +import LinkComponent from '@ember/routing/link-component'; + +export default LinkComponent.extend({ + attributeBindings: ['data-toggle', 'lang'] +}); +``` + +Now the same template above renders the following HTML: + +```html +Fotos +``` diff --git a/guides/v3.6.0/templates/built-in-helpers.md b/guides/v3.6.0/templates/built-in-helpers.md new file mode 100644 index 0000000000..5361bd8fbe --- /dev/null +++ b/guides/v3.6.0/templates/built-in-helpers.md @@ -0,0 +1,88 @@ +In the last section you learned how to write a helper. +A helper is usually a simple function that can be used in any template. +Ember comes with a few helpers that can make developing your templates a bit easier. +These helpers can allow you to be more dynamic in passing data to another helper or component. + +### Using a helper to get a property dynamically + +The [`{{get}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/get?anchor=get) helper +makes it easy to dynamically send the value of a variable to another helper or component. +This can be useful if you want to output one of several values based on the result of a computed property. + +```handlebars +{{get address part}} +``` + +if the `part` computed property returns "zip", this will display the result of `this.address.zip`. +If it returns "city", you get `this.address.city`. + +### Nesting built-in helpers + +In the last section it was discussed that helpers can be nested. +This can be combined with these sorts of dynamic helpers. +For example, the [`{{concat}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/concat?anchor=concat) +helper makes it easy to dynamically send a number of parameters to a component or helper as a single parameter in the +format of a concatenated string. + +```handlebars +{{get "foo" (concat "item" index)}} +``` + +This will display the result of `this.foo.item1` when index is 1, and `this.foo.item2` when index is 2, etc. + +### Built-in block helpers +Now let's say your template is starting to get a bit cluttered and you now want to clean up the logic in your templates. +This can be achieved with the `let` block helper. +The [`{{let}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/let?anchor=let) helper lets you create new bindings in your template. + +Say your template now looks like this: + +```handlebars +Welcome back {{concat (capitalize person.firstName) ' ' (capitalize person.lastName)}} + +Account Details: +First Name: {{capitalize person.firstName}} +Last Name: {{capitalize person.lastName}} +``` + +As mentioned in the previous section we use the `concat` helper to render both `person.firstName` and `person.lastName` in one go. +But we also want to make sure that the names are capitalized. +It gets a bit repetitive to keep writing `capitalize` and honestly, we might just forget it at some point. +Thankfully, we can use the `{{let}}` helper to fix this: + +```handlebars +{{#let (capitalize person.firstName) (capitalize person.lastName) + as |firstName lastName| +}} + Welcome back {{concat firstName ' ' lastName}} + + Account Details: + First Name: {{firstName}} + Last Name: {{lastName}} +{{/let}} +``` + +Now, as long as your template is wrapped in the `let` helper you can access the capitalized first name and last name as +`firstName` and `lastName` instead of `(capitalize person.firstName)`. + +### Array helper +Using the [`{{array}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/array?anchor=array) helper, +you can pass arrays directly from the template as an argument to your components. + +```handlebars +{{my-component people=(array + 'Tom Dade' + 'Yehuda Katz' + myOtherPerson) + }} +``` + +In the component's template, you can then use the `people` argument as an array: + +```handlebars {data-filename=app/templates/components/my-component.hbs} +
      + {{#each people as |person|}} +
    • {{person}}
    • + {{/each}} +
    +``` diff --git a/guides/v3.6.0/templates/conditionals.md b/guides/v3.6.0/templates/conditionals.md new file mode 100644 index 0000000000..dc592a4ca6 --- /dev/null +++ b/guides/v3.6.0/templates/conditionals.md @@ -0,0 +1,92 @@ +Statements like [`if`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor=if) +and [`unless`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor=unless) +are implemented as built-in helpers. Helpers can be invoked three ways, each +of which is illustrated below with conditionals. + +The first style of invocation is **inline invocation**. This looks similar to +displaying a property, but helpers accept arguments. For example: + +```handlebars +
    + {{if isFast "zoooom" "putt-putt-putt"}} +
    +``` + +[`{{if}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor=if) +in this case returns `"zoooom"` when `isFast` is true and +`"putt-putt-putt"` when `isFast` is false. Helpers invoked as inline expressions +render a single value, the same way that properties are a single value. + +Inline helpers don't need to be used inside HTML tags. They can also be used +inside attribute values: + +```handlebars +
    +
    +``` + +**Nested invocation** is another way to use a helper. Like inline helpers, +nested helpers generate and return a single value. For example, this template +only renders `"zoooom"` if both `isFast` and `isFueled` are true: + +```handlebars +
    + {{if isFast (if isFueled "zoooom")}} +
    +``` + +The nested helper is called first returning `"zoooom"` only if `isFueled` is +true. Then the inline expression is called, rendering the nested helper's +value (`"zoooom"`) only if `isFast` is true. + +The third form of helper usage is **block invocation**. Use block helpers +to render only part of a template. Block invocation of a helper can be +recognized by the `#` before the helper name, and the closing `{{/` double +curly brace at the end of the invocation. + +For example, this template conditionally shows +properties on `person` only if that it is present: + +```handlebars +{{#if person}} + Welcome back, {{person.firstName}} {{person.lastName}}! +{{/if}} +``` + +[`{{if}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor=if) +checks for truthiness, which means all values except `false`, +`undefined`, `null`, `''`, `0` or `[]` (i.e., any JavaScript falsy value or an +empty array). + +If a value passed to `{{#if}}` evaluates to falsy, the `{{else}}` block +of that invocation is rendered: + +```handlebars +{{#if person}} + Welcome back, {{person.firstName}} {{person.lastName}}! +{{else}} + Please log in. +{{/if}} +``` + +`{{else}}` can chain helper invocation, the most common use case for this being +`{{else if}}`: + +```handlebars +{{#if isAtWork}} + Ship that code! +{{else if isReading}} + You can finish War and Peace eventually... +{{/if}} +``` + +The inverse of `{{if}}` is +[`{{unless}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor=unless), +which can be used in the same three styles of invocation. For example, this +template only shows an amount due when the user has not paid: + +```handlebars +{{#unless hasPaid}} + You owe: ${{total}} +{{/unless}} +``` diff --git a/guides/v3.6.0/templates/development-helpers.md b/guides/v3.6.0/templates/development-helpers.md new file mode 100644 index 0000000000..991e0b6e82 --- /dev/null +++ b/guides/v3.6.0/templates/development-helpers.md @@ -0,0 +1,59 @@ +## Development Helpers + +Handlebars and Ember come with a few helpers that can make developing your +templates a bit easier. These helpers make it simple to output variables into +your browser's console, or activate the debugger from your templates. + +### Logging + +The [`{{log}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor=log) helper makes it easy to output variables or expressions in + the +current rendering context into your browser's console: + +```handlebars +{{log 'Name is:' name}} +``` + +The `{{log}}` helper also accepts primitive types such as strings or numbers. + +### Adding a breakpoint + +The [``{{debugger}}``](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor=debugger) helper provides a handlebars equivalent to JavaScript's +`debugger` keyword. It will halt execution inside the debugger helper and give +you the ability to inspect the current rendering context: + + +```handlebars +{{debugger}} +``` + +When using the debugger helper you will have access to a `get` function. This +function retrieves values available in the context of the template. +For example, if you're wondering why a value `{{foo}}` isn't rendering as +expected within a template, you could place a `{{debugger}}` statement and, +when the `debugger;` breakpoint is hit, you can attempt to retrieve this value: + +```javascript +> get('foo') +``` + +`get` is also aware of keywords. So in this situation: + +```handlebars +{{#each items as |item|}} + {{debugger}} +{{/each}} +``` + +You'll be able to get values from the current item: + +```javascript +> get('item.name') +``` + +You can also access the context of the view to make sure it is the object that +you expect: + +```javascript +> context +``` diff --git a/guides/v3.6.0/templates/displaying-a-list-of-items.md b/guides/v3.6.0/templates/displaying-a-list-of-items.md new file mode 100644 index 0000000000..923360f09f --- /dev/null +++ b/guides/v3.6.0/templates/displaying-a-list-of-items.md @@ -0,0 +1,79 @@ +To iterate over a list of items, use the +[`{{#each}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor=each) +helper. The first argument to this helper is the array to be iterated, and +the value being iterated is yielded as a block param. Block params are only +available inside the block of their helper. + +For example, this template iterates an array named `people` that contains +objects. Each item in the array is provided as the block param `person`. + +```handlebars +
      + {{#each people as |person|}} +
    • Hello, {{person.name}}!
    • + {{/each}} +
    +``` + +Block params, like function arguments in JavaScript, are positional. `person` +is what each item is named in the above template, but `human` would work just +as well. + +The template inside of the `{{#each}}` block will be repeated once for +each item in the array, with the each item set to the `person` block param. + +Given an input array like: + +```javascript +[ + { name: 'Yehuda' }, + { name: 'Tom' }, + { name: 'Trek' } +] +``` + +The above template will render HTML like this: + +```html +
      +
    • Hello, Yehuda!
    • +
    • Hello, Tom!
    • +
    • Hello, Trek!
    • +
    +``` + +Like other helpers, the `{{#each}}` helper is bound. If a new item is added to +or removed from the iterated array, the DOM will be updated without having to +write any additional code. That said, Ember requires that you use [special +methods](../../object-model/enumerables/#toc_use-of-observable-methods-and-properties) +to update bound arrays. Also be aware that [using the `key` option with an each +helper](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor=each) +can improve re-render performance when an array is replaced with another +containing similar items. + +### Accessing an item's `index` + +During iteration, the index of each item in the array is provided as a second +block param. Block params are space-separated, without commas. For example: + +```handlebars +
      + {{#each people as |person index|}} +
    • Hello, {{person.name}}! You're number {{index}} in line
    • + {{/each}} +
    +``` + +### Empty Lists + +The [`{{#each}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor=each) +helper can have a corresponding `{{else}}`. The contents of this block will +render if the array passed to `{{#each}}` is empty: + +```handlebars +{{#each people as |person|}} + Hello, {{person.name}}! +{{else}} + Sorry, nobody is here. +{{/each}} +``` diff --git a/guides/v3.6.0/templates/displaying-the-keys-in-an-object.md b/guides/v3.6.0/templates/displaying-the-keys-in-an-object.md new file mode 100644 index 0000000000..2777d3e01d --- /dev/null +++ b/guides/v3.6.0/templates/displaying-the-keys-in-an-object.md @@ -0,0 +1,76 @@ +If you need to display all of the keys or values of a JavaScript object in your template, +you can use the [`{{#each-in}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor=each-in) helper: + +```javascript {data-filename=/app/components/store-categories.js} +import Component from '@ember/component'; + +export default Component.extend({ + willRender() { + // Set the "categories" property to a JavaScript object + // with the category name as the key and the value a list + // of products. + this.set('categories', { + 'Bourbons': ['Bulleit', 'Four Roses', 'Woodford Reserve'], + 'Ryes': ['WhistlePig', 'High West'] + }); + } +}); +``` + +```handlebars {data-filename=/app/templates/components/store-categories.hbs} +
      + {{#each-in categories as |category products|}} +
    • {{category}} +
        + {{#each products as |product|}} +
      1. {{product}}
      2. + {{/each}} +
      +
    • + {{/each-in}} +
    +``` + +The template inside of the `{{#each-in}}` block is repeated once for each key in the passed object. +The first block parameter (`category` in the above example) is the key for this iteration, +while the second block parameter (`products`) is the actual value of that key. + +The above example will print a list like this: + +```html +
      +
    • Bourbons +
        +
      1. Bulleit
      2. +
      3. Four Roses
      4. +
      5. Woodford Reserve
      6. +
      +
    • +
    • Ryes +
        +
      1. WhistlePig
      2. +
      3. High West
      4. +
      +
    • +
    +``` + +### Ordering + +An object's keys will be listed in the same order as the array returned from calling `Object.keys` on that object. +If you want a different sort order, you should use `Object.keys` to get an array, sort that array with the built-in JavaScript tools, +and use the [`{{#each}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor=each-in) helper instead. + +### Empty Lists + +The [`{{#each-in}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor=each-in) +helper can have a matching `{{else}}`. +The contents of this block will render if the object is empty, null, or undefined: + +```handlebars +{{#each-in people as |name person|}} + Hello, {{name}}! You are {{person.age}} years old. +{{else}} + Sorry, nobody is here. +{{/each-in}} +``` diff --git a/guides/v3.6.0/templates/handlebars-basics.md b/guides/v3.6.0/templates/handlebars-basics.md new file mode 100644 index 0000000000..b21a578d96 --- /dev/null +++ b/guides/v3.6.0/templates/handlebars-basics.md @@ -0,0 +1,95 @@ +Ember uses the [Handlebars templating library](http://www.handlebarsjs.com) +to power your app's user interface. Handlebars templates contain static HTML and dynamic content inside Handlebars expressions, which are invoked with double curly braces: `{{}}`. + +Dynamic content inside a Handlebars expression is rendered with data-binding. This means if you update a property, your usage of that property in a template will be automatically updated to the latest value. + +### Displaying Properties + +Templates are backed with a context. A context is an object from which +Handlebars expressions read their properties. In Ember this is often a component. For templates rendered by a route (like `application.hbs`), the context is a controller. + +For example, this `application.hbs` template will render a first and last name: + +```handlebars {data-filename=app/templates/application.hbs} +Hello, {{firstName}} {{lastName}}! +``` + +The `firstName` and `lastName` properties are read from the +context (the application controller in this case), and rendered inside the +`` HTML tag. + +To provide a `firstName` and `lastName` to the above template, properties +must be added to the application controller. If you are following along with +an Ember CLI application, you may need to create this file: + +```javascript {data-filename=app/controllers/application.js} +import Controller from '@ember/controller'; + +export default Controller.extend({ + firstName: 'Trek', + lastName: 'Glowacki' +}); +``` + +The above template and controller render as the following HTML: + +```html +Hello, Trek Glowacki! +``` + +Remember that `{{firstName}}` and `{{lastName}}` are bound data. That means +if the value of one of those properties changes, the DOM will be updated +automatically. + +As an application grows in size, it will have many templates backed by +controllers and components. + +### Helpers + +Ember Helpers are functions that can compute values and can be used in any template. + +Ember gives you the ability to [write your own helpers](../writing-helpers/), to bring a minimum of logic into Ember templating. + +For example, let's say you would like the ability to add a few numbers together, without needing to define a computed property everywhere you would like to do so. + +```javascript {data-filename=app/helpers/sum.js} +import { helper } from '@ember/component/helper'; + +export function sum(params) { + return params.reduce((a, b) => { + return a + b; + }); +}; + +export default helper(sum); +``` + +The above code will allow you invoke the `sum()` function as a `{{sum}}` handlebars "helper" in your templates: + +```html +

    Total: {{sum 1 2 3}}

    +``` + +This helper will output a value of `6`. + +Ember ships with several built-in helpers, which you will learn more about in the following guides. + +#### Nested Helpers + +Helpers have the ability to be nested within other helper invocations and also component invocations. + +This gives you the flexibility to compute a value _before_ it is passed in as an argument or an attribute of another. + +It is not possible to nest curly braces `{{}}`, so the correct way to nest a helper is by using parentheses `()`: + +```html +{{sum (multiply 2 4) 2}} +``` + +In this example, we are using a helper to multiply `2` and `4` _before_ passing the value into `{{sum}}`. + +Thus, the output of these combined helpers is `10`. + +As you move forward with these template guides, keep in mind that a helper can be used anywhere a normal value can be used. + +Thus, many of Ember's built-in helpers (as well as your custom helpers) can be used in nested form. diff --git a/guides/v3.6.0/templates/input-helpers.md b/guides/v3.6.0/templates/input-helpers.md new file mode 100644 index 0000000000..33bf1858e8 --- /dev/null +++ b/guides/v3.6.0/templates/input-helpers.md @@ -0,0 +1,115 @@ +The [`{{input}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor=input) +and [`{{textarea}}`](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor=textarea) +helpers in Ember.js are the easiest way to create common form controls. +Using these helpers, you can create form controls that are almost identical to the native HTML `` or `