Browse Source

Code formatting

tags/v5.0.0
Cédric Belin 6 months ago
parent
commit
689fee3f02
27 changed files with 1006 additions and 1003 deletions
  1. +6
    -3
      .editorconfig
  2. +1
    -1
      .github/workflows/build.yaml
  3. +5
    -5
      .vscode/settings.json
  4. +29
    -29
      angular.json
  5. +0
    -4
      doc/about/see_also.md
  6. +2
    -2
      doc/index.md
  7. +8
    -8
      doc/installation.md
  8. +172
    -172
      doc/usage/api.md
  9. +37
    -37
      doc/usage/events.md
  10. +16
    -16
      doc/usage/iteration.md
  11. +61
    -61
      etc/eslint.yaml
  12. +14
    -14
      etc/karma.cjs
  13. +9
    -5
      etc/mkdocs.yaml
  14. +43
    -43
      example/main.ts
  15. +7
    -7
      ng-package.json
  16. +83
    -83
      package.json
  17. +1
    -1
      src/index.ts
  18. +189
    -189
      src/storage.ts
  19. +19
    -19
      src/tsconfig.json
  20. +5
    -5
      test/main.ts
  21. +270
    -270
      test/storage_test.ts
  22. +5
    -5
      test/tsconfig.json
  23. +6
    -6
      tool/build.ps1
  24. +3
    -3
      tool/clean.ps1
  25. +0
    -4
      tool/fix.ps1
  26. +5
    -1
      tool/publish.ps1
  27. +10
    -10
      tool/watch.ps1

+ 6
- 3
.editorconfig View File

@@ -3,11 +3,14 @@ root = true

[*]
charset = utf-8
indent_size = 2
indent_style = space
indent_style = tab
insert_final_newline = true
tab_width = 2
trim_trailing_whitespace = true

[*.md]
indent_size = 4
trim_trailing_whitespace = false

[*.{yaml,yml}]
indent_size = 2
indent_style = space

+ 1
- 1
.github/workflows/build.yaml View File

@@ -3,7 +3,7 @@ on:
pull_request:
push:
schedule:
- cron: '0 0 1 * *'
- cron: "0 0 1 * *"
jobs:
test:
runs-on: ubuntu-latest


+ 5
- 5
.vscode/settings.json View File

@@ -1,7 +1,7 @@
{
"editor.insertSpaces": true,
"editor.tabSize": 2,
"files.encoding": "utf8",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true
"editor.insertSpaces": false,
"editor.tabSize": 2,
"files.encoding": "utf8",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true
}

+ 29
- 29
angular.json View File

@@ -1,31 +1,31 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"defaultProject": "ngx-webstorage",
"version": 1,
"projects": {
"ngx-webstorage": {
"prefix": "ngx",
"projectType": "library",
"root": "",
"sourceRoot": "src",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"options": {
"project": "ng-package.json",
"tsConfig": "src/tsconfig.json"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"codeCoverage": true,
"main": "test/main.ts",
"karmaConfig": "etc/karma.cjs",
"tsConfig": "test/tsconfig.json"
}
}
}
}
}
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"defaultProject": "ngx-webstorage",
"version": 1,
"projects": {
"ngx-webstorage": {
"prefix": "ngx",
"projectType": "library",
"root": "",
"sourceRoot": "src",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"options": {
"project": "ng-package.json",
"tsConfig": "src/tsconfig.json"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"codeCoverage": true,
"main": "test/main.ts",
"karmaConfig": "etc/karma.cjs",
"tsConfig": "test/tsconfig.json"
}
}
}
}
}
}

+ 0
- 4
doc/about/see_also.md View File

@@ -8,7 +8,3 @@
## Testing
- [Continuous integration](https://github.com/cedx/ngx-webstorage/actions)
- [Code coverage](https://coveralls.io/github/cedx/ngx-webstorage)

## Other implementations
- Dart: [Web Storage for Dart](https://docs.belin.io/webstorage.dart)
- JavaScript: [Web Storage for JS](https://docs.belin.io/webstorage.js)

+ 2
- 2
doc/index.md View File

@@ -9,11 +9,11 @@ implemented in [TypeScript](https://www.typescriptlang.org).
## Quick start
Install the latest version of **Web Storage for Angular** with [npm](https://www.npmjs.com):

```shell
``` shell
npm install @cedx/ngx-webstorage
```

!!! info
This library is packaged as [ECMAScript modules](https://nodejs.org/api/esm.html).
This library is packaged as [ECMAScript modules](https://nodejs.org/api/esm.html).

For detailed instructions, see the [installation guide](installation.md).

+ 8
- 8
doc/installation.md View File

@@ -6,30 +6,30 @@ and [npm](https://www.npmjs.com), the Node.js package manager, up and running.

You can verify if you're already good to go with the following commands:

```shell
``` shell
node --version
# v14.2.0
# v14.3.0

npm --version
# 6.14.4
# 6.14.5
```

!!! info
If you plan to play with the package sources, you will also need
[PowerShell](https://docs.microsoft.com/en-us/powershell) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material).
If you plan to play with the package sources, you will also need
[PowerShell](https://docs.microsoft.com/en-us/powershell) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material).

## Installing with npm package manager

### 1. Install it
From a command prompt, run:

```shell
``` shell
npm install @cedx/ngx-webstorage
```

### 2. Import it
Now in your [TypeScript](https://www.typescriptlang.org) code, you can use:

```typescript
import {LocalStorage, SessionStorage} from '@cedx/ngx-webstorage';
``` typescript
import {LocalStorage, SessionStorage} from "@cedx/ngx-webstorage";
```

+ 172
- 172
doc/usage/api.md View File

@@ -6,24 +6,24 @@ source: src/storage.ts
# Programming interface
This package provides two services dedicated to the Web Storage: the `LocalStorage` and `SessionStorage` classes.

```typescript
import {Component, OnInit} from '@angular/core';
import {LocalStorage} from '@cedx/ngx-webstorage';
``` typescript
import {Component, OnInit} from "@angular/core";
import {LocalStorage} from "@cedx/ngx-webstorage";

@Component({
selector: 'my-component',
templateUrl: './my-component.html'
selector: "my-component",
templateUrl: "./my-component.html"
})
export class MyComponent implements OnInit {
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
this._storage.set('foo', 'bar');
console.log(this._storage.get('foo')); // "bar"
this._storage.setObject('foo', {baz: 'qux'});
console.log(this._storage.getObject('foo')); // {baz: "qux"}
}
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
this._storage.set("foo", "bar");
console.log(this._storage.get("foo")); // "bar"
this._storage.setObject("foo", {baz: "qux"});
console.log(this._storage.getObject("foo")); // {baz: "qux"}
}
}
```

@@ -32,94 +32,94 @@ Each class extends from the `WebStorage` abstract class that has the following A
## **keys**: string[]
Returns the keys of the the associated storage:

```typescript
import {Component, OnInit} from '@angular/core';
import {LocalStorage} from '@cedx/ngx-webstorage';
``` typescript
import {Component, OnInit} from "@angular/core";
import {LocalStorage} from "@cedx/ngx-webstorage";

@Component({
selector: 'my-component',
templateUrl: './my-component.html'
selector: "my-component",
templateUrl: "./my-component.html"
})
export class MyComponent implements OnInit {
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.keys); // []
this._storage.set('foo', 'bar');
console.log(this._storage.keys); // ["foo"]
}
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.keys); // []
this._storage.set("foo", "bar");
console.log(this._storage.keys); // ["foo"]
}
}
```

## **length**: number
Returns the number of entries in the associated storage:

```typescript
import {Component, OnInit} from '@angular/core';
import {LocalStorage} from '@cedx/ngx-webstorage';
``` typescript
import {Component, OnInit} from "@angular/core";
import {LocalStorage} from "@cedx/ngx-webstorage";

@Component({
selector: 'my-component',
templateUrl: './my-component.html'
selector: "my-component",
templateUrl: "./my-component.html"
})
export class MyComponent implements OnInit {
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.length); // 0
this._storage.set('foo', 'bar');
console.log(this._storage.length); // 1
}
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.length); // 0
this._storage.set("foo", "bar");
console.log(this._storage.length); // 1
}
}
```

## **clear**(): void
Removes all entries from the associated storage:

```typescript
import {Component, OnInit} from '@angular/core';
import {LocalStorage} from '@cedx/ngx-webstorage';
``` typescript
import {Component, OnInit} from "@angular/core";
import {LocalStorage} from "@cedx/ngx-webstorage";

@Component({
selector: 'my-component',
templateUrl: './my-component.html'
selector: "my-component",
templateUrl: "./my-component.html"
})
export class MyComponent implements OnInit {
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
this._storage.set('foo', 'bar');
console.log(this._storage.length); // 1
this._storage.clear();
console.log(this._storage.length); // 0
}
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
this._storage.set("foo", "bar");
console.log(this._storage.length); // 1
this._storage.clear();
console.log(this._storage.length); // 0
}
}
```

## **get**(key: string, defaultValue?: string): string|undefined
Returns the value associated to the specified key:

```typescript
import {Component, OnInit} from '@angular/core';
import {LocalStorage} from '@cedx/ngx-webstorage';
``` typescript
import {Component, OnInit} from "@angular/core";
import {LocalStorage} from "@cedx/ngx-webstorage";

@Component({
selector: 'my-component',
templateUrl: './my-component.html'
selector: "my-component",
templateUrl: "./my-component.html"
})
export class MyComponent implements OnInit {
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.get('foo')); // undefined
console.log(this._storage.get('foo', 'qux')); // "qux"
this._storage.set('foo', 'bar');
console.log(this._storage.get('foo')); // "bar"
}
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.get("foo")); // undefined
console.log(this._storage.get("foo", "qux")); // "qux"
this._storage.set("foo", "bar");
console.log(this._storage.get("foo")); // "bar"
}
}
```

@@ -128,52 +128,52 @@ Returns `undefined` or the given default value if the key is not found.
## **getObject**(key: string, defaultValue?: any): any
Deserializes and returns the value associated to the specified key:

```typescript
import {Component, OnInit} from '@angular/core';
import {LocalStorage} from '@cedx/ngx-webstorage';
``` typescript
import {Component, OnInit} from "@angular/core";
import {LocalStorage} from "@cedx/ngx-webstorage";

@Component({
selector: 'my-component',
templateUrl: './my-component.html'
selector: "my-component",
templateUrl: "./my-component.html"
})
export class MyComponent implements OnInit {
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.getObject('foo')); // undefined
console.log(this._storage.getObject('foo', 'qux')); // "qux"
this._storage.setObject('foo', {bar: 'baz'});
console.log(this._storage.getObject('foo')); // {bar: "baz"}
}
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.getObject("foo")); // undefined
console.log(this._storage.getObject("foo", "qux")); // "qux"
this._storage.setObject("foo", {bar: "baz"});
console.log(this._storage.getObject("foo")); // {bar: "baz"}
}
}
```

