0.6.7 • Published 6 years ago

dactory v0.6.7

Weekly downloads
10
License
MIT
Repository
github
Last release
6 years ago

Dactory :speak_no_evil: :factory:

Dialect fACTORY



The concept

Dactory is a library that allows you to use React's transpiler for different purpose. React is a view layer. It takes care for the rendering part and the actual DOM. Dactory is opposite. It is dealing with the business logic of our applications. It allows us to write markup and basically create our own dialect based on the JSX syntax.

/** @jsx D */
import { D, speak } from 'dactory';

const Greeting = function({ name }) {
  console.log(`Hello dear ${name}!`);
};
const Text = function({ what }) {
  console.log(`You know what ... ${what}!`);
};

speak(
  <D>
    <Greeting name="Jon Snow" />
    <Text what="winter is coming" />
  </D>
);
/* Outputs:
Hello dear Jon Snow! 
You know what ... winter is coming.
*/

You must add the @jsx comment at the top of your files. And you must import D function. Otherwise Dactory will not work.

Installation and setup

Grab the library by running npm install dactory or yarn install dactory. Dactory uses JSX as a base so you have to have some sort of Babel transpilation setup. Check out the examples folder to get an idea how to do it.

Fundamentals

The code that we write follows the JSX syntax. You don't have to learn anything new. If you ever worked with JSX you already know how to write code that Dactory understands.

Core API

The core API of Dactory is just two functions - D and speak. Every tag that we write gets transpiled to D() calls similarly to React.createElement. The more interesting one is speak. It accepts a markup-like code which we will define as dialect and every tag inside as a word. The dialect describes in a declarative fashion what our program does.

Order of execution

The order of the execution is from top to bottom and from outer to inner words.

const Foo = () => console.log('Foo');
const Bar = () => console.log('Bar');
const Mar = () => console.log('Mar');

speak(
  <Foo>
    <Bar />
    <Mar />
  </Foo>
);

/* Outputs:
Foo 
Bar 
Mar
*/

Everything is considered asynchronous

The speak function is asynchronous. Dactory makes an assumption that all the words in our dialect are also asynchronous. For example:

const Fetch = async function ({ url }) {
  return await fetch(url);
}
const App = function () {}

await speak(
  <App>
    <Fetch url="https://jsonplaceholder.typicode.com/posts" />
    <Fetch url="https://jsonplaceholder.typicode.com/users" />
  </App>
);

If there are multiple asynchronous functions they are executed one after each other. If you need to run something in parallel keep reading. There's a word in the predefined words section for that.

Passing data around

Every dialect gets executed with a given context. The context is just a plain JavaScript object and all the words in the dialect has access to it. In fact the speak function accepts one as a second argument (by default set to {}). We also receive the context when the promise returned by speak is resolved. Which means that if we want to get something back we have to inject it into the context because that's the only one output of the speak's call. This happens by using the special exports prop like so:

const GetAnswer = async function () {
  // this gets assigned to `answer` prop in the context
  return 42;
};

speak(<GetAnswer exports="answer" />)
  .then(context => {
    console.log(context.answer); // 42
  });

Think about exports as something that defines a property in context. The value of that newly defined property is what our word returns.

Passing data between words happens by adding a prop with no value and same name prefixed with $. For example:

const GetAnswer = async function() {
  return 42;
};
const Print = function({ answer }) {
  console.log(`The answer is ${answer}.`);
};
const App = function() {};

speak(
  <App>
    <GetAnswer exports="answer" />
    <Print $answer />
  </App>
);

GetAnswer defines a property answer in our context which becomes { answer: 42 }. Later Print says "I need answer prop from the context.

That's not the only one way to pass data around. The function as children pattern works here too:

function GetTitle() {
  return 'developer';
}
function PrintUser({ title, name }) {
  console.log(`Hello ${name} ${title}!`);
}
function App() {
  return 'Boobooo';
}

speak(
  <App exports="name">
    <GetTitle>{ title => <PrintUser title={title} $name /> }</GetTitle>
  </App>
);

PrintUser receives two props title and name. title comes from what GetTitle returns while name comes from the context.

It's just easier to write it as markup:

speak(
  <App exports="name">
    <GetTitle exports="title">
      <PrintUser $title $name />
    </GetTitle>
  </App>
);

If we for some reason don't like the naming in our context we may change it by adding a value to the prefixed prop. For example:

