totaljs/flow

Function Component

ColtonMcInroy opened this issue · 4 comments

I love the initial idea of the Teleport component but I found that it destroys the incoming message and creates a new message where the message gets teleported to. For handling HTTP requests, or handling messages that upon destroy result in an action becomes a bit of a problem making it unable to be used.
I wanted a component which would process data in another flow pattern and have the results sent back to the original message flow. This essentially creates a function that can be called anywhere within a flow without disrupting the flow of that message it is handling.
I cam up with the following component which is not fully fleshed out yet, it is based on the teleport component, only difference is that the original message is not destroyed, a new message is delivered to the output and when that message ends the data from that message is passed along the output of the input function component.

<script total>

	exports.name = 'Function';
	exports.icon = 'ti ti-globe';
	exports.author = 'Colton McInroy';
	exports.version = '1';
	exports.group = 'Common';
	exports.config = { type: 'input', target: '', targetName: '', name: '', color: '#B9261A', is: false };

	exports.make = function(instance, config) {

		instance.message = function($) {

			if (config.type === 'output') {
				$.send('output');
				return;
			}

			if (config.target) {
				var target = instance.main.meta.flow[config.target];
				if (target) {
					const msg = target.newmessage();
					msg.refs.originalMessage = $;
					msg.refs.controller = $.refs.controller;
					msg.on('end', (msg) => {
						$.send('output', msg.data);
					});
					msg.send('output', $.data);
					return;
					// $.toid = target.id;
					// $.instance = target;
					// target.message($);
					// return;
				}
			}

			$.destroy();
		};

		instance.configure = function() {

			config.is = config.name || config.target ? true : false;

			var inputs = [];
			var outputs = [];
			var is = false;

			switch (config.type) {
				case 'input':
					var target = config.target ? instance.main.meta.flow[config.target] : null;
					config.targetName = target.config.name;
					inputs.push({ id: 'input', name: 'Input' });
					outputs.push({ id: 'output', name: 'Output' });
					break;
				case 'output':
					config.targetName = '';
					outputs.push({ id: 'output', name: config.name });
					break;
			}

			var jsoninputs = JSON.stringify(instance.inputs || EMPTYARRAY);
			var jsonoutputs = JSON.stringify(instance.outputs || EMPTYARRAY);

			if (JSON.stringify(inputs) !== jsoninputs) {
				is = true;
				instance.inputs = inputs;
			}

			if (JSON.stringify(outputs) !== jsonoutputs) {
				is = true;
				instance.outputs = outputs;
			}

			is && instance.save();
		};

		instance.close = function(destroyed) {
			if (!destroyed || config.type === 'input')
				return;

			var meta = instance.main.meta;
			for (var key in meta.flow) {
				var tmp = meta.flow[key];
				if (tmp.component) {
					var com = meta.components[tmp.component];
					if (com &&
						com.name === 'Teleport' && tmp.config.type === 'input' && tmp.config.target === instance.id)
						tmp.reconfigure({ target: '' });
				}
			}
		};

		instance.configure();

	};

</script>

<readme>
The component can teleport (input) message to another teleport (output) component.
</readme>