!!! info
The value is deserialized using the [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) method.
The value is deserialized using the [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) method.

Returns `undefined` or the given default value if the key is not found.

## **has**(key: string): boolean
Returns a boolean value indicating whether the associated storage contains the specified key:

```typescript
import {Component, OnInit} from '@angular/core';
import {LocalStorage} from '@cedx/ngx-webstorage';
``` typescript
import {Component, OnInit} from "@angular/core";
import {LocalStorage} from "@cedx/ngx-webstorage";

@Component({
selector: 'my-component',
templateUrl: './my-component.html'
selector: "my-component",
templateUrl: "./my-component.html"
})
export class MyComponent implements OnInit {
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.has('foo')); // false
this._storage.set('foo', 'bar');
console.log(this._storage.has('foo')); // true
}
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.has("foo")); // false
this._storage.set("foo", "bar");
console.log(this._storage.has("foo")); // true
}
}
```

@@ -182,27 +182,27 @@ Looks up the value of the specified key, or add a new value if it isn't there.

Returns the value associated to the key, if there is one. Otherwise calls `ifAbsent` to get a new value, associates the key to that value, and then returns the new value:

```typescript
import {Component, OnInit} from '@angular/core';
import {LocalStorage} from '@cedx/ngx-webstorage';
``` typescript
import {Component, OnInit} from "@angular/core";
import {LocalStorage} from "@cedx/ngx-webstorage";

@Component({
selector: 'my-component',
templateUrl: './my-component.html'
selector: "my-component",
templateUrl: "./my-component.html"
})
export class MyComponent implements OnInit {
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.has('foo')); // false
let value = this._storage.putIfAbsent('foo', () => 'bar');
console.log(this._storage.has('foo')); // true
console.log(value); // "bar"
value = this._storage.putIfAbsent('foo', () => 'qux');
console.log(value); // "bar"
}
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.has("foo")); // false
let value = this._storage.putIfAbsent("foo", () => "bar");
console.log(this._storage.has("foo")); // true
console.log(value); // "bar"
value = this._storage.putIfAbsent("foo", () => "qux");
console.log(value); // "bar"
}
}
```

@@ -211,54 +211,54 @@ Looks up the value of the specified key, or add a new value if it isn't there.

Returns the deserialized value associated to the key, if there is one. Otherwise calls `ifAbsent` to get a new value, serializes and associates the key to that value, and then returns the new value:

```typescript
import {Component, OnInit} from '@angular/core';
import {LocalStorage} from '@cedx/ngx-webstorage';
``` typescript
import {Component, OnInit} from "@angular/core";
import {LocalStorage} from "@cedx/ngx-webstorage";

@Component({
selector: 'my-component',
templateUrl: './my-component.html'
selector: "my-component",
templateUrl: "./my-component.html"
})
export class MyComponent implements OnInit {
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.has('foo')); // false
let value = this._storage.putObjectIfAbsent('foo', () => 123);
console.log(this._storage.has('foo')); // true
console.log(value); // 123
value = this._storage.putObjectIfAbsent('foo', () => 456);
console.log(value); // 123
}
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.has("foo")); // false
let value = this._storage.putObjectIfAbsent("foo", () => 123);
console.log(this._storage.has("foo")); // true
console.log(value); // 123
value = this._storage.putObjectIfAbsent("foo", () => 456);
console.log(value); // 123
}
}
```

!!! info
The value is serialized using the [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) method, and deserialized using the [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) method.
The value is serialized using the [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) method, and deserialized using the [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) method.

## **remove**(key: string): string|undefined
Removes the value associated to the specified key:

```typescript
import {Component, OnInit} from '@angular/core';
import {LocalStorage} from '@cedx/ngx-webstorage';
``` typescript
import {Component, OnInit} from "@angular/core";
import {LocalStorage} from "@cedx/ngx-webstorage";

@Component({
selector: 'my-component',
templateUrl: './my-component.html'
selector: "my-component",
templateUrl: "./my-component.html"
})
export class MyComponent implements OnInit {
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
this._storage.set('foo', 'bar');
console.log(this._storage.has('foo')); // true
console.log(this._storage.remove('foo')); // "bar"
console.log(this._storage.has('foo')); // false
}
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
this._storage.set("foo", "bar");
console.log(this._storage.has("foo")); // true
console.log(this._storage.remove("foo")); // "bar"
console.log(this._storage.has("foo")); // false
}
}
```

