Giter VIP home page Giter VIP logo

runtime's Introduction

curvenote.dev

@curvenote/runtime

Runtime on npm MIT License Documentation CI

The runtime package allows you to create variables and components that react to changes in state through user-defined functions. The runtime is a small component that can be used in other packages to keep the state of a document reactive. The package is based on Redux which is compatible with many popular javascript frameworks (e.g. React, Vue, etc.).

Getting Started

This package is not setup directly for use in a browser, please see the @curvenote/components package to see it in use. For use in other packages, node, etc. you can download the latest release from npm:

>> npm install @curvenote/runtime

You should then be able to extend/integrate the runtime as you see fit:

import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunkMiddleware from 'redux-thunk';
import runtime, { actions, reducer } from '@curvenote/runtime';

// Create a store
const store = createStore(
  combineReducers({ runtime: reducer }),
  applyMiddleware(
    thunkMiddleware,
    runtime.triggerEvaluate,
    runtime.dangerousEvaluatation,
  ),
);

For more information on Redux or Redux Thunk, please see their docs and tutorials.

State Structure

The basic state structure is:

{
  runtime: {
    specs: {…},
    variables: {…},
    components: {…},
  }
}

Each of the sub-states, {…}, is a dictionary with uuid keys, to an object that represents a variable or a component.

  • specs: the definition of components, including properties and events. The variable spec is the only component spec included by default.
  • variables: holds the state of named values (e.g. numbers, strings, etc.), they cannot have events (other than changing the value of the variable)
  • components: an object that holds the state of a component (e.g. a slider, equation, etc. or more complicated widget). Components have properties that can be defined as functions, as well as named events (e.g. click, change, etc.) that are defined within the spec.

The state must be composed inside of an runtime dictionary. This allows you to compose the runtime state inside of your larger application, if required.

Variables

Variables have a name and a value and they can also be defined by a function (func). Depending on if a function is provided the variable will be derived, meaning that the function is used to evaluate the variable and provide the current value.

All components and variables also have a scope which is used to provide the variables by name when they are evaluated.

To create a variable, create a store and dispatch the createVariable action:

const x = store.dispatch(actions.createVariable('myScope.x', null, '1 + 1'));
const y = store.dispatch(actions.createVariable('myScope.y', 1));

The name must be a simple variable name, with an optional scope prepending the name, the default scope is "global". The value in this case of x is null and a function is provided as ('1 + 1') which will be evaluated by the middleware in the store.

Note: The functions provided are strings and their evaluation can be dangerous if you do not trust the source. Read more on the dangers on MDN, the runtime package uses a Function constructor not eval.

Get and Set Variable Properties

The dispatched action returns a shortcut that can be used to decrease the verbosity of further changes to the variable properties. Note that the current state and the value of the variable are often different. The variable is guaranteed to have the value only initialization, as other events may change its current value.

To get the current state of the variable:

let current = x.get();
// This can also be accessed through:
current = x.variable.current

All of the properties of the variable are contained within the variable object that is up to date with the state provided by the store.

To change the value of the variable, or provide a func for evaluation, this can be done through setting the variable:

x.set(42)
x.set(null, 'y')

In the second line, a function is provided referencing y, which will be evaluated as these variables live in the same scope.

Components & Specs

To define a new component you must first define a component spec. This lays out all of the properties that a component has as well as any events it may create.

Define a Spec

For example, a slider has the following spec:

export const SliderSpec = {
  name: 'slider',
  description: 'Range input or a slider!',
  properties: {
    value: { type: PropTypes.number, default: 0 },
    min: { type: PropTypes.number, default: 0 },
    max: { type: PropTypes.number, default: 100 },
    step: { type: PropTypes.number, default: 1 },
  },
  events: {
    change: { args: ['value'] },
  },
};

// Register this component spec
store.dispatch(actions.createSpec(SliderSpec));

The slider has a min, max, step and a value, when a user drags the slider, it creates a change event function and handler that has a single input to a function called "value" (which is not necessarily related to the value property 😕, more on that later.)

The name of the spec will need to be referenced when creating components of this type. As such that needs to be registered with the store, shown in the last line of the example above.

Create a Component

To create a range component, there must be a spec defined, and the properties and event handlers of this instance of the component can be defined. Note also that this component must live in a scope, which allows you to reference variables in that scope by name.

const slider = store.dispatch(actions.createComponent(
  'slider', 'scope',
  { value: { func: 'x' }, min: { value: 1 } },
  { change: { func: '{"x": value}' } },
));

In this case the current sliders state can be accessed in a few ways:

x.get() === slider.state.value
x.get() === slider.component.properties.value.current

