Skip to content
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

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions guides/release/services/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Copy link
Contributor

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

Copy link
Contributor Author

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

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:
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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-contents/vanilla-class.js}
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-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 and possibly provide yourself an easy way to access the value:

```javascript {data-filename=app/components/cart-contents/vanilla-class.js}
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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

@NullVoxPopuli NullVoxPopuli Nov 2, 2023

Choose a reason for hiding this comment

The 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:

  • wrapper class/object/whatever (with getters)
  • arrow function

curious what solutions you think there may be

Loading