@@ -267,48 +267,48 @@ Returns the value associated with the specified key before it was removed.
## **set**(key: string, value: string): this
Associates a given value to the specified key:

```typescript
import {Component, OnInit} from '@angular/core';
import {LocalStorage} from '@cedx/ngx-webstorage';
``` typescript
import {Component, OnInit} from "@angular/core";
import {LocalStorage} from "@cedx/ngx-webstorage";

@Component({
selector: 'my-component',
templateUrl: './my-component.html'
selector: "my-component",
templateUrl: "./my-component.html"
})
export class MyComponent implements OnInit {
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.get('foo')); // undefined
this._storage.set('foo', 'bar');
console.log(this._storage.get('foo')); // "bar"
}
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.get("foo")); // undefined
this._storage.set("foo", "bar");
console.log(this._storage.get("foo")); // "bar"
}
}
```

## **setObject**(key: string, value: any): this
Serializes and associates a given value to the specified key:

```typescript
import {Component, OnInit} from '@angular/core';
import {LocalStorage} from '@cedx/ngx-webstorage';
``` typescript
import {Component, OnInit} from "@angular/core";
import {LocalStorage} from "@cedx/ngx-webstorage";

@Component({
selector: 'my-component',
templateUrl: './my-component.html'
selector: "my-component",
templateUrl: "./my-component.html"
})
export class MyComponent implements OnInit {
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.getObject('foo')); // undefined
this._storage.setObject('foo', {bar: 'baz'});
console.log(this._storage.getObject('foo')); // {bar: "baz"}
}
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
console.log(this._storage.getObject("foo")); // undefined
this._storage.setObject("foo", {bar: "baz"});
console.log(this._storage.getObject("foo")); // {bar: "baz"}
}
}
```

!!! info
The value is serialized using the [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) method.
The value is serialized using the [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) method.

+ 37
- 37
doc/usage/events.md View File

@@ -3,64 +3,64 @@ Every time one or several values are changed (added, removed or updated) through

This event is exposed as an [Observable](https://angular.io/guide/observables), you can subscribe to it using the `onChanges` property:

```typescript
import {Component, OnInit, SimpleChanges} from '@angular/core';
import {LocalStorage} from '@cedx/ngx-webstorage';
``` typescript
import {Component, OnInit, SimpleChanges} from "@angular/core";
import {LocalStorage} from "@cedx/ngx-webstorage";

@Component({
selector: 'my-component',
templateUrl: './my-component.html'
selector: "my-component",
templateUrl: "./my-component.html"
})
export class MyComponent implements OnInit {
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
this._storage.onChanges.subscribe((changes: SimpleChanges) => {
for (const [key, value] of Object.entries(changes)) console.log(`${key}: ${JSON.stringify(value)}`);
});
}
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
this._storage.onChanges.subscribe((changes: SimpleChanges) => {
for (const [key, value] of Object.entries(changes)) console.log(`${key}: ${JSON.stringify(value)}`);
});
}
}
```

The changes are expressed as a [`SimpleChanges`](https://angular.io/api/core/SimpleChanges) object.
The values of this object are [`SimpleChange`](https://angular.io/api/core/SimpleChange) instances, where an `undefined` property indicates an absence of value:

```typescript
import {Component, OnInit, SimpleChanges} from '@angular/core';
import {LocalStorage} from '@cedx/ngx-webstorage';
``` typescript
import {Component, OnInit, SimpleChanges} from "@angular/core";
import {LocalStorage} from "@cedx/ngx-webstorage";

@Component({
selector: 'my-component',
templateUrl: './my-component.html'
selector: "my-component",
templateUrl: "./my-component.html"
})
export class MyComponent implements OnInit {
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
this._storage.onChanges.subscribe((changes: SimpleChanges) => {
for (const [key, change] of Object.entries(changes)) console.log({
key,
current: change.currentValue,
previous: change.previousValue
});
});
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
this._storage.onChanges.subscribe((changes: SimpleChanges) => {
for (const [key, change] of Object.entries(changes)) console.log({
key,
current: change.currentValue,
previous: change.previousValue
});
});

this._storage.set('foo', 'bar');
// Prints: {key: "foo", current: "bar", previous: undefined}
this._storage.set("foo", "bar");
// Prints: {key: "foo", current: "bar", previous: undefined}

this._storage.set('foo', 'baz');
// Prints: {key: "foo", current: "baz", previous: "bar"}
this._storage.set("foo", "baz");
// Prints: {key: "foo", current: "baz", previous: "bar"}

this._storage.remove('foo');
// Prints: {key: "foo", current: undefined, previous: "baz"}
}
this._storage.remove("foo");
// Prints: {key: "foo", current: undefined, previous: "baz"}
}
}
```

The values contained in the `currentValue` and `previousValue` properties of the `SimpleChange` instances are the raw storage values. If you use the `WebStorage.setObject()` method to store a value, you will get the serialized string value, not the original value passed to the method:

```typescript
this._storage.setObject('foo', {bar: 'baz'});
``` typescript
this._storage.setObject("foo", {bar: "baz"});
// Prints: {key: "foo", current: "{\"bar\": \"baz\"}", previous: undefined}
```

@@ -70,4 +70,4 @@ The `WebStorage` parent class supports the global [storage events](https://devel
When a change is made to the storage area within the context of another document (i.e. in another tab or `<iframe>`), a `changes` event is triggered to notify the modification.

!!! info
You do not need to explicitly subscribe to the global [storage events](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event): this is automatically done when instantiating the service. The subscription is canceled when the service is destroyed (i.e. its `ngOnDestroy()` method is called).
You do not need to explicitly subscribe to the global [storage events](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event): this is automatically done when instantiating the service. The subscription is canceled when the service is destroyed (i.e. its `ngOnDestroy()` method is called).

+ 16
- 16
doc/usage/iteration.md View File

@@ -2,26 +2,26 @@
The [`LocalStorage`](api.md) and [`SessionStorage`](api.md) classes are iterable: you can go through all key/value pairs contained using a `for...of` loop.
Each entry is an array with two elements (i.e. the key and the value):

```typescript
import {Component, OnInit} from '@angular/core';
import {LocalStorage} from '@cedx/ngx-webstorage';
``` typescript
import {Component, OnInit} from "@angular/core";
import {LocalStorage} from "@cedx/ngx-webstorage";

@Component({
selector: 'my-component',
templateUrl: './my-component.html'
selector: "my-component",
templateUrl: "./my-component.html"
})
export class MyComponent implements OnInit {
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
this._storage.set('foo', 'bar');
this._storage.set('anotherKey', 'anotherValue');
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
this._storage.set("foo", "bar");
this._storage.set("anotherKey", "anotherValue");

for (const entry of this._storage) {
console.log(entry);
// Round 1: ["foo", "bar"]
// Round 2: ["anotherKey", "anotherValue"]
}
}
for (const entry of this._storage) {
console.log(entry);
// Round 1: ["foo", "bar"]
// Round 2: ["anotherKey", "anotherValue"]
}
}
}
```

+ 61
- 61
etc/eslint.yaml View File