speak(
  <App exports="name">
    <GetTitle exports="title">
      <PrintUser $title $name='applicationName' />
    </GetTitle>
  </App>
);

PrintUser will receive { title: '...', applicationName: '...' } instead of { title: '...', name: '...' }.

Error handling

Because speak returns a promise we can just catch the error at a global level:

const Problem = function() {
  return iDontExist; // throws an error "iDontExist is not defined"
};
const App = function() {};

speak(
  <App>
    <Problem />
  </App>
).catch(error => {
  console.log('Ops, an error: ', error.message);
  // Ops, an error:  iDontExist is not defined
});

That's all fine but it is not really flexible. What we may want is to handle the error inside our dialect. In such cases we have the special onError prop. It accepts another dialect which receives the error as a prop.

const Problem = function() {
  return iDontExist;
};
const App = function() {};
const HandleError = ({ error }) => console.log(error.message); // logs "iDontExist is not defined"

speak(
  <App>
    <Problem onError={ <HandleError /> } />
  </App>
);

Dactory has several strategies for handling errors:

  • If there's no handler provided the error bubbles up
  • If there's a handler and the handler returns false the error is swallowed. Dactory stops the execution of the current set of words.
  • If there's a handler and the handler returns true the error is swallowed. Dactory continues the execution of the current set of words.
  • If there's a handler and the handler returns nothing the error bubbles up.

By stopping the current set of words we meant:

const Problem = function() {
  return iDontExist;
};
const App = function() {};
const Wrapper = function() {};
const HandleError = () => {};
const A = () => console.log('A');
const B = () => console.log('B');
const C = () => console.log('C');

await speak(
  <App exports='answer'>
    <Wrapper>
      <Problem onError={ <HandleError /> } />
      <A />
    </Wrapper>
    <Wrapper>
      <B />
      <C />
    </Wrapper>
  </App>
);

We will see B followed by C but not A because there's an error at that level.

Branching your logic

Obviously we don't have a straight business logic. It has branches. Dactory has no API for this. The cheapest solution for that is the function as children pattern:

function MyLogic({ answer }) {
  if (answer === 42) {
    return true;
  }
  return false;
}
function PrintCorrectAnswer() {
  console.log('Correct!');
}
function PrintWrongAnswer() {
  console.log('Wrong!');
}
function App() {}

await speak(
  <App>
    <MyLogic answer={ 42 }>
      {
        isCorrect => isCorrect ? <PrintCorrectAnswer /> : <PrintWrongAnswer />
      }
    </MyLogic>
  </App>
);

Behavior options

There are options that define the behavior of Dactory while processing your word. For example if you want to run your word's children in parallel you can to use the processChildrenInParallel option.

function A() {
  return new Promise((done) => {
    setTimeout(() => {
      console.log('A');
      done();
    }, 30);
  })
}
function B() {
  console.log('B');
}
function C() {}

C.processChildrenInParallel = true;

speak(
  <C>
    <A />
    <B />
  </C>
);

Without C.processChildrenInParallel = true we will get A followed by B. That's because Dactory will wait till A finishes to run B. However, processChildrenInParallel makes A and B run in parallel and we are getting B followed by A.

Predefined words

Dactory comes with some predefined words.

Wrapper (D)

So far in the examples above we had to define a wrapper function like function App() {}. Instead we can simply use <D />. For example:

/** @jsx D */
import { D, speak } from 'dactory';

const Foo = function () {
  console.log(`Hello world!`);
}

speak(<D><Foo /></D>);

Run words in parallel (Parallel)

/** @jsx D */
import { D, speak, Parallel } from 'dactory';

const A = async function() {}
const B = async function() {}

speak(<Parallel><A /><B /></Parallel>);

Dactory runs B without waiting for A to be resolved.

0.6.7

6 years ago

0.6.6

6 years ago

0.6.5

6 years ago

0.6.4

6 years ago

0.6.1

6 years ago

0.6.0

6 years ago

0.5.0

6 years ago

0.4.0

6 years ago

0.3.0

6 years ago

0.2.4

6 years ago

0.2.3

6 years ago

0.2.2

6 years ago

0.2.1

6 years ago

0.2.0

6 years ago

0.1.12

6 years ago

0.1.11

6 years ago

0.1.10

6 years ago

0.1.9

6 years ago

0.1.8

6 years ago

0.1.7

6 years ago

0.1.6

6 years ago

0.1.5

6 years ago

0.1.4

6 years ago