Skip to content
Permalink
Browse files

Extract Ink initialization into instances for better flexibility

  • Loading branch information...
vadimdemedes committed Mar 3, 2019
1 parent da5ff3d commit f7dbb97afcf1bd1ebe05301404964303c3af263e
Showing with 144 additions and 104 deletions.
  1. +1 −0 package.json
  2. +21 −9 readme.md
  3. +108 −0 src/instance.js
  4. +14 −95 src/render.js
@@ -40,6 +40,7 @@
"dependencies": {
"ansi-escapes": "^3.2.0",
"arrify": "^1.0.1",
"auto-bind": "^2.0.0",
"chalk": "^2.4.1",
"cli-cursor": "^2.1.0",
"lodash.throttle": "^4.1.1",
@@ -129,7 +129,7 @@ In this readme only Ink's methods will be documented.

#### render(tree, options)

Returns: `App`
Returns: `Instance`

Mount a component and render the output.

@@ -216,31 +216,43 @@ There's also a shortcut to avoid passing `options` object:
render(<Counter>, process.stdout);
```

#### App
#### Instance

This is the object that `render()` returns.

##### rerender

Replace previous root node with a new one or update props of the current root node.

```jsx
// Update props of the root node
const {rerender} = render(<Counter count={1}/>);
rerender(<Counter count={2}/>);
// Replace root node
const {rerender} = render(<OldCounter/>);
rerender(<NewCounter/>);
```

##### unmount

Manually unmount the whole Ink app.

```jsx
const app = render(<MyApp/>);
app.unmount();
const {unmount} = render(<MyApp/>);
unmount();
```

##### waitUntilExit

Returns a promise, which resolves when app is unmounted.

```jsx
const app = render(<MyApp/>);
const {unmount, waitUntilExit} = render(<MyApp/>);
setTimeout(() => {
app.unmount();
}, 1000);
setTimeout(unmount, 1000);
await app.waitUntilExit(); // resolves after `app.unmount()` is called
await waitUntilExit(); // resolves after `unmount()` is called
```

## Building Layouts
@@ -0,0 +1,108 @@
import React from 'react';
import throttle from 'lodash.throttle';
import autoBind from 'auto-bind';
import logUpdate from './vendor/log-update';
import createReconciler from './create-reconciler';
import createRenderer from './create-renderer';
import {createNode} from './dom';
import App from './components/App';

export default class Instance {
constructor(options) {
autoBind(this);

this.options = options;

this.rootNode = createNode('root');
this.renderer = createRenderer({
terminalWidth: options.stdout.columns
});

this.log = logUpdate.create(options.stdout);
this.throttledLog = options.debug ? this.log : throttle(this.log, {
leading: true,
trailing: true
});

// Ignore last render after unmounting a tree to prevent empty output before exit
this.ignoreRender = false;

// Store last output to only rerender when needed
this.lastOutput = '';
this.lastStaticOutput = '';

// This variable is used only in debug mode to store full static output
// so that it's rerendered every time, not just new static parts, like in non-debug mode
this.fullStaticOutput = '';

this.reconciler = createReconciler(this.onRender);
this.container = this.reconciler.createContainer(this.rootNode, false);

this.exitPromise = new Promise(resolve => {
this.resolveExitPromise = resolve;
});
}

onRender() {
if (this.ignoreRender) {
return;
}

const {output, staticOutput} = this.renderer(this.rootNode);

// If <Static> output isn't empty, it means new children have been added to it
const hasNewStaticOutput = staticOutput && staticOutput !== '\n' && staticOutput !== this.lastStaticOutput;

if (this.options.debug) {
if (hasNewStaticOutput) {
this.fullStaticOutput += staticOutput;
this.lastStaticOutput = staticOutput;
}

this.options.stdout.write(this.fullStaticOutput + output);
return;
}

// To ensure static output is cleanly rendered before main output, clear main output first
if (hasNewStaticOutput) {
this.log.clear();
this.options.stdout.write(staticOutput);
this.log(output);

this.lastStaticOutput = staticOutput;
}

if (output !== this.lastOutput) {
this.throttledLog(output);

this.lastOutput = output;
}
}

render(node) {
const tree = (
<App
stdin={this.options.stdin}
stdout={this.options.stdout}
exitOnCtrlC={this.options.exitOnCtrlC}
onExit={this.unmount}
>
{node}
</App>
);

this.reconciler.updateContainer(tree, this.container);
}

unmount() {
this.onRender();
this.log.done();
this.ignoreRender = true;
this.reconciler.updateContainer(null, this.container);
this.resolveExitPromise();
}

waitUntilExit() {
return this.exitPromise;
}
}
@@ -1,10 +1,6 @@
import React from 'react';
import throttle from 'lodash.throttle';
import logUpdate from './vendor/log-update';
import createReconciler from './create-reconciler';
import createRenderer from './create-renderer';
import {createNode} from './dom';
import App from './components/App';
import Instance from './instance';

const instances = new WeakMap();

export default (node, options = {}) => {
// Stream was passed instead of `options` object
@@ -23,97 +19,20 @@ export default (node, options = {}) => {
...options
};

const rootNode = createNode('root');
const render = createRenderer({
terminalWidth: options.stdout.columns
});

const log = logUpdate.create(options.stdout);
const throttledLog = options.debug ? log : throttle(log, {
leading: true,
trailing: true
});

// Ignore last render after unmounting a tree to prevent empty output before exit
let ignoreRender = false;

// Store last output to only rerender when needed
let lastOutput = '';
let lastStaticOutput = '';

// This variable is used only in debug mode to store full static output
// so that it's rerendered every time, not just new static parts, like in non-debug mode
let fullStaticOutput = '';

const onRender = () => {
if (ignoreRender) {
return;
}

const {output, staticOutput} = render(rootNode);

// If <Static> output isn't empty, it means new children have been added to it
const hasNewStaticOutput = staticOutput && staticOutput !== '\n' && staticOutput !== lastStaticOutput;

if (options.debug) {
if (hasNewStaticOutput) {
fullStaticOutput += staticOutput;
lastStaticOutput = staticOutput;
}

options.stdout.write(fullStaticOutput + output);
return;
}

// To ensure static output is cleanly rendered before main output, clear main output first
if (hasNewStaticOutput) {
log.clear();
options.stdout.write(staticOutput);
log(output);

lastStaticOutput = staticOutput;
}

if (output !== lastOutput) {
throttledLog(output);

lastOutput = output;
}
};

const reconciler = options.stdout._inkReconciler || createReconciler(onRender);

if (!options.stdout._ink) {
options.stdout._ink = true;
options.stdout._inkReconciler = reconciler;
options.stdout._inkContainer = reconciler.createContainer(rootNode, false);
let instance;
if (instances.has(options.stdout)) {
instance = instances.get(options.stdout);
} else {
instance = new Instance(options);
instances.set(options.stdout, instance);
}

let resolveExitPromise;
const exitPromise = new Promise(resolve => {
resolveExitPromise = resolve;
});

const unmount = () => {
onRender();
log.done();
ignoreRender = true;
reconciler.updateContainer(null, options.stdout._inkContainer);
resolveExitPromise();
};

const tree = (
<App stdin={options.stdin} stdout={options.stdout} exitOnCtrlC={options.exitOnCtrlC} onExit={unmount}>
{node}
</App>
);

reconciler.updateContainer(tree, options.stdout._inkContainer);
instance.render(node);

return {
waitUntilExit() {
return exitPromise;
},
unmount
rerender: instance.render,
unmount: instance.unmount,
waitUntilExit: instance.waitUntilExit,
cleanup: () => instances.delete(options.stdout)
};
};

0 comments on commit f7dbb97

Please sign in to comment.
You can’t perform that action at this time.