@@ -1,23 +1,23 @@
root: true
env:
browser: true
es2020: true
node: true
extends:
- eslint:recommended
- plugin:@typescript-eslint/eslint-recommended
- plugin:@typescript-eslint/recommended
- plugin:@typescript-eslint/recommended-requiring-type-checking
parser: '@typescript-eslint/parser'
parser: "@typescript-eslint/parser"
parserOptions:
project: src/tsconfig.json
sourceType: module
warnOnUnsupportedTypeScriptVersion: false
plugins: ['@typescript-eslint']
plugins: ["@typescript-eslint"]

rules:
# Possible errors.
no-console: [error, {allow: [error, warn]}]
no-extra-semi: 'off'
no-extra-semi: "off"
no-template-curly-in-string: error
require-atomic-updates: error

@@ -31,7 +31,7 @@ rules:
grouped-accessor-pairs: error
no-alert: error
no-caller: error
no-case-declarations: 'off'
no-case-declarations: "off"
no-constructor-return: error
no-else-return: error
no-eval: error
@@ -74,7 +74,7 @@ rules:
no-label-var: error
no-shadow: error
no-undef-init: error
no-unused-vars: 'off'
no-unused-vars: "off"

# Stylistic issues.
array-bracket-newline: [error, consistent]
@@ -99,7 +99,7 @@ rules:
no-mixed-operators: error
no-multiple-empty-lines: error
no-new-object: error
no-tabs: error
no-tabs: [error, {allowIndentationTabs: true}]
no-trailing-spaces: error
no-unneeded-ternary: error
no-whitespace-before-property: error
@@ -126,10 +126,10 @@ rules:
arrow-parens: [error, as-needed]
arrow-spacing: error
generator-star-spacing: error
no-dupe-class-members: 'off'
no-dupe-class-members: "off"
no-duplicate-imports: error
no-useless-computed-key: error
no-useless-constructor: 'off'
no-useless-constructor: "off"
no-useless-rename: error
no-var: error
object-shorthand: error
@@ -146,57 +146,57 @@ rules:
yield-star-spacing: error

# TypeScript: supported rules.
'@typescript-eslint/array-type': [error, {default: array-simple}]
'@typescript-eslint/camelcase': 'off'
'@typescript-eslint/class-literal-property-style': error
'@typescript-eslint/explicit-function-return-type': [error, {allowExpressions: true}]
'@typescript-eslint/explicit-member-accessibility': [error, {accessibility: no-public}]
'@typescript-eslint/member-naming': [error, {private: '^_', protected: '^_'}]
'@typescript-eslint/member-ordering': [error , {default: [public-field, protected-field, private-field, constructor, public-method, protected-method, private-method]}]
'@typescript-eslint/method-signature-style': error
'@typescript-eslint/no-dynamic-delete': error
'@typescript-eslint/no-extraneous-class': error
'@typescript-eslint/no-explicit-any': 'off'
'@typescript-eslint/no-floating-promises': error
'@typescript-eslint/no-implied-eval': error
'@typescript-eslint/no-inferrable-types': [error, {ignoreParameters: true, ignoreProperties: true}]
'@typescript-eslint/no-invalid-void-type': error
'@typescript-eslint/no-non-null-assertion': 'off'
'@typescript-eslint/no-require-imports': error
'@typescript-eslint/no-throw-literal': error
'@typescript-eslint/no-unnecessary-boolean-literal-compare': error
'@typescript-eslint/no-unnecessary-condition': error
'@typescript-eslint/no-unnecessary-qualifier': error
'@typescript-eslint/no-unnecessary-type-arguments': error
'@typescript-eslint/no-unused-vars-experimental': error
'@typescript-eslint/prefer-as-const': error
'@typescript-eslint/prefer-for-of': error
'@typescript-eslint/prefer-function-type': error
'@typescript-eslint/prefer-nullish-coalescing': error
'@typescript-eslint/prefer-optional-chain': error
'@typescript-eslint/prefer-readonly': error
'@typescript-eslint/prefer-reduce-type-parameter': error
'@typescript-eslint/require-array-sort-compare': error
'@typescript-eslint/restrict-plus-operands': error
'@typescript-eslint/switch-exhaustiveness-check': error
'@typescript-eslint/unbound-method': [error, {ignoreStatic: true}]
'@typescript-eslint/unified-signatures': error
"@typescript-eslint/array-type": [error, {default: array-simple}]
"@typescript-eslint/camelcase": "off"
"@typescript-eslint/class-literal-property-style": error
"@typescript-eslint/explicit-function-return-type": [error, {allowExpressions: true}]
"@typescript-eslint/explicit-member-accessibility": [error, {accessibility: no-public}]
"@typescript-eslint/member-ordering": [error , {default: [public-field, protected-field, private-field, constructor, public-method, protected-method, private-method]}]
"@typescript-eslint/method-signature-style": error
"@typescript-eslint/naming-convention": error
"@typescript-eslint/no-dynamic-delete": error
"@typescript-eslint/no-extraneous-class": error
"@typescript-eslint/no-explicit-any": "off"
"@typescript-eslint/no-floating-promises": error
"@typescript-eslint/no-implied-eval": error
"@typescript-eslint/no-inferrable-types": [error, {ignoreParameters: true, ignoreProperties: true}]
"@typescript-eslint/no-invalid-void-type": error
"@typescript-eslint/no-non-null-assertion": "off"
"@typescript-eslint/no-require-imports": error
"@typescript-eslint/no-throw-literal": error
"@typescript-eslint/no-unnecessary-boolean-literal-compare": error
"@typescript-eslint/no-unnecessary-condition": error
"@typescript-eslint/no-unnecessary-qualifier": error
"@typescript-eslint/no-unnecessary-type-arguments": error
"@typescript-eslint/no-unused-vars-experimental": error
"@typescript-eslint/prefer-as-const": error
"@typescript-eslint/prefer-for-of": error
"@typescript-eslint/prefer-function-type": error
"@typescript-eslint/prefer-nullish-coalescing": error
"@typescript-eslint/prefer-optional-chain": error
"@typescript-eslint/prefer-readonly": error
"@typescript-eslint/prefer-reduce-type-parameter": error
"@typescript-eslint/require-array-sort-compare": error
"@typescript-eslint/restrict-plus-operands": error
"@typescript-eslint/switch-exhaustiveness-check": error
"@typescript-eslint/unbound-method": [error, {ignoreStatic: true}]
"@typescript-eslint/unified-signatures": error

# TypeScript: extension rules.
'@typescript-eslint/brace-style': [error, stroustrup, {allowSingleLine: true}]
'@typescript-eslint/comma-spacing': error
'@typescript-eslint/default-param-last': error
'@typescript-eslint/dot-notation': error
'@typescript-eslint/func-call-spacing': error
'@typescript-eslint/indent': [error, 2, {SwitchCase: 1}]
'@typescript-eslint/keyword-spacing': error
'@typescript-eslint/lines-between-class-members': error
'@typescript-eslint/no-dupe-class-members': error
'@typescript-eslint/no-extra-semi': error
'@typescript-eslint/no-invalid-this': error
'@typescript-eslint/no-use-before-define': [error, nofunc]
'@typescript-eslint/no-useless-constructor': error
'@typescript-eslint/quotes': [error, single, {avoidEscape: true}]
'@typescript-eslint/return-await': error
'@typescript-eslint/semi': error
'@typescript-eslint/space-before-function-paren': [error, {anonymous: never, asyncArrow: always, named: never}]
"@typescript-eslint/brace-style": [error, stroustrup, {allowSingleLine: true}]
"@typescript-eslint/comma-spacing": error
"@typescript-eslint/default-param-last": error
"@typescript-eslint/dot-notation": error
"@typescript-eslint/func-call-spacing": error
"@typescript-eslint/indent": [error, tab, {SwitchCase: 1}]
"@typescript-eslint/keyword-spacing": error
"@typescript-eslint/lines-between-class-members": error
"@typescript-eslint/no-dupe-class-members": error
"@typescript-eslint/no-extra-semi": error
"@typescript-eslint/no-invalid-this": error
"@typescript-eslint/no-use-before-define": [error, nofunc]
"@typescript-eslint/no-useless-constructor": error
"@typescript-eslint/quotes": [error, double, {avoidEscape: true}]
"@typescript-eslint/return-await": error
"@typescript-eslint/semi": error
"@typescript-eslint/space-before-function-paren": [error, {anonymous: never, asyncArrow: always, named: never}]