<style>
	.CLASS .content { min-height: 10px; min-width: 50px; border-radius: var(--radius) var(--radius) 0 0; }
	.CLASS .input, .CLASS .output { border-top-color: transparent; }
	.CLASS .outputs { text-align: right; }
	.CLASS .colorbar { height: 10px; }
	.CLASS span { pointer-events: none; }
	.CLASS-settings figure { cursor: pointer; }
	.CLASS-settings .item { line-height: 28px; font-size: 12px; background-color: #F8F8F8; border-radius: var(--radius); margin-bottom: 2px; padding: 0 10px; cursor: pointer; }
	.CLASS-settings .item.selected { background-color: #E8E8E8; }
	.ui-dark .CLASS-settings .item { background-color: #333; }
	.ui-dark .CLASS-settings .item.selected { background-color: #404040; }
</style>

<settings>
	<div class="CLASS-settings padding">

		<ui-bind path="?.is" config="hide" class="block">
			<ui-component name="choose" path="?.type" config="selector:div" class="iconmenu m">
				<div data-id="input">
					<i class="ti ti-dot-circle"></i>
					<span>Input</span>
				</div>
				<div data-id="output">
					<i class="ti ti-crosshairs"></i>
					<span>Output</span>
				</div>
			</ui-component>
		</ui-bind>

		<ui-bind path="?.type" config="show:value=='output'" class="hidden block">
			<div class="row">
				<div class="col-md-9 m">
					<ui-component name="input" path="?.name" config="required:1">Name</ui-component>
				</div>
				<div class="col-md-3 m">
					<ui-component name="input" path="?.color" config="type:color">Color</ui-component>
				</div>
			</div>
		</ui-bind>

		<ui-bind path="?.type" config="show:value=='input'" class="block">
			<div class="caption m">
				<label>Where to send data?</label>
			</div>
			<ui-component name="choose" path="?.target" config="selector:.item">
				<ui-bind path="%functioninstances" config="template:.item -> data-id" class="listing block">
					<script type="text/html">
						{{ foreach m in value }}
						<div class="item" data-id="{{ m.id }}"><i class="ti ti-crosshairs mr5"></i>{{ m.name }}</div>
						{{ end }}
					</script>
				</ui-bind>
			</ui-component>
		</ui-bind>
	</div>
</settings>

<body>
	<header>
		<i class="ICON"></i>NAME <span data-bind="CONFIG.targetName__text">
	</header>
	<ui-component name="watcher" config="CONFIG">
		<script type="text/js">
			component.watcherbackup && component.unwatch(component.watcherbackup);
			var defcolor = '#888';
			var deftext = 'Not configured';
			var el = element.closest('.area');
			if (value.type ==='input') {
				if (value.target) {
					var target = 'flow.config.' + value.target;
					component.watch(target, function(path, config) {
						var conf = config || EMPTYOBJECT;
						el.find('.content').css('background', conf.color || defcolor);
						el.find('.inputs span').text(conf.name || deftext);
					}, true);
				} else {
					el.find('.content').css('background', defcolor);
					el.find('.inputs span').text(deftext);
				}
				component.watcherbackup = target;
			} else
				el.find('.content').css('background', value.color || defcolor);
		</script>
	</ui-component>
</body>

<script>

	TOUCH(function(exports, reinit) {

		exports.settings = function(meta) {

			var arr = [];

			for (var key in flow.data) {
				var tmp = flow.data[key];
				if (tmp.Component && tmp.Component.name === 'Function' && tmp.config.type === 'output' && key !== exports.id)
					arr.push({ id: key, name: tmp.config.name });
			}

			SET('%functioninstances', arr);
		};

		exports.configure = function() {

			var changes = exports.instance.changes;
			if (changes && changes.newbie) {

				var config = exports.config;
				var inputs = [];
				var outputs = [];

				if (config.type === 'input') {
					var target = flow.data[config.target];
					// inputs.push({ id: 'input', name: target ? target.config.name : 'Input' });
					inputs.push({ id: 'input', name: 'Input' });
					outputs.push({ id: 'output', name: 'Output' });
				} else
					outputs.push({ id: 'output', name: config.name });

				exports.instance.outputs = outputs;
				exports.instance.inputs = inputs;
				UPD('flow.data');
			}

		};

	});

</script>

This has become a very powerful component for me used in various places to make really nice flow patterns.

Hi @ColtonMcInroy, I have fixed the Teleport component. It contained a wrong implementation of the Web Component declaration.

I found that it destroys the incoming message

This is not true. The Teleport component does not destroy messages when input/output exists, otherwise it must destroy them to prevent zombie messages.

This is not true. The Teleport component does not destroy messages when input/output exists, otherwise it must destroy them to prevent zombie messages.

@petersirka You are correct, sorry, mixed up that with FlowStream Input/Output/Subscribe/Publish components.

The other feature this implements is very handy though.

image

The message data at the end of the Output Mode handler is returned to the Input Mode component as the payload of the message it outputs. This way the data is returned to the correct location in a flow. If you used teleport for instance to process logic in another location, you would have to use teleport to send it back. If that logic is to be applied to multiple locations, then this could become problematic. This component takes care of it.

Here is a copy of the html file version of this component
Function.html.tar.gz

@ColtonMcInroy - I do not have an email for you, but try our brand-new product UI Studio. This demo is primarily intended for the Flow as a UI Builder: DEMO (don't change the data there). Let me know your feelings.

Tomorrow I will look into this issue.

@ColtonMcInroy improve your code in the Function component:

<ui-component name="watcher" config="CONFIG">
    <script type="text/js">

        // IMPORTANT:
        if (!value)
            return;

        component.watcherbackup && component.unwatch(component.watcherbackup);
        var defcolor = '#888';
        var deftext = 'Not configured';