-
-
Notifications
You must be signed in to change notification settings - Fork 497
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a section for using vanilla classes with dependency injection #1974
base: master
Are you sure you want to change the base?
Changes from 14 commits
632b999
018f09e
1d1a003
8108309
f6d6e5a
ad97829
ea5d37a
986f920
4f6bd04
d408c3e
9a71149
d010687
afaf5f0
b852466
6a20bf7
c773137
5a1ee2e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -140,3 +140,145 @@ Note `cart` being used below to get data from the cart. | |
``` | ||
|
||
<!-- eof - needed for pages that end in a code block --> | ||
|
||
### Accessing services from native classes | ||
|
||
If you want to access a service from a plain JavaScript class, you'll need to get a reference to the "[owner](https://api.emberjs.com/ember/release/modules/@ember%2Fowner)" object, which is responsible for managing services. | ||
|
||
First, we can define a class that accesses services as described above: | ||
|
||
```javascript {data-filename=app/components/cart-contents/vanilla-class.js} | ||
import { service } from '@ember/service'; | ||
|
||
export class VanillaClass { | ||
@service shoppingCart; | ||
|
||
someMethod() { | ||
// Now you can use the service | ||
this.shoppingCart.add(/* ... */); | ||
} | ||
} | ||
``` | ||
|
||
And then to wire up `VanillaClass` to work with `@service`, you'll need to implement a ceremony: | ||
|
||
```javascript {data-filename=app/components/cart-contents/index.js} | ||
import { getOwner, setOwner } from '@ember/owner'; | ||
import { VanillaClass } from './vanilla-class'; | ||
NullVoxPopuli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
export default class CartContentsComponent extends Component { | ||
@cached | ||
get vanillaClass() { | ||
const instance = new VanillaClass(); | ||
|
||
setOwner(instance, getOwner(this)); | ||
|
||
return instance; | ||
} | ||
} | ||
``` | ||
|
||
In reality, this could be any framework-construct: a service, route, controller, etc -- in this case we use a component, but this could also be done in another vanilla class that's already be wired up. | ||
The pattern here is to use a [`@cached`](https://api.emberjs.com/ember/5.3/functions/@glimmer%2Ftracking/cached) getter to ensure a stable reference to the class, and then using [`setOwner`]( https://api.emberjs.com/ember/5.3/functions/@ember%2Fowner/setOwner) and [`getOwner`](https://api.emberjs.com/ember/5.3/functions/@ember%2Fowner/getOwner), we finish the wiring ceremony needed to make native classes work with services. | ||
|
||
|
||
<div class="cta"> | ||
<div class="cta-note"> | ||
<div class="cta-note-body"> | ||
<div class="cta-note-heading">Zoey says...</div> | ||
<div class="cta-note-message"> | ||
|
||
Note that a stable reference in this situation means that when the property is accessed multiple times the same reference is returned. Without the `@cached` decorator, a new `VanillaClass` would be instantiated upon each access of the getter. | ||
|
||
</div> | ||
</div> | ||
<img src="/images/mascots/zoey.png" role="presentation" alt=""> | ||
</div> | ||
</div> | ||
|
||
The exact way in which the wiring ceremony is done is up to you, but it often depends on what is needed, and community libraries may abstract away all of these if they wish. | ||
|
||
#### With arguments | ||
|
||
If your native class needs arguments, we can change the above example to instantiate the class like this: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From here down seems like useful content, but maybe unrelated to services. Not sure where else it could go. Do we have a cookbook section? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, it's more related with "how to in general work with native classes in general reactive systems" -- which we don't have a section for about reactivity patterns, derived data, or anything like that. there was a whole cookbook project, rfc and such, I don't have context as to where that's at tho |
||
|
||
```javascript {data-filename=app/components/cart-contents/index.js} | ||
import { setOwner, getOwner } from '@ember/owner'; | ||
|
||
import { VanillaClass } from './vanilla-class'; | ||
NullVoxPopuli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
export default class CartContentsComponent extends Component { | ||
@cached | ||
get vanillaClass() { | ||
const instance = new VanillaClass(this.args.foo); | ||
|
||
setOwner(instance, getOwner(this)); | ||
|
||
return instance; | ||
} | ||
} | ||
``` | ||
|
||
Back in the `VanillaClass` itself, you must store the value somewhere, via the constructor: | ||
|
||
```javascript {data-filename=app/components/cart-content/vanilla-class.js} | ||
NullVoxPopuli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
import { getOwner } from '@ember/owner'; | ||
import { service } from '@ember/service'; | ||
|
||
export class VanillaClass { | ||
@service shoppingCart; | ||
|
||
constructor(foo) { | ||
this.foo = foo; | ||
} | ||
|
||
/* ... */ | ||
} | ||
``` | ||
|
||
In this situation, when the component's `@foo` argument changes (accessed in JavaScript via `this.args.foo`), a new `VanillaClass` will be instantiated and wired up if it was accessed. | ||
|
||
#### Reactive arguments | ||
|
||
Sometimes you'll want `@tracked` state to retain its reactivity when passing to a native class, so for that you'll need to use an anonymous arrow function. | ||
|
||
|
||
```javascript {data-filename=app/components/cart-content/index.js} | ||
NullVoxPopuli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
import { setOwner, getOwner } from '@ember/owner'; | ||
|
||
import { VanillaClass } from './vanilla-class'; | ||
NullVoxPopuli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
export default class CartContentsComponent extends Component { | ||
@cached | ||
get vanillaClass() { | ||
const instance = new VanillaClass(() => this.args.foo); | ||
|
||
setOwner(instance, getOwner(this)); | ||
|
||
return instance; | ||
} | ||
} | ||
``` | ||
|
||
Back in the `VanillaClass` itself, you must store the value somewhere and possibly provide yourself an easy way to access the value: | ||
|
||
```javascript {data-filename=app/components/cart-content/vanilla-class.js} | ||
NullVoxPopuli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
import { service } from '@ember/service'; | ||
|
||
export class VanillaClass { | ||
@service shoppingCart; | ||
|
||
constructor(fooFunction) { | ||
this.fooFunction = fooFunction; | ||
} | ||
|
||
get foo() { | ||
return this.fooFunction(); | ||
} | ||
|
||
/* ... */ | ||
} | ||
``` | ||
|
||
With this technique, the tracked data provided by `this.arg.foo` is lazily evaluated in `VanillaClass`, allowing the `VanillaClass` to participate in lazy evaluation and auto-tracking like every where else you may be used to in an app. | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don’t love these suggestions here. These are the problems I was bringing up in the spec channel in discord the other day. We can discuss more in the meeting today There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we only have two ways to pass reactive data (in js) without consuming it:
curious what solutions you think there may be |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the class is going to use a service, it really should have the owner passed in to the constructor and do the setOwner in the constructor like glimmer components imo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really agree :-\
if the responsibility of setting up the owner is always in the constructor then it's impossible to abstract reasonably, as one might to do here: https://ember-resources.pages.dev/funcs/link.link