+ 14
- 14
etc/karma.cjs View File

@@ -1,16 +1,16 @@
module.exports = config => config.set({
basePath: '..',
browsers: ['FirefoxHeadless'],
client: {
clearContext: false
},
coverageIstanbulReporter: {
dir: 'var',
fixWebpackSourcePaths: true,
reports: ['lcovonly']
},
frameworks: ['mocha', 'chai', '@angular-devkit/build-angular'],
plugins: ['karma-*', '@angular-devkit/build-angular/plugins/karma'],
reporters: ['progress'],
singleRun: true
basePath: "..",
browsers: ["FirefoxHeadless"],
client: {
clearContext: false
},
coverageIstanbulReporter: {
dir: "var",
fixWebpackSourcePaths: true,
reports: ["lcovonly"]
},
frameworks: ["mocha", "chai", "@angular-devkit/build-angular"],
plugins: ["karma-*", "@angular-devkit/build-angular/plugins/karma"],
reporters: ["progress"],
singleRun: true
});

+ 9
- 5
etc/mkdocs.yaml View File

@@ -8,19 +8,23 @@ site_dir: ../www

repo_name: git.belin.io
repo_url: https://git.belin.io/cedx/ngx-webstorage
edit_uri: ''
edit_uri: ""

copyright: Copyright &copy; 2017 - 2020 Cédric Belin
extra:
social:
- icon: fontawesome/solid/globe
link: 'https://belin.io'
link: "https://belin.io"
name: Belin.io
- icon: fontawesome/brands/github
link: 'https://github.com/cedx'
link: "https://github.com/cedx"
name: GitHub
- icon: fontawesome/brands/twitter
link: 'https://twitter.com/cedxbelin'
link: "https://twitter.com/cedxbelin"
name: Twitter
- icon: fontawesome/brands/linkedin
link: 'https://linkedin.com/in/cedxbelin'
link: "https://linkedin.com/in/cedxbelin"
name: LinkedIn

markdown_extensions:
- admonition


+ 43
- 43
example/main.ts View File

@@ -1,49 +1,49 @@
import {Component, OnInit} from '@angular/core';
import {LocalStorage} from '@cedx/ngx-webstorage';
import {Component, OnInit} from "@angular/core";
import {LocalStorage} from "@cedx/ngx-webstorage";

/** A component that demonstrates the usage of the `LocalStorage` service. */
@Component({
selector: 'my-component',
templateUrl: './my-component.html'
selector: "my-component",
templateUrl: "./my-component.html"
})
export class MyComponent implements OnInit {
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
// Query the storage.
console.log(this._storage.has('foo')); // false
console.log(this._storage.has('baz')); // false
console.log(this._storage.length); // 0
console.log(this._storage.keys); // []
// Write to the storage.
this._storage.set('foo', 'bar');
console.log(this._storage.has('foo')); // true
console.log(this._storage.length); // 1
console.log(this._storage.keys); // ["foo"]
this._storage.setObject('baz', {qux: 123});
console.log(this._storage.has('baz')); // true
console.log(this._storage.length); // 2
console.log(this._storage.keys); // ["foo", "baz"]
// Read the storage.
console.log(this._storage.get('foo').constructor.name); // "String"
console.log(this._storage.get('foo')); // "bar"
console.log(this._storage.getObject('baz').constructor.name); // "Object"
console.log(this._storage.getObject('baz')); // {qux: 123}
console.log(this._storage.getObject('baz').qux); // 123
// Delete from the storage.
this._storage.remove('foo');
console.log(this._storage.has('foo')); // false
console.log(this._storage.length); // 1
console.log(this._storage.keys); // ["baz"]
this._storage.clear();
console.log(this._storage.has('baz')); // false
console.log(this._storage.length); // 0
console.log(this._storage.keys); // []
}
constructor(private _storage: LocalStorage) {}
ngOnInit(): void {
// Query the storage.
console.log(this._storage.has("foo")); // false
console.log(this._storage.has("baz")); // false
console.log(this._storage.length); // 0
console.log(this._storage.keys); // []
// Write to the storage.
this._storage.set("foo", "bar");
console.log(this._storage.has("foo")); // true
console.log(this._storage.length); // 1
console.log(this._storage.keys); // ["foo"]
this._storage.setObject("baz", {qux: 123});
console.log(this._storage.has("baz")); // true
console.log(this._storage.length); // 2
console.log(this._storage.keys); // ["foo", "baz"]
// Read the storage.
console.log(this._storage.get("foo").constructor.name); // "String"
console.log(this._storage.get("foo")); // "bar"
console.log(this._storage.getObject("baz").constructor.name); // "Object"
console.log(this._storage.getObject("baz")); // {qux: 123}
console.log(this._storage.getObject("baz").qux); // 123
// Delete from the storage.
this._storage.remove("foo");
console.log(this._storage.has("foo")); // false
console.log(this._storage.length); // 1
console.log(this._storage.keys); // ["baz"]
this._storage.clear();
console.log(this._storage.has("baz")); // false
console.log(this._storage.length); // 0
console.log(this._storage.keys); // []
}
}

+ 7
- 7
ng-package.json View File

@@ -1,9 +1,9 @@
{
"$schema": "./node_modules/ng-packagr/ng-package.schema.json",
"dest": "build",
"lib": {
"entryFile": "src/index.ts",
"flatModuleFile": "ngx-webstorage",
"umdId": "ngx.webstorage"
}
"$schema": "./node_modules/ng-packagr/ng-package.schema.json",
"dest": "build",
"lib": {
"entryFile": "src/index.ts",
"flatModuleFile": "ngx-webstorage",
"umdId": "ngx.webstorage"
}
}

+ 83
- 83
package.json View File

