Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions docs/docs/api/get.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { get } from "@c11/engine.macro"
`get` provides the ability to get values from the global state at a later time,
after the `view` or `producer` was triggered. It works the same way as
[observe](/docs/api/observe), except:
1. `get` don't provide a value, but instead a function which can be called at
1. `get` don't provide a value, but instead an api for that path which can be used at
any time in future to get the latest value form state
2. `get` don't cause `view`s and `producer`s to get triggered

Expand All @@ -25,20 +25,21 @@ after the `view` or `producer` was triggered. It works the same way as

## API

### `get.<path>: (newParams?: object) => any`
`get.<path>` returns an object with following properties:

A call to `get.<path>` (where `<path>` is a path to any data in state) returns a
getter function.
1. `.value(params?: object)` returns the date stored at that `<path>`
`params` is an optional object argument, the keys of which set the
[param](/docs/api/param)s.
2. `.includes(value: any, params?: object)` if the value at the given
`<path>` is an array or a string, it returns a boolean if the provided
`value` exists at that `<path>`
3. `.length(params?: object)` if the value at the given `<path>` is an `array`,a `string`, or a `function` it returns the length property

Calling this function returns the data stored in that path, or `undefined` (for
non-existent path). If the stored data is serializable (e.g a primitive
For the `value` getter method, if the stored data is serializable (e.g a primitive
Javascript type, a plain object), a copy of the data is returned. However, if
the data is not serializable (e.g a class instance, function etc), a reference
to it is returned.

The getter function also receives an optional argument of type `Object`. The
keys of this object set the [param](/docs/api/param)s.

## Example

For example, if the state looks like:
Expand All @@ -57,7 +58,7 @@ e.g

```
const doSomeWork: producer = ({ getBar = get.foo.bar }) => {
const var = getBar(); // provides lates value of bar, and does not trigger if bar ever changes
const var = getBar.value(); // provides lates value of bar, and does not trigger if bar ever changes
}
```

Expand Down
10 changes: 6 additions & 4 deletions docs/docs/api/update.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ from state, `update` allows changing values in state.

`update.<path>` returns an object with following properties:

1. `.set(val: any, newParams?: object)` to replace the value of `<path>` in
state, or create it if it doesn't exist yet. `newParams` is an optional
1. `.set(value: any, params?: object)` to replace the value of `<path>` in
state, or create it if it doesn't exist yet. `params` is an optional
object argument, the keys of which set the [param](/docs/api/param)s.
2. `.merge(val: any)` accepts an object, and merge it with existing object value
2. `.merge(value: any, params?: object)` accepts an object, and merge it with existing object value
of `<path>` in state
3. `.remove()` removes the `<path>` from state
3. `.remove(params?: object)` removes the `<path>` from state
4. `.push(value: any, params?: object)` if the value at the given `<path>` is an array, then the value will be appened to the array
5. `.pop(params?: object)` if the value at the given `<path>` is an array, then the last element will be removed

## Example

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"test": "jest --clear-cache && lerna run test --parallel",
"describe": "npm-scripts-info",
"commit": "git-cz",
"release": "yarn clean && lerna run build && lerna run test",
"release": "yarn clean && lerna run build && lerna run test --parallel",
"version:lerna": "lerna version --conventional-commits",
"publish": "lerna publish from-package",
"publish:local": "lerna run publish:local",
Expand Down
73 changes: 71 additions & 2 deletions packages/engine.producer/specs/producer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ test("should support get operations", () => {
},
};
const struct: producer = ({ refProp = get.items[param.id.bar].value }) => {
const value = refProp({
const value = refProp.value({
id: {
bar: "foo",
},
Expand Down Expand Up @@ -416,7 +416,7 @@ test("should support path values to be used", () => {
val3 = update[prop.path1][prop.path2.bam.value],
}) => {
observeVal = val1;
getVal = val2({ propName: "value" });
getVal = val2.value({ propName: "value" });
updateVal = val3;
};
const propName = "bar";
Expand Down Expand Up @@ -520,6 +520,75 @@ test("should redo paths and keep reference if external props change", () => {
expect(fn.mock.calls[2][0]).toBe(333);
});

test("should support the full api for the update operation", () => {
const struct: producer = ({
b = update.b,
c = update.c,
d = update.d,
e = update.e,
f = update.f,
g = update.g,
}) => {
b.set("123");
c.merge({ foo: "123" });
d.remove();
e.push(3);
f.push(2);
g.pop();
};
const result = run(struct, {
b: "abc2",
c: {
bar: "123",
},
d: "foo",
e: [1, 2],
f: 1,
g: [1, 2, 3],
});
jest.runAllTimers();
expect(result.db.get("/b")).toBe("123");
expect(result.db.get("/c")).toEqual({
foo: "123",
bar: "123",
});
expect(result.db.get("/d")).toBe(undefined);
expect(result.db.get("/e")).toEqual([1, 2, 3]);
expect(result.db.get("/f")).toBe(1);
expect(result.db.get("/g")).toEqual([1, 2]);
});

test("should support the full api for the get operation", () => {
const struct: producer = ({
a = get.a,
b = get.b[param.prop],
c = get.c,
d = get.d,
e = get.e,
f = get.f
}) => {
expect(a.value()).toBe('abc1')
expect(b.value({prop: 'bar'})).toBe('123')
expect(c.includes('foo')).toBe(true)
expect(c.includes('baz')).toBe(false)
expect(d.length()).toBe(4)
expect(e.includes('bam')).toBe(true)
expect(e.includes('qux')).toBe(false)
expect(f.length()).toBe(3)
};
run(struct, {
a: "abc1",
b: {
bar: "123",
},
c: ["foo"],
d: [1, 2, 3, 4],
e: 'foo bam baz',
f: (a, b, c) => {}
});
jest.runAllTimers();
});

/*
test("should allow args composition", () => {
const state = {
Expand Down
3 changes: 2 additions & 1 deletion packages/engine.producer/src/graph/getInvokablePath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
GraphStructure,
UpdateOperation,
GetOperation,
OperationParams
} from "@c11/engine.types";
import { PathSymbol } from "../path";
import { resolveValue } from "./resolveValue";
Expand All @@ -10,7 +11,7 @@ import { wildcard } from "../wildcard";
export const getInvokablePath = (
structure: GraphStructure,
op: GetOperation | UpdateOperation,
params: any
params: OperationParams
) => {
const path = op.path.reduce((acc, x: any) => {
const value = resolveValue(structure, x, params);
Expand Down
32 changes: 30 additions & 2 deletions packages/engine.producer/src/graph/getOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import {
DatastoreInstance,
GraphStructure,
GetOperation,
OperationParams,
} from "@c11/engine.types";
import isString from "lodash/isString";
import isArray from "lodash/isArray";
import { getInvokablePath } from "./getInvokablePath";
import isFunction from "lodash/isFunction";

// TODO: add a isValid method to be able to check
// if the ref path is properly generated
Expand All @@ -15,11 +19,35 @@ export const getOperation = (
structure: GraphStructure,
op: GetOperation
) => {
const get = (params: any) => {
const value = (params: OperationParams): unknown => {
const path = getInvokablePath(structure, op, params);
if (path) {
return db.get(path);
}
return
};
const includes = (value: any, params: OperationParams): void | boolean => {
const path = getInvokablePath(structure, op, params);
if (path) {
const val = db.get(path);
if (isArray(val) || isString(val)) {
return val.includes(value)
}
}
}
const length = (params: OperationParams): void | number => {
const path = getInvokablePath(structure, op, params);
if (path) {
const val = db.get(path);
if (!(isString(val) || isArray(val) || isFunction(val))) {
return
}
return val.length
}
}
return {
value,
includes,
length
};
return get;
};
47 changes: 43 additions & 4 deletions packages/engine.producer/src/graph/updateOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ import {
DatastoreInstance,
GraphStructure,
UpdateOperation,
OperationParams
} from "@c11/engine.types";
import isArray from "lodash/isArray";
import { getInvokablePath } from "./getInvokablePath";

export const updateOperation = (
db: DatastoreInstance,
structure: GraphStructure,
op: UpdateOperation
) => {
const set = (value: any, params: any) => {
const set = (value: any, params: OperationParams) => {
const path = getInvokablePath(structure, op, params);
if (path) {
const patch = {
Expand All @@ -21,7 +23,7 @@ export const updateOperation = (
db.patch([patch]);
}
};
const merge = (value: any, params: any) => {
const merge = (value: any, params: OperationParams) => {
const path = getInvokablePath(structure, op, params);
if (path) {
const patch = {
Expand All @@ -42,7 +44,7 @@ export const updateOperation = (
db.patch([patch]);
}
};
const remove = (params: any) => {
const remove = (params: OperationParams) => {
const path = getInvokablePath(structure, op, params);
if (path) {
const patch = {
Expand All @@ -52,9 +54,46 @@ export const updateOperation = (
db.patch([patch]);
}
};
const push = (value: any, params: OperationParams) => {
const path = getInvokablePath(structure, op, params);
if (path) {
const val = db.get(path)
if (!isArray(val)) {
// console.error('path is not an array')
return
}
val.push(value)
const patch = {
op: "add",
path: path,
value: val
};
db.patch([patch]);
}
};
const pop = (params: OperationParams) => {
const path = getInvokablePath(structure, op, params);
if (path) {
const val = db.get(path)
if (!isArray(val)) {
// console.error('path is not an array')
return
}
val.pop()
const patch = {
op: "add",
path: path,
value: val
};
db.patch([patch]);
}
};

return {
set,
merge,
remove,
};
push,
pop
}
};
2 changes: 1 addition & 1 deletion packages/engine.react/specs/get.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ test("Expect to call using only get", async (done) => {
document.body.appendChild(rootEl);
const Component: view = ({ foo = get.foo }) => {
expect(foo).toBeDefined();
return <div data-testid="foo">{foo()}</div>;
return <div data-testid="foo">{foo.value()}</div>;
};
engine({
state: defaultState,
Expand Down
2 changes: 1 addition & 1 deletion packages/engine.react/specs/path.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ test("should support path operations with multiple components", async (done) =>
<input
data-testid="foo"
type="text"
defaultValue={value()}
defaultValue={value.value()}
onChange={(e) => path2.set(e.target.value)}
/>
);
Expand Down
9 changes: 9 additions & 0 deletions packages/engine.types/src/producer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,12 @@ export interface ProducerContext {
debug?: boolean;
addView?: (view: ViewInstance) => void;
}

export type OperationParams = {
[k: string]: OperationParams | string | number | void | null;
}