Here we have created a component that is set up with two-way-data-binding to the variable x:

  1. when x changes the value property of the slider will also change; and

  2. when the slider is interacted with and dispatches a change event, that event evaluates the func:

function onChangeHandler(value) {
  return {"x": value};
}

This dictionary is used to update the variables in the state, and changes the value of x.

Responding to Component Events

As was mentioned before, you do not have to necessarily update the value of the slider (in this case it won’t move) or you may want to update multiple variables at the same time:

slider.set({}, { change: { func: '{ x: value, y: value + 1, z: value*2 }' }});

This changes the slider component to declare that when a change event happens, update:

  • x = value
  • y = value + 1
  • z = value * 2

Here the function has a single argument called “value” because that is what we defined in the spec:

events: {
  change: { args: ['value'] },
},

We could change this to any other string or add other required entries for the event. These variable names will overwrite any variables named that in the scope (or globally).

Remember these are arbitrary evaluated strings, so you can do anything that Javascript can do. This includes executing user defined functions:

function helloSliderInput(value) {
  console.log('The slider is updating to:', value);
  return { x: magicOtherFunction(value) }; // Or no return at all.
}
// Note, it does need to be accessible to the evaluation function!
window.helloSliderInput = helloSliderInput;

slider.set({}, { change: { func: 'helloSliderInput(value)' }});

You also have access to other variables in the scope from the evaluated function:

// ignore the value from the change event, and just set things to "x":
slider.set({}, { change: { func: 'helloSliderInput(y)' }});

runtime's People

Contributors

dependabot[bot] avatar rowanc1 avatar stevejpurves avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

runtime's Issues

Bring Spec/Components to base state

This allows for how I wrote the docs. :)

The flatter state is a bit more sensible, will require a few changes in the folder structure, but I think clearer overall.

createSpec

This doesn't actually work:

store.dispatch(actions.createComponentSpec(SliderSpec));

It requires you to break it out. The first arg should take a string or an object.

also:
createComponentSpec --> createSpec to match #2

createComponent

const slider = store.dispatch(actions.createComponent(
  'slider', 'scope',
  { value: { func: 'x' }, min: { value: 1 } },
  { change: { func: '{"x": value}' } },
));

This should follow the same pattern as #3 for taking an object.

This should simplify the scope/name issue. However, the default behavior should not name the component. Only set the scope.

This is getting overridden in the ink-basic packages. And components should not have names by default.

Feature suggestion: reactive aggregate r-var

I'm not sure if this is the correct repo to post this feature request, I'm sorry if it's not, I can repost elsewhere if necessary.

First off, I am very impressed at the concise expressivity iooxa offers in terms of reactive components, it's very easy and reliable once you understand how it's meant to be used! Awesome work, it's even better than the already great ink-components predecessor package :-D

I would like to suggest an extension on how variables are handled to make some of them reactive, just like r-display for example. This would be especially useful for variables that are defined as an aggregate of other variables, in other words are dependent on other variables, which is often useful for complex calculations to store and reuse intermediate results.

Currently, the only way to propagate changes is "top-down": the parent variables must update the children's variables values using the :change attribute. But this leads to an unnatural writing style in complex applications. This could be improved with a "bottom-up" approach where we directly define inside the children's variables definition what are the parent/dependent variables, similarly to what is done with r-display.

Let me show an example to clarify: let's say we make an app with 5 r-vars: a, b, c, the sum of a+b+c and the sum squared.

Here is what we currently need to write:

<script src="https://unpkg.com/@iooxa/article"></script>
<link rel="stylesheet" href="https://unpkg.com/@iooxa/article/dist/iooxa.css">

<div style="display: flex; flex-direction: column">
<r-var name="a" value="1" type="Number"></r-var>
<r-var name="b" value="10" type="Number"></r-var>
<r-var name="c" value="100" type="Number"></r-var>
<r-var name="sum" value="111" type="Number"></r-var>
<r-var name="sum_squared" value="12321" type="Number"></r-var>

<r-input label="a" :value="a" :change="{a: parseFloat(value), sum: parseFloat(value)+b+c, sum_squared: (parseFloat(value)+b+c)**2}"></r-input>
<r-input label="b" :value="b" :change="{b: parseFloat(value), sum: a+parseFloat(value)+c, sum_squared: (a+parseFloat(value)+c)**2}"></r-input>
<r-input label="c" :value="c" :change="{c: parseFloat(value), sum: a+b+parseFloat(value), sum_squared: (a+b+parseFloat(value))**2}"></r-input>
<r-input label="Sum" :value="sum" :change="{sum: value, a: value-b-c, sum_squared: value**2}"></r-input> <!-- Simply change one variable so that the input sum is correct -->
<r-input label="Sum squared" bind="sum_squared"></r-input>
</div>