@@ -1,85 +1,85 @@
{
"bugs": "https://git.belin.io/cedx/ngx-webstorage/issues",
"description": "Angular services for interacting with the Web Storage.",
"homepage": "https://docs.belin.io/ngx-webstorage",
"license": "MIT",
"main": "lib/index.js",
"name": "@cedx/ngx-webstorage",
"type": "module",
"types": "lib/index.d.ts",
"version": "4.1.0",
"author": {
"email": "cedric@belin.io",
"name": "Cédric Belin",
"url": "https://belin.io"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.901.6",
"@angular-devkit/build-ng-packagr": "^0.901.6",
"@angular/cli": "^9.1.6",
"@angular/common": "^9.1.7",
"@angular/compiler": "^9.1.7",
"@angular/compiler-cli": "^9.1.7",
"@angular/core": "^9.1.7",
"@angular/language-service": "^9.1.7",
"@angular/platform-browser": "^9.1.7",
"@angular/platform-browser-dynamic": "^9.1.7",
"@cedx/coveralls": "^10.0.0",
"@compodoc/compodoc": "^1.1.11",
"@types/chai": "^4.2.11",
"@types/mocha": "^7.0.2",
"@types/node": "^14.0.1",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"chai": "^4.2.0",
"eslint": "^7.0.0",
"karma": "^5.0.5",
"karma-chai": "^0.1.0",
"karma-coverage-istanbul-reporter": "^3.0.2",
"karma-firefox-launcher": "^1.3.0",
"karma-mocha": "^2.0.1",
"mocha": "^7.1.2",
"ng-packagr": "^9.1.3",
"rxjs": "^6.5.5",
"tslib": "^2.0.0",
"typescript": "^3.9.2",
"zone.js": "^0.10.3"
},
"engines": {
"node": ">=14.2.0"
},
"files": [
"lib/"
],
"funding": {
"type": "patreon",
"url": "https://www.patreon.com/cedx"
},
"keywords": [
"angular",
"esm",
"local",
"ngx",
"observable",
"rxjs",
"service",
"session",
"storage"
],
"peerDependencies": {
"@angular/common": ">=9.1.0",
"@angular/core": ">=9.1.0",
"rxjs": ">=6.5.0",
"tslib": ">=1.11.0",
"zone.js": ">=0.10.0"
},
"repository": {
"type": "git",
"url": "https://git.belin.io/cedx/ngx-webstorage.git"
},
"scripts": {
"coverage": "coveralls var/lcov.info",
"prepack": "pwsh tool/build.ps1",
"test": "ng test"
}
"bugs": "https://git.belin.io/cedx/ngx-webstorage/issues",
"description": "Angular services for interacting with the Web Storage.",
"homepage": "https://docs.belin.io/ngx-webstorage",
"license": "MIT",
"main": "./lib/index.js",
"name": "@cedx/ngx-webstorage",
"type": "module",
"types": "./lib/index.d.ts",
"version": "4.1.0",
"author": {
"email": "cedric@belin.io",
"name": "Cédric Belin",
"url": "https://belin.io"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.901.7",
"@angular-devkit/build-ng-packagr": "^0.901.7",
"@angular/cli": "^9.1.7",
"@angular/common": "^9.1.9",
"@angular/compiler": "^9.1.9",
"@angular/compiler-cli": "^9.1.9",
"@angular/core": "^9.1.9",
"@angular/language-service": "^9.1.9",
"@angular/platform-browser": "^9.1.9",
"@angular/platform-browser-dynamic": "^9.1.9",
"@cedx/coveralls": "^10.1.0",
"@compodoc/compodoc": "^1.1.11",
"@types/chai": "^4.2.11",
"@types/mocha": "^7.0.2",
"@types/node": "^14.0.5",
"@typescript-eslint/eslint-plugin": "^3.0.2",
"@typescript-eslint/parser": "^3.0.2",
"chai": "^4.2.0",
"eslint": "^7.1.0",
"karma": "^5.0.9",
"karma-chai": "^0.1.0",
"karma-coverage-istanbul-reporter": "^3.0.2",
"karma-firefox-launcher": "^1.3.0",
"karma-mocha": "^2.0.1",
"mocha": "^7.2.0",
"ng-packagr": "^9.1.5",
"rxjs": "^6.5.5",
"tslib": "^2.0.0",
"typescript": "^3.9.3",
"zone.js": "^0.10.3"
},
"engines": {
"node": ">=14.3.0"
},
"files": [
"lib/"
],
"funding": {
"type": "patreon",
"url": "https://www.patreon.com/cedx"
},
"keywords": [
"angular",
"esm",
"local",
"ngx",
"observable",
"rxjs",
"service",
"session",
"storage"
],
"peerDependencies": {
"@angular/common": ">=9.1.0",
"@angular/core": ">=9.1.0",
"rxjs": ">=6.5.0",
"tslib": ">=1.11.0",
"zone.js": ">=0.10.0"
},
"repository": {
"type": "git",
"url": "https://git.belin.io/cedx/ngx-webstorage.git"
},
"scripts": {
"coverage": "coveralls var/lcov.info",
"prepack": "pwsh tool/build.ps1",
"test": "ng test"
}
}

+ 1
- 1
src/index.ts View File

@@ -1 +1 @@
export * from './storage';
export * from "./storage";

+ 189
- 189
src/storage.ts View File

@@ -1,204 +1,204 @@
import {Injectable, OnDestroy, SimpleChange, SimpleChanges} from '@angular/core';
import {fromEvent, Observable, Subject, Subscription} from 'rxjs';
import {Injectable, OnDestroy, SimpleChange, SimpleChanges} from "@angular/core";
import {fromEvent, Observable, Subject, Subscription} from "rxjs";

