Comparing version 0.8.1 to 0.8.2
@@ -46,3 +46,3 @@ 'use strict'; | ||
if (!parent) { | ||
throw new Error('The Element can not be run without a parent.'); | ||
throw new Error('The Element can not be run with no parent.'); | ||
} | ||
@@ -70,5 +70,3 @@ this.parent = parent; | ||
scope: {}, | ||
dispatch: function dispatch(type, value) { | ||
this.scope[type] = value; | ||
}, | ||
dispatch: function dispatch() {}, | ||
readFromScope: function readFromScope(key) { | ||
@@ -75,0 +73,0 @@ var value = this.scope[key]; |
@@ -42,3 +42,3 @@ 'use strict'; | ||
}; | ||
}, props, children); | ||
}, _extends({}, props, { scope: '*' }), children); | ||
} | ||
@@ -45,0 +45,0 @@ return (0, _Element2.default)(func, props, children); |
@@ -12,144 +12,16 @@ 'use strict'; | ||
var _Element = require('./Element'); | ||
var _execute = require('./middlewares/execute'); | ||
var _Element2 = _interopRequireDefault(_Element); | ||
var _execute2 = _interopRequireDefault(_execute); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
var _processResult = require('./middlewares/processResult'); | ||
// helpers | ||
var handleElementError = async function handleElementError(error, props, element) { | ||
if (props && props.onError) { | ||
props.onError.mergeToProps({ error: error }); | ||
var _processResult2 = _interopRequireDefault(_processResult); | ||
var onErrorStrategy = await props.onError.run(element); | ||
var _processChildren = require('./middlewares/processChildren'); | ||
if (onErrorStrategy === false) { | ||
throw new Error(_Element2.default.errors.STOP_PROCESSING); | ||
} else if (onErrorStrategy === true) { | ||
throw new Error(_Element2.default.errors.CONTINUE_PROCESSING); | ||
} else { | ||
// swallowing the error | ||
} | ||
} else { | ||
throw error; | ||
} | ||
}; | ||
var _processChildren2 = _interopRequireDefault(_processChildren); | ||
// middlewares | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
async function execute(element) { | ||
var func = element.func, | ||
props = element.props; | ||
var normalizedProps = _extends({}, props); | ||
// normalizing props | ||
if (props) { | ||
normalizedProps = _extends({}, props); | ||
Object.keys(props).forEach(function (propName) { | ||
if (propName.charAt(0) === '$') { | ||
var prop = propName.substr(1, propName.length); | ||
var value = element.readFromScope(prop); | ||
if (typeof value !== 'undefined') { | ||
if (typeof props[propName] === 'string') { | ||
normalizedProps[props[propName]] = value; | ||
} else if (typeof props[propName] === 'function') { | ||
normalizedProps = _extends({}, normalizedProps, props[propName](value)); | ||
} else { | ||
normalizedProps[prop] = value; | ||
} | ||
delete normalizedProps[propName]; | ||
} | ||
} | ||
}); | ||
} | ||
// actual running of the function | ||
try { | ||
element.result = await func.call(element, normalizedProps); | ||
} catch (error) { | ||
await handleElementError(error, normalizedProps, element); | ||
} | ||
} | ||
async function processResult(element) { | ||
var result = element.result, | ||
props = element.props, | ||
scope = element.scope; | ||
if (props && props.exports) { | ||
if (typeof props.exports === 'function') { | ||
var exportedProps = props.exports(result); | ||
Object.keys(exportedProps).forEach(function (key) { | ||
scope[key] = exportedProps[key]; | ||
element.dispatch(key, exportedProps[key]); | ||
}); | ||
} else { | ||
scope[props.exports] = result; | ||
element.dispatch(props.exports, result); | ||
} | ||
} | ||
if (result) { | ||
if (_Element2.default.isItAnElement(result)) { | ||
await result.run(element); | ||
} | ||
// Generator | ||
if (typeof result.next === 'function') { | ||
var gen = result; | ||
var genRes = { value: undefined, done: false }; | ||
while (!genRes.done) { | ||
genRes = gen.next(genRes.value); | ||
if (_Element2.default.isItAnElement(genRes.value)) { | ||
genRes.value = await genRes.value.run(element); | ||
} | ||
} | ||
element.result = genRes.value; | ||
} | ||
} | ||
} | ||
async function processChildren(element) { | ||
var func = element.func, | ||
children = element.children, | ||
result = element.result; | ||
// FACC pattern | ||
if (children && children.length === 1 && !_Element2.default.isItAnElement(children[0])) { | ||
var resultOfFACC = await children[0].call(element, result); | ||
if (_Element2.default.isItAnElement(resultOfFACC)) { | ||
await resultOfFACC.run(element); | ||
} | ||
// nested tags | ||
} else if (children && children.length > 0) { | ||
var pointer = 0; | ||
var parallelProcessing = !!func.processChildrenInParallel; | ||
while (pointer < children.length) { | ||
var w = children[pointer]; | ||
try { | ||
if (parallelProcessing) { | ||
w.run(element); | ||
} else { | ||
await w.run(element); | ||
} | ||
} catch (error) { | ||
if (error.message === _Element2.default.errors.STOP_PROCESSING) { | ||
break; | ||
} else if (!(error.message === _Element2.default.errors.CONTINUE_PROCESSING)) { | ||
throw error; | ||
} | ||
} | ||
pointer++; | ||
} | ||
} | ||
} | ||
function Pipeline(element) { | ||
@@ -204,5 +76,5 @@ if (!element) { | ||
Pipeline.execute = execute; | ||
Pipeline.processResult = processResult; | ||
Pipeline.processChildren = processChildren; | ||
Pipeline.execute = _execute2.default; | ||
Pipeline.processResult = _processResult2.default; | ||
Pipeline.processChildren = _processChildren2.default; | ||
@@ -212,7 +84,7 @@ function createDefaultPipeline(element) { | ||
pipeline.add(execute); | ||
pipeline.add(processResult, 'result'); | ||
pipeline.add(processChildren, 'children'); | ||
pipeline.add(_execute2.default); | ||
pipeline.add(_processResult2.default, 'result'); | ||
pipeline.add(_processChildren2.default, 'children'); | ||
return pipeline; | ||
} |
{ | ||
"name": "actml", | ||
"version": "0.8.1", | ||
"version": "0.8.2", | ||
"description": "Like jsx but for your business logic", | ||
@@ -5,0 +5,0 @@ "main": "lib", |
116
README.md
@@ -9,3 +9,3 @@ # <ActML /> :rocket: <!-- omit in toc --> | ||
- [Getting in and out of your function/element](#getting-in-and-out-of-your-functionelement) | ||
- [Context API](#context-api) | ||
- [Scope API](#scope-api) | ||
- [Setting in and getting out from the context](#setting-in-and-getting-out-from-the-context) | ||
@@ -20,59 +20,36 @@ - [Setting initial context value](#setting-initial-context-value) | ||
If you are old enough you'll remember the times when we were writing all of our JavaScript in a single file. Most of the time our logic was placed in the global scope with no ideas for architecture or separation. And it was fine because JavaScript was just sugar on top of HTML and CSS. Today is completely different. We put our logic in functions/classes and we organize them in a giant graph with dozen of branches. Let's take the following code snippet. | ||
Wouldn't be cool if we can define a function and execute it the same way as we render React component. Like for example: | ||
```js | ||
async function getSeason(endpoint) { | ||
const result = await fetch(endpoint); | ||
const { season } = await result.json(); | ||
return season; | ||
const Greeting = function () { | ||
return 'Hey!'; | ||
} | ||
async function getMySchedule(endpoint) { | ||
return (await getSeason(endpoint)) === 'summer' ? '🌴🍨🏄' : '⏰☕️💻'; | ||
} | ||
async function amIGoingToTheBeach() { | ||
const schedule = await getMySchedule('https://www.mocky.io/v2/5ba2a2b52f00006a008d2e0d'); | ||
console.log(schedule.indexOf('🏄') >= 0 ? '👍😎' : '👉😭'); | ||
} | ||
amIGoingToTheBeach(); | ||
run(<Greeting />).then( | ||
message => console.log(message) | ||
); | ||
``` | ||
Tthe asynchronous `getSeason` is fetching the current season. Based on the season `getMySchedule` decides what will be the user's activities. Then we have some logic in `amIGoingToTheBeach` that uses the schedule to decide what emojis to print in the console. | ||
What if we have more functions, they depend on each other and some of them are asynchronous: | ||
There are couple of problems with this code. Of course the biggest one is that the user will never go to the beach because the fake endpoint always returns `{"season": "not summer"}`. Besides that we have a dependency problem. `getMySchedule` not only needs the current season but also knows how to get it because it directly uses `getSeason`. Sure, we can use some more composition by getting the season in `amIGoingToTheBeach` and passing it as parameter to `getSeason` but wouldn't be cool if we can use a code like this: | ||
```js | ||
import { A, run } from 'actml'; | ||
const Greeting = function({ name }) { | ||
return `Hey ${name}!`; | ||
}; | ||
async function GetProfileName() { | ||
const response = await fetch('https://reqres.in/api/users/2'); | ||
const { data: { first_name, last_name } } = await response.json(); | ||
async function GetSeason({ endpoint }) { | ||
const result = await fetch(endpoint); | ||
const { season } = await result.json(); | ||
return season; | ||
return first_name + ' ' + last_name; | ||
} | ||
async function GetMySchedule({ season }) { | ||
return season === 'summer' ? '🌴🍨🏄' : '⏰☕️💻'; | ||
function Print({ message }) { | ||
console.log(message); | ||
} | ||
function AmIGoingToTheBeach({ schedule }) { | ||
console.log(schedule.indexOf('🏄') >= 0 ? '👍😎' : '👉😭'); | ||
} | ||
run( | ||
<GetSeason endpoint="https://www.mocky.io/v2/5ba2a2b52f00006a008d2e0d"> | ||
{ season => ( | ||
<GetMySchedule season={ season }> | ||
{ schedule => <AmIGoingToTheBeach schedule={ schedule } /> } | ||
</GetMySchedule> | ||
)} | ||
</GetSeason> | ||
); | ||
``` | ||
Or if we use [ActML's context API](#context-api): | ||
```js | ||
run( | ||
<A> | ||
<GetSeason exports="season" endpoint="https://www.mocky.io/v2/5ba2a2b52f00006a008d2e0d" /> | ||
<GetMySchedule $season exports="schedule" /> | ||
<AmIGoingToTheBeach $schedule /> | ||
<GetProfileName exports="name" /> | ||
<Greeting $name> | ||
{ message => <Print message={message} /> } | ||
</Greeting> | ||
</A> | ||
@@ -82,4 +59,14 @@ ); | ||
Notice how `GetMySchedule` and `AmIGoingToTheBeach` became pure functions which only accept what they need. I know what you are thinking - "Do we really need such kung-fu to make those functions pure?". Well, we may achieve the same thing but we need a fourth function that act as a composition layer and wires everything. ActML is doing that and it comes with a lot more opportunities for composition. It is your glue layer where you say what needs to happen without specifying how. | ||
Isn't it like writing React? Let's see step by step what ActML does. | ||
1. The `<A>` element is just a wrapper. | ||
2. `<GetProfileName>` is an asynchronous function so ActML waits till its promise is resolved. `<GetProfileName>` also returns a result and has `exports` prop defined. That is a special prop which is saying "Export a variable with name `name` and make it available for other elements". | ||
3. `<Greeting>` needs the `name` of the user and uses the special dollar sign notation which to ActML processor means "Inject a variable with name `name` as a prop". | ||
4. `<Greeting>` also has a function as child and it sends its result there which in our case is the full message. | ||
5. `<Print>` just gets the message and prints it out in the console. | ||
_Here is a working [Codesandbox](https://codesandbox.io/s/341xn5vrlq) of the code above._ | ||
So, that is the concept of ActML. It allows us to define in a declarative fashion our business logic. Same as our UI. There is nothing (almost) imperative. In fact all the code that we pass to the `run` function is nothing but definitions of _what_ should happen. It is not saying _how_. This is extremely powerful concept because it shifts the responsibility to another level and makes the development a lot more easier. We use composition over raw implementation. If you like this way of thinking then ActML may be for you. | ||
## What you need to use ActML | ||
@@ -96,3 +83,3 @@ | ||
From a tools perspective you need some sort of [Babel](https://babeljs.io/docs/en/babel-preset-react) integration. There's a Redux+ActML example app [here](https://github.com/krasimir/actml/tree/master/examples/react-redux-app) that you can check out. | ||
From a tools perspective you need some sort of [Babel](https://babeljs.io/docs/en/babel-preset-react) integration. There's a React+Redux+ActML example app [here](https://github.com/krasimir/actml/tree/master/examples/react-redux-app) that you can check. | ||
@@ -126,3 +113,3 @@ ## What is an ActML element | ||
In general ActML runner assumes that every of the elements is asynchronous. It executes the functions from the outer to inner ones and from top to bottom. All the three types of elements may return another element. In the case of generator we may `yield` also another element. For example: | ||
In general ActML processor assumes that every of the elements is asynchronous. It executes the functions from the outer to inner ones and from top to bottom. All the three types of elements may return another element. In the case of generator we may `yield` also another element. For example: | ||
@@ -152,5 +139,7 @@ ```js | ||
_[Codesandbox](https://codesandbox.io/s/o41140ro85) with the example._ | ||
## Getting in and out of your function/element | ||
The input to your ActML is the attributes that we pass to the tag. They come as `props` to your function. For example: | ||
The input to your ActML element is the attributes that we pass to the tag or if we have to make a parallel with React - the `props`. For example: | ||
@@ -180,8 +169,33 @@ ```js | ||
Also the returned by a function value gets injected into the ActML's runner context and it can be fetched from there via the context API. | ||
Another way to pass data between elements is the Scope API. Read about that below. | ||
## Context API | ||
## Scope API | ||
Using the FACC pattern everywhere is not very convenient. So, there's a context object which is shared through all the elements in the currently executed set of elements. Think about it as a JavaScript object with `set` and `get` method. In one side we are registering stuff inside by using the `set` method and from another we are getting them using the `get` method. | ||
Using the FACC pattern everywhere is not very convenient. So there is a Scope API that can keep data and share it with other elements. Let's take the following example: | ||
```js | ||
const Foo = () => 'Jon Snow'; | ||
<App> | ||
<Foo exports='name'> | ||
<Zoo $name /> | ||
</Foo> | ||
<Bar $name /> | ||
</App> | ||
``` | ||
Every ActML element has a `scope` object. It is really just a plain JavaScript object `{}` and every time when we use `exports` we are saving something there. For example the `scope` object of the `Foo` element is equal to `{ name: 'Jon Snow' }`. Together with creating the `name` variable in the `scope` of `Foo` we are also _sending_ an event to the parent `App` element. Then the scope of `App` also has the variable `name` with value `Jon Snow`. Then `App` sends an event to his parent and so on. Overall we have the following picture: | ||
```js | ||
<App> scope={ name: 'Jon Snow' } | ||
<Foo exports='name'> scope={ name: 'Jon Snow' } | ||
<Zoo $name /> | ||
</Foo> | ||
<Bar $name /> | ||
</App> | ||
``` | ||
If we want to read something from the scope we use the dollar sign (`$`) plus the name of the variable. Like it is done in the example - `$name`. First the element checks in its own scope object. If it doesn't find the variable there asks the parent and the parent of the parent and so on. In the code above `Zoo` reads the `name` from `Foo`'s scope while `Bar` reads it from `App`'s scope. | ||
### Setting in and getting out from the context | ||
@@ -188,0 +202,0 @@ |
Sorry, the diff of this file is not supported yet
381645
44
1017
349