Beside the issues with parseFloat which I mentioned in another issue elsewhere, the current approach leads to a lot of redundancy. Basically, the children r-var sum needs to be redefined inside each parent r-var's :change attribute, with pretty much the same code copy/pasted except that the current parent variable becomes value. Not only that, but all children of sum itself, such as sum_squared here, also need to be redefined at the level of each parents at all levels, hence here in both a, b, c and sum. We have only a 3 level depth tree here, the more depth the more redundancy.

Here is my "bottom-up" suggestion:

<script src="https://unpkg.com/@iooxa/article"></script>
<link rel="stylesheet" href="https://unpkg.com/@iooxa/article/dist/iooxa.css">

<div style="display: flex; flex-direction: column">
<r-var name="a" value="1" type="Number"></r-var>
<r-var name="b" value="10" type="Number"></r-var>
<r-var name="c" value="100" type="Number"></r-var>
<r-var name="sum" value="a+b+c" type="Number"></r-var>
<r-var name="sum_squared" value="sum**2" type="Number"></r-var>

<r-input label="a" :value="a" :change="{a: parseFloat(value)}"></r-input>
<r-input label="b" :value="b" :change="{b: parseFloat(value)}"></r-input>
<r-input label="c" :value="c" :change="{c: parseFloat(value)}"></r-input>
<r-input label="Sum" :value="sum" :change="{a: value-b-c}"></r-input> <!-- Simply change one variable so that the input sum is correct -->
<r-input label="Sum squared" bind="sum_squared"></r-input>
</div>

As you can see now the aggregate variables, sum and sum_squared, are now defined directly in r-var. The idea is that the value of sum should always reflect a+b+c. Hence in the :change attribute of the r-input of sum, we do not need to update the value of sum as it is always defined by the sum of a+b+c, we only need to change one of these parent variables (here I chose a for the sake of this example). Also I guess that in the runtime, if we make something reactive, it's then tied to these variables, so I guess that trying to set {sum: value} wouldn't work anyway.

So to summarize, sum and sum_squared are in this case a bit different variables than the standard r-var: whereas the r-var are meant to be directly set by user inputs, sum and sum_squared are only indirectly set, and are directly tied to other r-vars. That's why I call them "aggregate variables". In the above example I used :value="a+b+c" to define the dependency to the parents, but if it's simpler from an implementation standpoint to use a differently named attribute I see no issue.

About potential issues: it seems infinite recursion may be possible when updating reactive aggregate variables. In the example above, updating r-var a should update sum but then in cascade it should also update sum_squared. We could imagine two aggregates variables like: <r-var name="b1" value="a*b2"></r-var> and <r-var name="b2" value="a*b1"></r-var> with r-var a being a standard r-var that can be set with a r-input for example. Updating a would trigger an infinite loop between b1 and b2 in theory. I'm honestly not very experienced with javascript so I don't know how reactivity works technically, so I'm not sure how this could be handled, maybe with a list of variables to update to ensure the cascading only updates each children variable once when a is updated? Or just throw an error, that would work too.

Maybe this is not possible to implement, so in that case please disregard this ticket :-) It just spawned from my experiments to see how far iooxa could go, and as I found it can be used to make not just explorable explorations but even calculators and probably more when combined with custom javascript (or Brython) :-) The root of my thought started as very simple question, I was just wondering why I could not store a r-var for an intermediate result just like values of r-display while keeping the reactivity. In the end, as you can see in the concrete case that is the calculator I linked above, I used a mix of the first approach of duplicating code in all parent r-vars' inputs, and of avoiding the declaration of unnecessary intermediate resultst r-vars to minimize the depth of the dependency tree.

Arguments for properties

transform, for example, takes an argument scoped within the component.

export const InkRangeSpec = {
  name: 'range',
  description: 'Range input',
  properties: {
    value: { type: types.PropTypes.number, default: 0 },
    min: { type: types.PropTypes.number, default: 0 },
    max: { type: types.PropTypes.number, default: 100 },
    step: { type: types.PropTypes.number, default: 1 },
    transform: {type: types.PropTypes.string, default: '', args: ['value'], has:{func: true, value: false}}
  },
  events: {
    change: { args: ['value'] },
  },
};

How to do chart equation?

I am not really sure how to get the mapped function.

I want to return this:

() => Math.sin(x)

So that I can:

array.map((x) => [x, func(x)])

And get a chart ....
image

Problem is that that requires a function type? And the array doesn't exist on the object, so it can't really be a property remap. :(

Needs more thought.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.