/** Provides access to the [Web Storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage). */
export abstract class WebStorage implements Iterable<[string, string|undefined]>, OnDestroy {

/** The handler of "changes" events. */
private readonly _onChanges: Subject<SimpleChanges> = new Subject<SimpleChanges>();
/** The subscription to the storage events. */
private readonly _subscription: Subscription;
/**
* Creates a new storage service.
* @param _backend The underlying data store.
*/
protected constructor(private readonly _backend: Storage) {
this._subscription = fromEvent<StorageEvent>(window, 'storage').subscribe(event => {
if (event.key == null || event.storageArea != this._backend) return;
this._onChanges.next({
[event.key]: new SimpleChange(event.oldValue ?? undefined, event.newValue ?? undefined, false)
});
});
}
/** The keys of this storage. */
get keys(): string[] {
const keys = [];
for (let i = 0; ; i++) { // eslint-disable-line no-constant-condition
const key = this._backend.key(i);
if (key == null) return keys;
keys.push(key);
}
}
/** The number of entries in this storage. */
get length(): number {
return this._backend.length;
}
/** The stream of "changes" events. */
get onChanges(): Observable<SimpleChanges> {
return this._onChanges.asObservable();
}
/**
* Returns a new iterator that allows iterating the entries of this storage.
* @return An iterator for the entries of this storage.
*/
*[Symbol.iterator](): IterableIterator<[string, string|undefined]> {
for (const key of this.keys) yield [key, this.get(key)];
}
/** Removes all entries from this storage. */
clear(): void {
const changes: SimpleChanges = {};
for (const [key, value] of this) changes[key] = new SimpleChange(value, undefined, false);
this._backend.clear();
this._onChanges.next(changes);
}
/**
* Gets the value associated to the specified key.
* @param key The key to seek for.
* @param defaultValue The value to return if the item does not exist.
* @return The value of the storage item, or the default value if the item is not found.
*/
get(key: string, defaultValue?: string): string|undefined {
return this._backend.getItem(key) ?? defaultValue;
}
/**
* Gets the deserialized value associated to the specified key.
* @param key The key to seek for.
* @param defaultValue The value to return if the item does not exist.
* @return The deserialized value of the storage item, or the default value if the item is not found.
*/
getObject(key: string, defaultValue?: any): any {
try {
const value = this.get(key);
return value != undefined ? JSON.parse(value) : defaultValue;
}
catch {
return defaultValue;
}
}
/**
* Gets a value indicating whether this storage contains the specified key.
* @param key The key to seek for.
* @return `true` if this storage contains the specified key, otherwise `false`.
*/
has(key: string): boolean {
return this.keys.includes(key);
}
/** Method invoked before the service is destroyed. */
ngOnDestroy(): void {
this._subscription.unsubscribe();
this._onChanges.complete();
}
/**
* Looks up the value of the specified key, or add a new value if it isn't there.
*
* Returns the value associated to `key`, if there is one. Otherwise calls `ifAbsent` to get a new value,
* associates `key` to that value, and then returns the new value.
*
* @param key The key to seek for.
* @param ifAbsent The function called to get a new value.
* @return The value associated with the specified key.
*/
putIfAbsent(key: string, ifAbsent: () => string): string {
if (!this.has(key)) this.set(key, ifAbsent());
return this.get(key)!;
}
/**
* Looks up the value of the specified key, or add a new value if it isn't there.
*
* Returns the deserialized value associated to `key`, if there is one. Otherwise calls `ifAbsent` to get a new value,
* serializes and associates `key` to that value, and then returns the new value.
*
* @param key The key to seek for.
* @param ifAbsent The function called to get a new value.
* @return The deserialized value associated with the specified key.
*/
putObjectIfAbsent(key: string, ifAbsent: () => any): any {
if (!this.has(key)) this.setObject(key, ifAbsent());
return this.getObject(key);
}
/**
* Removes the value associated to the specified key.
* @param key The key to seek for.
* @return The value associated with the specified key before it was removed.
*/
remove(key: string): string|undefined {
const previousValue = this.get(key);
this._backend.removeItem(key);
this._onChanges.next({
[key]: new SimpleChange(previousValue, undefined, false)
});
return previousValue;
}
/**
* Associates a given value to the specified key.
* @param key The key to seek for.
* @param value The item value.
* @return This instance.
*/
set(key: string, value: string): this {
const previousValue = this.get(key);
this._backend.setItem(key, value);
this._onChanges.next({
[key]: new SimpleChange(previousValue, value, false)
});
return this;
}
/**
* Serializes and associates a given value to the specified key.
* @param key The key to seek for.
* @param value The item value.
* @return This instance.
*/
setObject(key: string, value: any): this {
return this.set(key, JSON.stringify(value));
}
/**
* Converts this object to a map in JSON format.
* @return The map in JSON format corresponding to this object.
*/
toJSON(): Record<string, any> {
const map: Record<string, any> = {};
for (const [key, value] of this) map[key] = value ?? null;
return map;
}
/** The handler of "changes" events. */
private readonly _onChanges: Subject<SimpleChanges> = new Subject<SimpleChanges>();
/** The subscription to the storage events. */
private readonly _subscription: Subscription;
/**
* Creates a new storage service.
* @param _backend The underlying data store.
*/
protected constructor(private readonly _backend: Storage) {
this._subscription = fromEvent<StorageEvent>(window, "storage").subscribe(event => {
if (event.key == null || event.storageArea != this._backend) return;
this._onChanges.next({
[event.key]: new SimpleChange(event.oldValue ?? undefined, event.newValue ?? undefined, false)
});
});
}
/** The keys of this storage. */
get keys(): string[] {
const keys = [];
for (let i = 0; ; i++) { // eslint-disable-line no-constant-condition
const key = this._backend.key(i);
if (key == null) return keys;
keys.push(key);
}
}
/** The number of entries in this storage. */
get length(): number {
return this._backend.length;
}
/** The stream of "changes" events. */
get onChanges(): Observable<SimpleChanges> {
return this._onChanges.asObservable();
}
/**
* Returns a new iterator that allows iterating the entries of this storage.
* @return An iterator for the entries of this storage.
*/
*[Symbol.iterator](): IterableIterator<[string, string|undefined]> {
for (const key of this.keys) yield [key, this.get(key)];
}
/** Removes all entries from this storage. */
clear(): void {
const changes: SimpleChanges = {};
for (const [key, value] of this) changes[key] = new SimpleChange(value, undefined, false);
this._backend.clear();
this._onChanges.next(changes);
}
/**
* Gets the value associated to the specified key.
* @param key The key to seek for.
* @param defaultValue The value to return if the item does not exist.
* @return The value of the storage item, or the default value if the item is not found.
*/
get(key: string, defaultValue?: string): string|undefined {
return this._backend.getItem(key) ?? defaultValue;
}
/**
* Gets the deserialized value associated to the specified key.
* @param key The key to seek for.
* @param defaultValue The value to return if the item does not exist.
* @return The deserialized value of the storage item, or the default value if the item is not found.
*/
getObject(key: string, defaultValue?: any): any {
try {
const value = this.get(key);
return value != undefined ? JSON.parse(value) : defaultValue;
}
catch {
return defaultValue;
}
}
/**
* Gets a value indicating whether this storage contains the specified key.
* @param key The key to seek for.
* @return `true` if this storage contains the specified key, otherwise `false`.
*/
has(key: string): boolean {
return this.keys.includes(key);
}
/** Method invoked before the service is destroyed. */
ngOnDestroy(): void {
this._subscription.unsubscribe();
this._onChanges.complete();
}
/**
* Looks up the value of the specified key, or add a new value if it isn't there.
*
* Returns the value associated to `key`, if there is one. Otherwise calls `ifAbsent` to get a new value,
* associates `key` to that value, and then returns the new value.
*
* @param key The key to seek for.
* @param ifAbsent The function called to get a new value.
* @return The value associated with the specified key.
*/
putIfAbsent(key: string, ifAbsent: () => string): string {
if (!this.has(key)) this.set(key, ifAbsent());
return this.get(key)!;
}
/**
* Looks up the value of the specified key, or add a new value if it isn't there.
*
* Returns the deserialized value associated to `key`, if there is one. Otherwise calls `ifAbsent` to get a new value,
* serializes and associates `key` to that value, and then returns the new value.
*
* @param key The key to seek for.
* @param ifAbsent The function called to get a new value.
* @return The deserialized value associated with the specified key.
*/
putObjectIfAbsent(key: string, ifAbsent: () => any): any {
if (!this.has(key)) this.setObject(key, ifAbsent());
return this.getObject(key);
}
/**
* Removes the value associated to the specified key.
* @param key The key to seek for.
* @return The value associated with the specified key before it was removed.
*/
remove(key: string): string|undefined {
const previousValue = this.get(key);
this._backend.removeItem(key);
this._onChanges.next({
[key]: new SimpleChange(previousValue, undefined, false)
});
return previousValue;
}
/**
* Associates a given value to the specified key.
* @param key The key to seek for.
* @param value The item value.
* @return This instance.
*/
set(key: string, value: string): this {
const previousValue = this.get(key);
this._backend.setItem(key, value);
this._onChanges.next({
[key]: new SimpleChange(previousValue, value, false)
});
return this;
}
/**
* Serializes and associates a given value to the specified key.
* @param key The key to seek for.
* @param value The item value.
* @return This instance.
*/
setObject(key: string, value: any): this {
return this.set(key, JSON.stringify(value));
}
/**
* Converts this object to a map in JSON format.
* @return The map in JSON format corresponding to this object.
*/
toJSON(): Record<string, any> {
const map: Record<string, any> = {};
for (const [key, value] of this) map[key] = value ?? null;
return map;
}
}

/** Provides access to the local storage. */
@Injectable({providedIn: 'root'})
@Injectable({providedIn: "root"})
export class LocalStorage extends WebStorage {

/** Creates a new storage service. */
constructor() {
super(localStorage);
}
/** Creates a new storage service. */
constructor() {
super(localStorage);
}
}

