preact-scoped-model
Version:
Scoped Model pattern for Preact
428 lines (399 loc) • 13.8 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __objRest = (source, exclude) => {
var target = {};
for (var prop in source)
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
target[prop] = source[prop];
if (source != null && __getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(source)) {
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
target[prop] = source[prop];
}
return target;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
createNullaryModel: () => createNullaryModel,
createPropsSelectorModel: () => createPropsSelectorModel,
createReducerModel: () => createReducerModel,
createSelector: () => createSelector,
createSelectorOnce: () => createSelectorOnce,
createSelectors: () => createSelectors,
createSnapshot: () => createSnapshot,
createStateModel: () => createStateModel,
createValue: () => createValue,
createValueOnce: () => createValueOnce,
default: () => src_default,
useScopedModelExists: () => useScopedModelExists,
useSelector: () => useSelector,
useSelectorOnce: () => useSelectorOnce,
useSelectors: () => useSelectors,
useSnapshot: () => useSnapshot,
useValue: () => useValue,
useValueOnce: () => useValueOnce
});
module.exports = __toCommonJS(src_exports);
// src/create-model.tsx
var import_preact = require("preact");
var import_hooks = require("preact/hooks");
var import_compat = require("preact/compat");
var import_preact_hooks = require("@lyonph/preact-hooks");
// src/notifier.tsx
var Notifier = class {
constructor() {
this.alive = true;
this.initialized = false;
this.listeners = /* @__PURE__ */ new Set();
}
subscribe(callback) {
if (this.alive) {
this.listeners.add(callback);
}
return () => {
if (this.alive) {
this.listeners.delete(callback);
}
};
}
consume(value) {
if (this.alive) {
this.ref = {
current: value
};
for (const listener of this.listeners.keys()) {
listener(value);
}
}
}
hydrate(value) {
if (this.alive && !this.initialized) {
this.ref = {
current: value
};
}
}
hasValue() {
return !!this.ref;
}
get value() {
if (this.ref == null) {
throw new Error("Unexpected missing model reference.");
}
return this.ref.current;
}
destroy() {
if (this.alive) {
this.listeners.clear();
this.alive = false;
}
}
};
// src/utils/id.ts
var ID = 0;
function generateId() {
const current = ID;
ID += 1;
return current;
}
// src/utils/MissingScopedModelError.tsx
var MissingScopedModelError = class extends Error {
constructor(modelName) {
super(`
The scoped model '${modelName}' is missing from the ancestor component tree.
Make sure that the model's Provider is mounted first before trying to access
the model's state.
`);
this.modelName = modelName;
}
};
// src/create-model.tsx
function SHOULD_NOTIFY(a, b) {
return !Object.is(a, b);
}
function createModel(useModelHook, options = {}) {
var _a;
const context = (0, import_preact.createContext)(null);
const id = generateId();
const displayName = options.displayName || `ScopedModel-${id}`;
const shouldNotify = (_a = options.shouldNotify) != null ? _a : SHOULD_NOTIFY;
const ProcessorInner = (props) => {
const emitter = (0, import_hooks.useContext)(context);
if (!emitter) {
throw new MissingScopedModelError(displayName);
}
const model = useModelHook(props);
emitter.hydrate(model);
(0, import_hooks.useEffect)(() => {
emitter.initialized = true;
}, [emitter]);
(0, import_hooks.useEffect)(() => {
const hasValue = emitter.hasValue();
if (hasValue && shouldNotify(emitter.value, model) || !hasValue) {
emitter.consume(model);
}
}, [emitter, model]);
return null;
};
const Processor = (0, import_compat.memo)(ProcessorInner, options.shouldUpdate);
const Provider = (_b) => {
var _c = _b, { children } = _c, props = __objRest(_c, ["children"]);
const emitter = (0, import_preact_hooks.useConstant)(() => new Notifier());
(0, import_hooks.useEffect)(() => () => {
emitter.destroy();
}, [emitter]);
return /* @__PURE__ */ (0, import_preact.h)(context.Provider, {
value: emitter
}, /* @__PURE__ */ (0, import_preact.h)(Processor, __spreadValues({}, props)), children);
};
Provider.defaultProps = options.defaultProps;
if (true) {
ProcessorInner.displayName = `ScopedModelProcessor(${displayName}.Processor)`;
Processor.displayName = `ScopedModelProcessor(${displayName}.Processor)`;
Provider.displayName = `ScopedModel(${displayName})`;
context.Provider.displayName = `${displayName}.Provider`;
}
return {
context,
Provider,
displayName
};
}
// src/model-factory/create-nullary-model.tsx
var NEVER_UPDATE = () => true;
function createNullaryModel(useModelHook, options) {
return createModel(useModelHook, {
displayName: options == null ? void 0 : options.displayName,
shouldUpdate: NEVER_UPDATE
});
}
// src/model-factory/create-state-model.tsx
var import_hooks2 = require("preact/hooks");
function createStateModel(initialState, options) {
return createNullaryModel(() => (0, import_hooks2.useState)(initialState), options);
}
// src/model-factory/create-reducer-model.tsx
var import_hooks3 = require("preact/hooks");
function createReducerModel(reducer, initialState, options) {
return createNullaryModel(() => (0, import_hooks3.useReducer)(reducer, initialState), options);
}
// src/model-factory/create-props-selector-model.tsx
function createPropsSelectorModel(options) {
return createModel((props) => props, options);
}
// src/utils/comparer.ts
function defaultCompare(a, b) {
return !Object.is(a, b);
}
function compareList(a, b, compare = defaultCompare) {
if (a === b) {
return false;
}
if (a.length !== b.length) {
return true;
}
for (let i = 0; i < a.length; i += 1) {
if (compare(a[i], b[i])) {
return true;
}
}
return false;
}
// src/hooks/useSelector.tsx
var import_hooks5 = require("preact/hooks");
var import_preact_hooks2 = require("@lyonph/preact-hooks");
// src/hooks/useScopedModelContext.tsx
var import_hooks4 = require("preact/hooks");
function useScopedModelContext(model) {
const context = (0, import_hooks4.useContext)(model.context);
if (!context) {
throw new MissingScopedModelError(model.displayName);
}
return context;
}
// src/hooks/useSelector.tsx
function useSelector(model, selector, shouldUpdate = defaultCompare) {
const notifier = useScopedModelContext(model);
const sub = (0, import_preact_hooks2.useConditionalMemo)(() => ({
read: () => selector(notifier.value),
subscribe: (callback) => notifier.subscribe(callback),
shouldUpdate
}), { notifier, shouldUpdate }, (prev, next) => prev.notifier !== next.notifier || prev.shouldUpdate !== next.shouldUpdate);
const current = (0, import_preact_hooks2.useSubscription)(sub);
(0, import_hooks5.useDebugValue)(current);
return current;
}
// src/hook-factory/create-selector-hook.tsx
function createSelector(model, selector, shouldUpdate = defaultCompare) {
return () => useSelector(model, selector, shouldUpdate);
}
// src/hooks/useSelectors.tsx
var import_hooks6 = require("preact/hooks");
var import_preact_hooks3 = require("@lyonph/preact-hooks");
function useSelectors(model, selector, shouldUpdate = defaultCompare) {
const compare = (0, import_preact_hooks3.useConditionalCallback)((a, b) => compareList(a, b, shouldUpdate), shouldUpdate);
const value = useSelector(model, selector, compare);
(0, import_hooks6.useDebugValue)(value);
return value;
}
// src/hook-factory/create-selectors-hook.tsx
function createSelectors(model, selector, shouldUpdate = defaultCompare) {
return () => useSelectors(model, selector, shouldUpdate);
}
// src/hooks/useValue.tsx
var import_hooks7 = require("preact/hooks");
var import_preact_hooks4 = require("@lyonph/preact-hooks");
function useValue(model, shouldUpdate = defaultCompare) {
const notifier = useScopedModelContext(model);
const sub = (0, import_preact_hooks4.useConditionalMemo)(() => ({
read: () => notifier.value,
subscribe: (callback) => notifier.subscribe(callback),
shouldUpdate
}), { notifier, shouldUpdate }, (prev, next) => prev.notifier !== next.notifier || prev.shouldUpdate !== next.shouldUpdate);
const current = (0, import_preact_hooks4.useSubscription)(sub);
(0, import_hooks7.useDebugValue)(current);
return current;
}
// src/hook-factory/create-value-hook.tsx
function createValue(model, shouldUpdate = defaultCompare) {
return () => useValue(model, shouldUpdate);
}
// src/hooks/useValueOnce.tsx
var import_hooks8 = require("preact/hooks");
function useValueOnce(model) {
const { value } = useScopedModelContext(model);
(0, import_hooks8.useDebugValue)(value);
return value;
}
// src/hook-factory/create-value-once-hook.tsx
function createValueOnce(model) {
return () => useValueOnce(model);
}
// src/hooks/useSnapshotBase.tsx
var import_hooks9 = require("preact/hooks");
function useSnapshotBase(notifier, listener) {
(0, import_hooks9.useEffect)(() => {
let mounted = true;
const callback = (value) => {
if (mounted) {
listener(value);
}
};
const unsubscribe = notifier.subscribe(callback);
return () => {
mounted = false;
unsubscribe();
};
}, [notifier, listener]);
}
// src/hooks/useSnapshot.tsx
function useSnapshot(model, listener) {
const notifier = useScopedModelContext(model);
useSnapshotBase(notifier, listener);
}
// src/hook-factory/create-snapshot-hook.tsx
function createSnapshot(model, listener) {
return () => {
useSnapshot(model, listener);
};
}
// src/hooks/useSelectorOnce.tsx
var import_hooks10 = require("preact/hooks");
var import_preact_hooks5 = require("@lyonph/preact-hooks");
function useSelectorOnce(model, selector) {
const baseValue = useValueOnce(model);
const value = (0, import_preact_hooks5.useConditionalMemo)(() => selector(baseValue), { baseValue, selector }, (prev, next) => prev.baseValue !== next.baseValue || prev.selector !== next.selector);
(0, import_hooks10.useDebugValue)(value);
return value;
}
// src/hook-factory/create-selector-once-hook.tsx
function createSelectorOnce(model, selector) {
return () => useSelectorOnce(model, selector);
}
// src/hooks/useScopedModelExists.tsx
var import_hooks11 = require("preact/hooks");
function useScopedModelExists(model) {
const context = (0, import_hooks11.useContext)(model.context);
return !!context;
}
// src/index.ts
var src_default = createModel;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
createNullaryModel,
createPropsSelectorModel,
createReducerModel,
createSelector,
createSelectorOnce,
createSelectors,
createSnapshot,
createStateModel,
createValue,
createValueOnce,
useScopedModelExists,
useSelector,
useSelectorOnce,
useSelectors,
useSnapshot,
useValue,
useValueOnce
});
/**
* @license
* MIT License
*
* Copyright (c) 2021 Alexis Munsayac
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*
* @author Alexis Munsayac <alexis.munsayac@gmail.com>
* @copyright Alexis Munsayac 2021
*/
//# sourceMappingURL=index.js.map
;