Comparing version 8.1.2 to 8.1.3
@@ -5,2 +5,10 @@ # Changelog | ||
### [8.1.3](https://github.com/hybridsjs/hybrids/compare/v8.1.2...v8.1.3) (2022-09-16) | ||
### Bug Fixes | ||
* **store:** empty nested array with primitive values ([8df84d2](https://github.com/hybridsjs/hybrids/commit/8df84d215e1d680342796bb075ef406bfffbd9ee)) | ||
* **store:** resolve with support for using model definition ([95aa812](https://github.com/hybridsjs/hybrids/commit/95aa812b80f6c90fa14c5a4f0017b30fd37bbd3e)) | ||
### [8.1.2](https://github.com/hybridsjs/hybrids/compare/v8.1.1...v8.1.2) (2022-09-14) | ||
@@ -7,0 +15,0 @@ |
@@ -202,2 +202,21 @@ # API Reference | ||
```typescript | ||
store.resolve(model: Model): Promise<Model> | ||
``` | ||
- **arguments**: | ||
- `model` - a model instance | ||
- **returns**: | ||
- A promise instance resolving with the latest model value or rejecting with an error | ||
```typescript | ||
store.resolve(Model: object, id?: string | object): Promise<Model> | ||
``` | ||
- **arguments**: | ||
- `Model` - a model definition | ||
- `id` - a string or an object representing identifier of the model instance | ||
- **returns**: | ||
- A promise instance resolving with the latest model value or rejecting with an error | ||
```typescript | ||
store.sync(modelOrDefinition: object, values: object | null) : Model; | ||
@@ -297,13 +316,2 @@ ``` | ||
### Resolve | ||
```typescript | ||
store.resolve(model: Model): Promise<Model> | ||
``` | ||
- **arguments**: | ||
- `model` - a model instance | ||
- **returns**: | ||
- A promise instance resolving with the latest model value or rejecting with an error | ||
## Router | ||
@@ -310,0 +318,0 @@ |
@@ -241,2 +241,13 @@ # Model | ||
If the nested array must be empty as a default value, you can use primitive constructor as the first item of the array: | ||
```javascript | ||
const Model = { | ||
domains: [String], | ||
numbers: [Number], | ||
} | ||
store.get(Model); // { domains: [], numbers: [] } | ||
``` | ||
#### Models (External) | ||
@@ -243,0 +254,0 @@ |
@@ -220,38 +220,2 @@ # Storage | ||
#### Invalidation | ||
Both memory and external storage uses a global cache mechanism based on the model definition reference. Model instances are global, so the cache mechanism cannot automatically predict which instance is no longer required. Because of that, the store provides the `store.clear()` method for invalidating model instances by the model definition or specific instance of the model. | ||
```typescript | ||
store.clear(model: object, clearValue?: boolean = true) | ||
``` | ||
* **arguments**: | ||
* `model` - a model definition (for all instances) or a model instance (for a specific one) | ||
* `clearValue` - indicates if the cached value should be deleted (`true`), or it should only notify the cache mechanism, that the value expired, but leaves the value untouched (`false`) | ||
For example, it might be useful to set the `clearValue` to `false` for the case when you want to implement the refresh button. Then, the values stay in the cache, but the store will fetch the next version of the instance. | ||
```javascript | ||
import Email from "./email.js"; | ||
function refresh(host) { | ||
store.clear(host.emails, false); | ||
} | ||
define({ | ||
tag: "my-element", | ||
emails: store([Email]), | ||
render: ({ emails }) => html` | ||
<button onclick="${refresh}">Refresh</button> | ||
${store.ready(emails) && ...} | ||
`, | ||
}); | ||
``` | ||
#### Garbage Collector | ||
The `store.clear()` method works as a garbage collector for unused model instances. Those that are not a dependency of any component connected to the DOM will be deleted entirely from the cache registry (as they would never exist) protecting from the memory leaks. It means, that even if you set `clearValue` to `false`, those instances that are not currently attached to the components, will be permanently deleted when the `store.clear()` method is invoked. | ||
### offline | ||
@@ -313,43 +277,1 @@ | ||
If the user does not have control over the model, it is recommended to keep that option off. You can avoid unnecessary calls to external storage when the list result does not depend on the model instance values. | ||
## Observables | ||
The storage methods are called only for the user interaction - when the model is got, or when a new value for the model instance is set. However, there might be a case, where your model instance is been updated outside of the user scope, for example by the server. | ||
Using the `store.set()` method as a callback for the update will trigger the storage `set` method, which can lead to an endless loop of updates. Fortunately, the store provides a special `store.sync()` method, which does the trick. It only updates the memory cache synchronously of the model instance without calling any storage method from the `[store.connect]` configuration. | ||
!> This method bypass the storage, so use it with caution, and only if you would use `store.get()` in another context. This method does not replace `store.set()`. | ||
```typescript | ||
store.sync(modelOrDefinition: object, values: object | null) : Model; | ||
``` | ||
* **arguments**: | ||
* `modelOrDefinition` - a model instance or model definition | ||
* `values` - an object with partial values of the model instance or `null` for deleting the model | ||
* **returns**: | ||
* Model instance or model instance placeholder | ||
```javascript | ||
const Model = { | ||
..., | ||
[store.connect] : { | ||
get: (id) => myApi.get("/model", id), | ||
set: (id, values) => myApi.set("/model", id, values), | ||
}, | ||
}; | ||
define({ | ||
tag: "my-element", | ||
model: store(Model), | ||
socket: (host) => { | ||
const socket = io(); | ||
socket.on("model:update", (values) => { | ||
store.sync(host.model, values); | ||
}); | ||
}, | ||
}); | ||
``` | ||
In the above example, even though the `Model` is connected to the external storage, when the websocket emits an event, the values of the model update without calling `[store.connect].set()`, as we expect. It is an update triggered by the server, so we don't want to send new values to the server again. |
@@ -23,3 +23,3 @@ # Usage | ||
### Get | ||
### get | ||
@@ -58,3 +58,3 @@ ```typescript | ||
### Set | ||
### set | ||
@@ -127,2 +127,130 @@ The `store.set()` method can create a new instance or update an existing model. According to the mode, the first argument should be a model definition or a model instance. | ||
### resolve | ||
You can use the `store.resolve()` method to simplify access to pending model instances, which can be updated at the moment. The function returns a promise resolving into the current model instance, regardless of the pending state. It also supports multiple chains of set methods, so the result will always be the latest instance. | ||
#### Model Instance | ||
```typescript | ||
store.resolve(model: Model): Promise<Model> | ||
``` | ||
* **arguments**: | ||
* `model` - a model instance | ||
* **returns**: | ||
* A promise instance resolving with the latest model value or rejecting with an error | ||
#### Model Definition | ||
```typescript | ||
store.resolve(Model: object, id?: string | object): Promise<Model> | ||
``` | ||
* **arguments**: | ||
* `Model` - a model definition | ||
* `id` - a string or an object representing identifier of the model instance | ||
* **returns**: | ||
* A promise instance resolving with the latest model value or rejecting with an error | ||
```javascript | ||
const State = { | ||
value: "" | ||
}; | ||
async function sendValue(host) { | ||
// state can be in pending state at the moment (updating by the change event) | ||
const state = await store.resolve(host.state); // or store.resolve(State) | ||
const res = await fetch("/my-endpoint", { method: "post", body: JSON.stringify(state) }); | ||
// do something with the response | ||
} | ||
define({ | ||
tag: "my-element", | ||
state: store(State), | ||
render: ({ state }) => html` | ||
<my-async-data-source onupdate="${html.set(state, "value")}"></my-async-data-source> | ||
<button onclick="${sendValue}">Send</button> | ||
`, | ||
}); | ||
``` | ||
### sync | ||
The storage methods are called only for the user interaction - when the model is got, or when a new value for the model instance is set. However, there might be a case, where your model instance is been updated outside of the user scope, for example by the server. | ||
Using the `store.set()` method as a callback for the update will trigger the storage `set` method, which can lead to an endless loop of updates. Fortunately, the store provides a special `store.sync()` method, which does the trick. It only updates the memory cache synchronously of the model instance without calling any storage method from the `[store.connect]` configuration. | ||
!> This method bypass the storage, so use it with caution, and only if you would use `store.get()` in another context. This method does not replace `store.set()`. | ||
```typescript | ||
store.sync(modelOrDefinition: object, values: object | null) : Model; | ||
``` | ||
* **arguments**: | ||
* `modelOrDefinition` - a model instance or model definition | ||
* `values` - an object with partial values of the model instance or `null` for deleting the model | ||
* **returns**: | ||
* Model instance or model instance placeholder | ||
```javascript | ||
const Model = { | ||
..., | ||
[store.connect] : { | ||
get: (id) => myApi.get("/model", id), | ||
set: (id, values) => myApi.set("/model", id, values), | ||
}, | ||
}; | ||
define({ | ||
tag: "my-element", | ||
model: store(Model), | ||
socket: (host) => { | ||
const socket = io(); | ||
socket.on("model:update", (values) => { | ||
store.sync(host.model, values); | ||
}); | ||
}, | ||
}); | ||
``` | ||
In the above example, even though the `Model` is connected to the external storage, when the websocket emits an event, the values of the model update without calling `[store.connect].set()`, as we expect. It is an update triggered by the server, so we don't want to send new values to the server again. | ||
### clear | ||
Both memory and external storage uses a global cache mechanism based on the model definition reference. Model instances are global, so the cache mechanism cannot automatically predict which instance is no longer required. Because of that, the store provides the `store.clear()` method for invalidating model instances by the model definition or specific instance of the model. | ||
```typescript | ||
store.clear(model: object, clearValue?: boolean = true) | ||
``` | ||
* **arguments**: | ||
* `model` - a model definition (for all instances) or a model instance (for a specific one) | ||
* `clearValue` - indicates if the cached value should be deleted (`true`), or it should only notify the cache mechanism, that the value expired, but leaves the value untouched (`false`) | ||
For example, it might be useful to set the `clearValue` to `false` for the case when you want to implement the refresh button. Then, the values stay in the cache, but the store will fetch the next version of the instance. | ||
```javascript | ||
import Email from "./email.js"; | ||
function refresh(host) { | ||
store.clear(host.emails, false); | ||
} | ||
define({ | ||
tag: "my-element", | ||
emails: store([Email]), | ||
render: ({ emails }) => html` | ||
<button onclick="${refresh}">Refresh</button> | ||
${store.ready(emails) && ...} | ||
`, | ||
}); | ||
``` | ||
#### Garbage Collector | ||
The `store.clear()` method works as a garbage collector for unused model instances. Those that are not a dependency of any component connected to the DOM will be deleted entirely from the cache registry (as they would never exist) protecting from the memory leaks. It means, that even if you set `clearValue` to `false`, those instances that are not currently attached to the components, will be permanently deleted when the `store.clear()` method is invoked. | ||
## Factory | ||
@@ -334,3 +462,3 @@ | ||
### `store.ready()` | ||
### ready | ||
@@ -371,3 +499,3 @@ ```typescript | ||
### `store.pending()` | ||
### pending | ||
@@ -387,41 +515,5 @@ ```typescript | ||
### `store.resolve()` | ||
### error | ||
You can use the `store.resolve()` method to simplify access to pending model instances, which can be updated at the moment. The function returns a promise resolving into the current model instance, regardless of the pending state. It also supports multiple chains of set methods, so the result will always be the latest instance. | ||
```typescript | ||
store.resolve(model: Model): Promise<Model> | ||
``` | ||
* **arguments**: | ||
* `model` - a model instance | ||
* **returns**: | ||
* A promise instance resolving with the latest model value or rejecting with an error | ||
```javascript | ||
const State = { | ||
value: "" | ||
}; | ||
async function sendValue(host) { | ||
// state can be in pending state at the moment (updating by the change event) | ||
const state = await store.resolve(host.state); | ||
const res = await fetch("/my-endpoint", { method: "post", body: JSON.stringify(state) }); | ||
// do something with the response | ||
} | ||
define({ | ||
tag: "my-element", | ||
state: store(State), | ||
render: ({ state }) => html` | ||
<my-async-data-source onupdate="${html.set(state, "value")}"></my-async-data-source> | ||
<button onclick="${sendValue}">Send</button> | ||
`, | ||
}); | ||
``` | ||
### `store.error()` | ||
```typescript | ||
store.error(model: Model, propertyName?: string | null): boolean | Error | any | ||
@@ -428,0 +520,0 @@ ``` |
{ | ||
"name": "hybrids", | ||
"version": "8.1.2", | ||
"version": "8.1.3", | ||
"description": "A JavaScript framework for creating fully-featured web applications, components libraries, and single web components with unique declarative and functional architecture", | ||
@@ -5,0 +5,0 @@ "type": "module", |
@@ -540,4 +540,21 @@ /* eslint-disable no-use-before-define */ | ||
if (nestedType !== "object") { | ||
const Constructor = getTypeConstructor(nestedType, key); | ||
const defaultArray = Object.freeze(defaultValue.map(Constructor)); | ||
if ( | ||
nestedType === "function" && | ||
![String, Number, Boolean].includes(defaultValue[0]) | ||
) { | ||
throw TypeError( | ||
`The array item for the '${key}' must be one of the primitive types constructor: String, Number, or Boolean`, | ||
); | ||
} | ||
const Constructor = | ||
nestedType === "function" | ||
? defaultValue[0] | ||
: getTypeConstructor(nestedType, key); | ||
const defaultArray = | ||
nestedType === "function" | ||
? [] | ||
: Object.freeze(defaultValue.map(Constructor)); | ||
return (model, data, lastModel) => { | ||
@@ -1315,4 +1332,5 @@ if (hasOwnProperty.call(data, key)) { | ||
function resolveToLatest(model) { | ||
function resolveToLatest(model, id) { | ||
model = stales.get(model) || model; | ||
if (!definitions.get(model)) model = get(model, id); | ||
@@ -1319,0 +1337,0 @@ const promise = pending(model); |
@@ -77,3 +77,3 @@ declare module "hybrids" { | ||
> | ||
? [Model<T>] | ((model: M) => T[]) | ||
? NestedArrayModel<T> | ((model: M) => NestedArrayModel<T>) | ||
: Required<M>[property] extends object | ||
@@ -87,2 +87,10 @@ ? Model<Required<M>[property]> | ((model: M) => M[property]) | ||
type NestedArrayModel<T> = T extends string | ||
? T[] | [StringConstructor] | ||
: T extends number | ||
? T[] | [NumberConstructor] | ||
: T extends boolean | ||
? T[] | [BooleanConstructor] | ||
: T[] | [Model<T>] | [Model<T>, { loose?: boolean }]; | ||
type ModelIdentifier = | ||
@@ -168,3 +176,6 @@ | string | ||
function submit<M>(draft: M, values?: ModelValues<M>): Promise<M>; | ||
function resolve<M>(model: M): Promise<M>; | ||
function resolve<M>(model: Model<M>, id?: ModelIdentifier): Promise<M>; | ||
function ref<M>(fn: () => M): M; | ||
@@ -305,3 +316,2 @@ | ||
css: (parts: TemplateStringsArray, ...args: unknown[]) => this; | ||
layout: (rules: string = "column") => this; | ||
} | ||
@@ -308,0 +318,0 @@ |
443515
6665