/** Provides access to the session storage. */
@Injectable({providedIn: 'root'})
@Injectable({providedIn: "root"})
export class SessionStorage extends WebStorage {

/** Creates a new storage service. */
constructor() {
super(sessionStorage);
}
/** Creates a new storage service. */
constructor() {
super(sessionStorage);
}
}

+ 19
- 19
src/tsconfig.json View File

@@ -1,21 +1,21 @@
{
"include": ["**/*.ts"],
"angularCompilerOptions": {
"disableTypeScriptVersionCheck": true,
"enableResourceInlining": true,
"skipTemplateCodegen": true,
"strictInjectionParameters": true,
"strictMetadataEmit": true,
"strictTemplates": true
},
"compilerOptions": {
"baseUrl": "..",
"declaration": true,
"experimentalDecorators": true,
"importHelpers": true,
"module": "es2015",
"moduleResolution": "node",
"strict": true,
"target": "es2019"
}
"include": ["**/*.ts"],
"angularCompilerOptions": {
"disableTypeScriptVersionCheck": true,
"enableResourceInlining": true,
"skipTemplateCodegen": true,
"strictInjectionParameters": true,
"strictMetadataEmit": true,
"strictTemplates": true
},
"compilerOptions": {
"baseUrl": "..",
"declaration": true,
"experimentalDecorators": true,
"importHelpers": true,
"module": "es2015",
"moduleResolution": "node",
"strict": true,
"target": "es2019"
}
}

+ 5
- 5
test/main.ts View File

@@ -1,12 +1,12 @@
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import {getTestBed} from '@angular/core/testing';
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing';
import "zone.js/dist/zone";
import "zone.js/dist/zone-testing";
import {getTestBed} from "@angular/core/testing";
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from "@angular/platform-browser-dynamic/testing";

// Initialize the testing environment.
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());

// Find all the tests and load the modules.
declare const require: any;
const context = require.context('.', true, /_test\.ts$/);
const context = require.context(".", true, /_test\.ts$/);
context.keys().map(context);

+ 270
- 270
test/storage_test.ts View File

@@ -1,273 +1,273 @@
import {SessionStorage} from '../src/index';
import {SessionStorage} from "../src/index";

/** Tests the features of the `WebStorage` class. */
describe('WebStorage', () => {
const {expect} = chai;
beforeEach(() => sessionStorage.clear());
describe('.keys', () => {
it('should return an empty array for an empty storage', () => {
expect(new SessionStorage().keys).to.be.empty;
});
it('should return the list of keys for a non-empty storage', () => {
sessionStorage.setItem('foo', 'bar');
sessionStorage.setItem('bar', 'baz');
expect(new SessionStorage().keys).to.have.ordered.members(['foo', 'bar']);
});
});
describe('.length', () => {
it('should return zero for an empty storage', () => {
expect(new SessionStorage()).to.have.lengthOf(0);
});
it('should return the number of entries for a non-empty storage', () => {
sessionStorage.setItem('foo', 'bar');
sessionStorage.setItem('bar', 'baz');
expect(new SessionStorage()).to.have.lengthOf(2);
});
});
describe('.onChanges', () => {
it('should trigger an event when a value is added', done => {
const storage = new SessionStorage;
const subscription = storage.onChanges.subscribe(changes => {
expect(Object.keys(changes)).to.have.members(['foo']);
expect(changes.foo.currentValue).to.equal('bar');
expect(changes.foo.previousValue).to.be.undefined;
done();
}, done);
storage.set('foo', 'bar');
subscription.unsubscribe();
});
it('should trigger an event when a value is updated', done => {
const storage = new SessionStorage;
sessionStorage.setItem('foo', 'bar');
const subscription = storage.onChanges.subscribe(changes => {
expect(Object.keys(changes)).to.have.members(['foo']);
expect(changes.foo.currentValue).to.equal('baz');
expect(changes.foo.previousValue).to.equal('bar');
done();
}, done);
storage.set('foo', 'baz');
subscription.unsubscribe();
});
it('should trigger an event when a value is removed', done => {
const storage = new SessionStorage;
sessionStorage.setItem('foo', 'bar');
const subscription = storage.onChanges.subscribe(changes => {
expect(Object.keys(changes)).to.have.members(['foo']);
expect(changes.foo.currentValue).to.be.undefined;
expect(changes.foo.previousValue).to.equal('bar');
done();
}, done);
storage.remove('foo');
subscription.unsubscribe();
});
it('should trigger an event when the storage is cleared', done => {
const storage = new SessionStorage;
sessionStorage.setItem('foo', 'bar');
sessionStorage.setItem('bar', 'baz');
const subscription = storage.onChanges.subscribe(changes => {
expect(Object.keys(changes)).to.have.ordered.members(['foo', 'bar']);
expect(changes.foo.currentValue).to.be.undefined;
expect(changes.foo.previousValue).to.equal('bar');
expect(changes.bar.currentValue).to.be.undefined;
expect(changes.bar.previousValue).to.equal('baz');
done();
}, done);
storage.clear();
subscription.unsubscribe();
});
});
describe('.[Symbol.iterator]()', () => {
it('should return a done iterator if storage is empty', () => {
const storage = new SessionStorage;
const iterator = storage[Symbol.iterator]();
expect(iterator.next().done).to.be.true;
});
it('should return a value iterator if storage is not empty', () => {
const storage = new SessionStorage;
sessionStorage.setItem('foo', 'bar');
sessionStorage.setItem('bar', 'baz');
const iterator = storage[Symbol.iterator]();
const values = [];
let next = iterator.next();
expect(next.done).to.be.false;
values.push(next.value);
next = iterator.next();
expect(next.done).to.be.false;
values.push(next.value);
expect(iterator.next().done).to.be.true;
expect(values).to.have.lengthOf(2);
expect(values[0]).to.have.ordered.members(['foo', 'bar']);
expect(values[1]).to.have.ordered.members(['bar', 'baz']);
});
});
describe('.clear()', () => {
it('should remove all storage entries', () => {
sessionStorage.setItem('foo', 'bar');
sessionStorage.setItem('bar', 'baz');
const storage = new SessionStorage;
expect(storage).to.have.lengthOf(2);
storage.clear();
expect(storage).to.have.lengthOf(0);
});
});
describe('.get()', () => {
it('should properly get the storage entries', () => {
const storage = new SessionStorage;
expect(storage.get('foo')).to.be.undefined;
expect(storage.get('foo', '123')).to.equal('123');
sessionStorage.setItem('foo', 'bar');
expect(storage.get('foo')).to.equal('bar');
sessionStorage.setItem('foo', '123');
expect(storage.get('foo')).to.equal('123');
});
it('should return the given default value if the key is not found', () => {
expect(new SessionStorage().get('bar', '123')).to.equal('123');
});
});
describe('.getObject()', () => {
it('should properly get the deserialized storage entries', () => {
const storage = new SessionStorage;
expect(storage.getObject('foo')).to.be.undefined;
expect(storage.getObject('foo', {key: 'value'})).to.be.an('object').that.deep.equal({key: 'value'});
sessionStorage.setItem('foo', '123');
expect(storage.getObject('foo')).to.equal(123);
sessionStorage.setItem('foo', '"bar"');
expect(storage.getObject('foo')).to.equal('bar');
sessionStorage.setItem('foo', '{"key": "value"}');
expect(storage.getObject('foo')).to.be.an('object').that.deep.equal({key: 'value'});
});
it('should return the default value if the value can\'t be deserialized', () => {
sessionStorage.setItem('foo', 'bar');