Skip to content

Commit

Permalink
fix(create-injectable): providedIn root by default
Browse files Browse the repository at this point in the history
  • Loading branch information
nartc committed Jan 22, 2024
1 parent 26e94a7 commit a948b31
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 27 deletions.
43 changes: 29 additions & 14 deletions docs/src/content/docs/utilities/Injectors/create-injectable.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,24 @@ The general difference is that rather than using a class, we use a `function` to
create the injectable. Whatever the function returns is what will be the
consumable public API of the service, everything else will be private.

Pass `{ providedIn: 'root' }` to the 2nd argument of `createInjectable` if you want to create a root service.
### `providedIn`

## Usage
By default, `createInjectable` returns a root service with `providedIn: 'root'`. You can override this by passing in a second argument to `createInjectable`:

- `scoped`: The service will be scoped to where it is provided in (i.e: `providers` array)
- `platform`: The service will be scoped to the platform (i.e: `platform-browser`). This is recommended if you create services that are used across multiple apps on the same platform.

### Non-root Service

```ts
// defining a service
export const MyService = createInjectable(() => {
const myState = signal(1);

return { myState: myState.asReadonly() };
});
export const MyService = createInjectable(
() => {
const myState = signal(1);
return { myState: myState.asReadonly() };
},
{ providedIn: 'scoped' },
);
```

```ts
Expand All @@ -44,16 +49,26 @@ const myService = inject(MyService);

```ts
// defining a root service
export const MyService = createInjectable(
() => {
const myState = signal(1);
return { myState: myState.asReadonly() };
},
{ providedIn: 'root' },
);
export const MyService = createInjectable(() => {
const myState = signal(1);
return { myState: myState.asReadonly() };
});
```

```ts
// using the service
const myService = inject(MyService);
```

### Using a named function

It is possible to use a named function as the `factory` instead of an arrow function. If a named function is used, the name of the function will be used as the name of the service constructor.

```ts
export const MyService = createInjectable(function MyService() {
const myState = signal(1);
return { myState: myState.asReadonly() };
});

console.log(MyService.name); // MyService
```
22 changes: 11 additions & 11 deletions libs/ngxtension/create-injectable/src/create-injectable.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@ import { createInjectable } from './create-injectable';
describe(createInjectable.name, () => {
it('should be able to access property returned from injectable', () => {
let count = 0;
const MyInjectable = createInjectable(
() => {
count += 1;
return { someProp: 1 };
},
{ providedIn: 'root' },
);
const MyInjectable = createInjectable(() => {
count += 1;
return { someProp: 1 };
});

TestBed.runInInjectionContext(() => {
// should be lazy until `inject()` is invoked
Expand All @@ -34,10 +31,13 @@ describe(createInjectable.name, () => {

it('should be able to provide non-root injectable', () => {
let count = 0;
const MyInjectable = createInjectable(() => {
count += 1;
return { someProp: 1 };
});
const MyInjectable = createInjectable(
() => {
count += 1;
return { someProp: 1 };
},
{ providedIn: 'scoped' },
);

TestBed.configureTestingModule({
providers: [MyInjectable],
Expand Down
10 changes: 8 additions & 2 deletions libs/ngxtension/create-injectable/src/create-injectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ import { Injectable, type Type } from '@angular/core';

export function createInjectable<TFactory extends (...args: any[]) => object>(
factory: TFactory,
{ providedIn }: { providedIn?: 'root' } = {},
{ providedIn = 'root' }: { providedIn?: 'root' | 'platform' | 'scoped' } = {},
): Type<ReturnType<TFactory>> {
@Injectable({ providedIn: providedIn || null })
@Injectable({ providedIn: providedIn === 'scoped' ? null : providedIn })
class _Injectable {
constructor() {
Object.assign(this, factory());
}
}

if (factory.name) {
Object.defineProperty(_Injectable, 'name', {
value: `_Injectable_${factory.name}`,
});
}

return _Injectable as Type<ReturnType<TFactory>>;
}

2 comments on commit a948b31

@spierala
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @nartc,

before it was better, IMO:

  • more consistent with Angular Injectable decorator
  • people who know Angular could more easily understand what createInjectable(() => {}, {providedIn: 'root'}) does (even without studying the ngxtension docs)
  • scoped sounds like a new concept, but its just the old Injectable decorator default (scoped would let me think I better check the docs ;))

@tomalaforge
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @nartc,

before it was better, IMO:

  • more consistent with Angular Injectable decorator
  • people who know Angular could more easily understand what createInjectable(() => {}, {providedIn: 'root'}) does (even without studying the ngxtension docs)
  • scoped sounds like a new concept, but its just the old Injectable decorator default (scoped would let me think I better check the docs ;))

I agree with @spierala. I like that the function should imitate what developer are already used to.
To provide in root, we need to specify it.
'scoped' nobody knows that word and nobody will know what it does. If someone new is ready the code and not used to ngxtension, he will not know what is going on, or that root is by default.

Please sign in to comment.