Giter VIP home page Giter VIP logo

sinuous's Introduction

Sinuous

Version Badge size codecov

npm: npm i sinuous
cdn: https://cdn.jsdelivr.net/npm/sinuous/+esm


  • Small. hello world at ~1.4kB gzip.
  • Fast. top ranked of 80+ UI libs.
  • Truly reactive. automatically derived from the app state.
  • DevEx. no compile step needed, choose your view syntax.

Add-ons

Size Name Description
Badge size sinuous/observable Tiny observable (included by default)
Badge size sinuous/map Fast list renderer
Badge size sinuous/hydrate Hydrate static HTML
Badge size sinuous/template Pre-rendered Template

Community

Examples


See complete docs, or in a nutshell...

View syntax

A goal Sinuous strives for is to have good interoperability. Sinuous creates DOM elements via hyperscript h calls. This allows the developer more freedom in the choice of the view syntax.

Hyperscript directly call h(type: string, props: object, ...children).

Tagged templates transform the HTML to h calls at runtime w/ the html`` tag or, at build time with sinuous/babel-plugin-htm.

JSX needs to be transformed at build time first with babel-plugin-transform-jsx-to-htm and after with sinuous/babel-plugin-htm.


Counter Example (1.4kB gzip) (Codesandbox)

Tagged template (recommended)

import { observable, html } from 'sinuous';

const counter = observable(0);
const view = () => html` <div>Counter ${counter}</div> `;

document.body.append(view());
setInterval(() => counter(counter() + 1), 1000);

JSX

import { h, observable } from 'sinuous';

const counter = observable(0);
const view = () => <div>Counter {counter}</div>;

document.body.append(view());
setInterval(() => counter(counter() + 1), 1000);

Hyperscript

import { h, observable } from 'sinuous';

const counter = observable(0);
const view = () => h('div', 'Counter ', counter);

document.body.append(view());
setInterval(() => counter(counter() + 1), 1000);

Reactivity

The Sinuous observable module provides a mechanism to store and update the application state in a reactive way. If you're familiar with S.js or Mobx some functions will look very familiar, in under 1kB Sinuous observable is not as extensive but offers a distilled version of the same functionality. It works under this philosophy:

Anything that can be derived from the application state, should be derived. Automatically.

import { observable, computed, subscribe } from 'sinuous/observable';

const length = observable(0);
const squared = computed(() => Math.pow(length(), 2));

subscribe(() => console.log(squared()));
length(4); // => logs 16

Use a custom reactive library

Sinuous can work with different observable libraries; S.js, MobX, hyperactiv. See the wiki for more info.

Hydration

Sinuous hydrate is a small add-on that provides fast hydration of static HTML. It's used for adding event listeners, adding dynamic attributes or content to existing DOM elements.

In terms of performance nothing beats statically generated HTML, both in serving and rendering on the client.

You could say using hydrate is a bit like using jQuery, you'll definitely write less JavaScript and do more. Additional benefits with Sinuous is that the syntax will be more declarative and reactivity is built-in.

import { observable } from 'sinuous';
import { hydrate, dhtml } from 'sinuous/hydrate';

const isActive = observable('');

hydrate(
  dhtml`<a class="navbar-burger burger${isActive}"
    onclick=${() => isActive(isActive() ? '' : ' is-active')} />`
);

hydrate(dhtml`<a class="navbar-menu${isActive}" />`);

Internal API

Sinuous exposes an internal API which can be overridden for fun and profit. For example sinuous-context uses it to implement a React like context API.

As of 0.27.4 the internal API should be used to make Sinuous work with a 3rd party reactive library like Mobx. This can be done by overriding subscribe, root, sample and cleanup.

Example

import { api } from 'sinuous';

const oldH = api.h;
api.h = (...args) => {
  console.log(args);
  return oldH(...args);
};

Methods

These are defined in sinuous/src and sinuous/h.

  • h(type: string, props: object, ...children)
  • hs(type: string, props: object, ...children)
  • insert<T>(el: Node, value: T, endMark?: Node, current?: T | Frag, startNode?: Node): T | Frag;
  • property(el: Node, value: unknown, name: string, isAttr?: boolean, isCss?: boolean): void;
  • add(parent: Node, value: Value | Value[], endMark?: Node): Node | Frag;
  • rm(parent: Node, startNode: Node, endMark: Node): void;
  • subscribe<T>(observer: () => T): () => void;
  • root<T>(fn: () => T): T;
  • sample<T>(fn: () => T): T;
  • cleanup<T extends () => unknown>(fn: T): T;

Note that some observable methods are imported into the internal API from sinuous-observable because they're used in Sinuous' core. To access all observable methods, import from sinuous/observable directly.

Concept

Sinuous started as a little experiment to get similar behavior as Surplus but with template literals instead of JSX. HTM compiles to an h tag. Adapted code from Ryan Solid's dom expressions + a Reactive library provides the reactivity.

Sinuous returns a hyperscript function which is armed to handle the callback functions from the reactive library and updates the DOM accordingly.

Contributors

Code Contributors

This project exists thanks to all the people who contribute. [Contribute].

Financial Contributors

Become a financial contributor and help us sustain our community. [Contribute]

Individuals

Organizations

Support this project with your organization. Your logo will show up here with a link to your website. [Contribute]

sinuous's People

Contributors

biw avatar brecert avatar canadaduane avatar davidmyersdev avatar dependabot[bot] avatar drsensor avatar luwes avatar lynlevenick avatar mindplay-dk avatar monkeywithacupcake avatar nettybun avatar raveclassic avatar thesherwood avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sinuous's Issues

How to render html

Hi how to render html in string
something like dangerouslySetInnerHTML in react or v-html in vue ?
Thanks.

Computed / Triggering a function

Suppose I have:

const strings = {
    'en': {'greeting': 'Good Morning'},
    'de': {'greeting': 'Guten Morgen'}
}

And an observable variable:

const lang = o('en')
And a function:

const i18n = (key) => {
    return strings[lang()][key]
}

Next, a cpl visual components:

<p>${lang}</p>

<h4>${i18n('greeting')}</h4>
And lastly a button:

<button onclick=${()=> lang('de')}>Switch Lang</button>

When I click the button the lang variable does indeed change (the p element is rendered), but the i18n function that powers the h4 element is not re-evaluated. Any ideas?

Investigate observable `runId` concept

Want to keep this here as a reminder. Mobx uses a run id for some optimizations which looked interesting but not sure it's worth the added bytes yet.

@localvoid explained the concept

@luwes I think that it is possible to get rid of all duplicates from observables to computeds at the cost of one additional property on observables, also it can improve reset performance (in most cases it won't be necessary to remove edges from observables to computed).

  1. When computed is executed, create a new execution id nextId++ and store it in the execution context(on stack) of this computed.
  2. When observable is accessed - push a new entry to the array of dependencies (computed -> observable) and assign computed execution id to observable lastAccessedBy to perform a simple check for duplicates.
  3. When computed is finished executing, iterate over all dependencies and refresh all lastAccesedBy to prevent edge cases when nested computeds could overwrite it.
  4. Iterate over old dependencies and remove observable->computed dependencies that have lastAccessedBy !== computed execution id. When observable is removed, assign lastAccesedBy to -1, if it still should remain in the list, assign -2.
  5. Iterate again over new dependencies and add observable->computed edges that doesn't have -2 in lastAccessedBy.

Just an idea, maybe I've missed something, maybe it isn't worth it :)

EDIT: Also, it is possible to detect when there aren't any nested computeds by checking that nextId isn't greater than current execution id + 1. And when there aren't any nesteds, we can skip step3.

EDIT2:

  • In step4 it is actually unnecessary to mark removed nodes with -1
  • In many cases we can also skip step5 by counting how many observable->computed should stay alive in step4 and then just comparing it with the length of new dependencies array (this optimization probably won't work when we detect nested computeds).

Builds and Contributing

Hi @luwes,

Are there any contribution guidelines? I'd like to contribute in some way if possible. If not to the core library, then make a helper module or two.

I've got a fork of the repo, but am having a devil of a time getting it to build. I'm trying to run npm run build, but I'm getting back a load of errors:

'sinuous/h' is imported by packages/sinuous/src/index.js, but could not be resolved โ€“ treating it as an external dependency
                sinuous.js โค  247 B
'sinuous/h' is imported by packages/sinuous/src/index.js, but could not be resolved โ€“ treating it as an external dependency
                sinuous.js โค  377 B
'sinuous/h' is imported by packages/sinuous/src/index.js, but could not be resolved โ€“ treating it as an external dependency
            sinuous.min.js โค  269 B
'sinuous/observable' is imported by packages/sinuous/src/index.js, but could not be resolved โ€“ treating it as an external dependency
'sinuous/h' is imported by packages/sinuous/src/index.js, but could not be resolved โ€“ treating it as an external dependency
 sinuous-observable.min.js โค  272 B
       babel-plugin-htm.js โค  1.78 kB
       babel-plugin-htm.js โค  3.62 kB
'sinuous/hydrate' is imported by packages/sinuous/all/src/all.js, but could not be resolved โ€“ treating it as an external dependency
'sinuous/observable' is imported by packages/sinuous/all/src/all.js, but could not be resolved โ€“ treating it as an external dependency
'sinuous/map/mini' is imported by packages/sinuous/all/src/all.js, but could not be resolved โ€“ treating it as an external dependency
'sinuous' is imported by packages/sinuous/all/src/all.js, but could not be resolved โ€“ treating it as an external dependency
[!] Error: Cannot create an explicit namespace object for module "all" because it contains a reexported external namespace
packages/sinuous/all/src/all.js

Any idea what I might be getting wrong?

sinuous and swiss ?

Hi, should I mix sinuous with your swiss, for component managment ? Is this goog idea ?, I can make components with sinuos itself, but swiss has a great element().

HTM docs

Add more info about sinuous/htm and sinuous/babel-plugin-htm.

Data gets not updated

When I try to render new data, where contents may changed, but same ID, the content gets not updated.

const data = observable([]);
  1. render:
var items = fetch_items_from_server();
data(items);
  1. render (updated contents):
var items = fetch_items_from_server();
data(items);

Is that a bug? What is the workaround to this issue?

Request: Context API

Are there any plans on the roadmap for a context api? I was looking at the implementation of Solid.js' context api:
https://github.com/ryansolid/solid/blob/master/packages/solid/src/signal.ts

It is minimal but it is also tangled up with the state/reactivity model. I don't know if there's any way to avoid that. I really like what Sinuous is doing with the multitude of small packages so that you only bring in what you need. It does seem like it would be a shame to add much to sinuous' super simple state/reactivity model. Is there a way to add a simple context system without messing with the reactivity model?

How to pass data from map to observable

In Event handler function I need both event and item
Then I have:

onclick = o(clicked);
clicked = (e, a) => {
    console.log('e:',e);
    console.log('a:',a);
    e.preventDefault();
  }
${map(this.categories, (item) => this.html`<li id=${item.id}><a class=${this.activeClass(item)} onclick="${this.onclick}" href="${item.id}">${item.name}</a></li>`)}

How to pass item to event handler with observable?

I can only:

onclick="${() => () => this.clicked(event, item)}

Thanks

Fav CSS approach

What is your recommendation when building a larger app: inline styles or css or do you have a fav css-in-js solution in the spirt of Sinuous?

Fix ES modules peer dependencies

Importing Sinuous with the new script module type doesn't work because the peer dependencies can't be resolved in the browser. e.g. sinuous/observable

<script type="module" src="https://unpkg.com/[email protected]/dist/sinuous.esm.js">

`memo` for memoizing components

Related to #6 and #56

import memoPropsFunction from "@luwes/memo";

const memo = fn => {
  const fragToArray = props => {
    // Add a root call here otherwise the removing of the component
    // would already automatically remove the observable subscriptions
    // This root will orphan this component's computations
    // and we'll be in charge of the clean up.
    const el = root(() => fn(props));

    // Memoizing a document fragment is impossible because
    // once it is appended, it's cleared of its children
    // leaving an empty shell, on next render the comp would just be cleared.
    // Store the child refs in an array and memo and return this.
    if (el.nodeType === 11) {
      return [].slice.call(el.childNodes);
    }
    return el;
  };
  return memoPropsFunction(fragToArray);
};

Request: explicit dependency observable

Hi @luwes et al,

First, thank you for sinuous. I haven't put it to hard use yet, but the tinkering I've done with your library has been a lot of fun. I'm looking forward to getting a lot of use out of it.

Looking at sinuous/observable, I didn't see any option for explicitly setting dependencies for an observable or computed function. S.js has S.on(...), for example. I find having that option pretty handy. And since you seem to favor global functions over dot notation, I thought watch([deps], fn) could work as an api.

Let me know if something like this falls within your vision for sinuous!

Best of luck and thanks again.

Observable object

Some research on whether an observable object should be added which is more in line with how Vue, Mobx and Solid use a proxy to the accessor of the observable.

This provides some advantages:

  • Simpler to get and set, state.color instead of color().
  • Easy JSON stringify

Disadvantages:

  • Always need to be called from the state object, users might find it strange the value can't be cached without losing the reactivity.

A `render` function or mode

I think this might be possible similarly to lit-html where they use the string part of the tagged template as a cache key and always return the same Node but update the dynamic parts on each render.

Sinuous could have a mode that instead of rendering once and update fine grained with the help of computeds, update with a render function which would also only create DOM once but update the dynamic values if they change with the api.insert and api.property calls.

All the logic is pretty much there from sinuous/template.
The only challenge is to create a unique key from the hyperscript calls that could be used for caching the DOM tree and getting the dynamic template fields/holes.

I need some help with my benchmark implementation

I want to provide a new test category where the performance of dealing with internal data comes back for libraries like surplus and sinuous. But I'm stuck on one specific step. Maybe you see the issue promptly.

This is the test implementation for your lib: https://github.com/nextapps-de/mikado/blob/master/bench/test/sinuous/internal.html
This is the specific line which fails: https://github.com/nextapps-de/mikado/blob/master/bench/bench.js#L424

I also tried to pass store reference to this method and apply like var data = items(); data[0].title = "foo"; items(data); but without luck, the changes are not rendered.

Would be really nice if I could solve this.

Plain computed in root doesn't return result

const title = o('Title');
html`${title}`

should return a string Title

html`${() => seq().map(i => html`<li>Counter #${i} ${counter}</li>`)}`

should return a DocumentFragment

Observable set works from other computed

This is currently not working

  var banana = o();
  var count = 0;
  S(() => {
    count++;
    return banana() + ' shake';
  });
  t.equal(count, 1);

  var carrot = o();
  S(() => {
    console.log('banana false');
    banana(false);

    carrot() + ' soup';

    console.log('banana true');
    banana(true);
  });

  carrot('carrot');
  t.equal(count, 5);

Question: dynamic components

I'm experimenting with some dynamic rendering of some components here:
https://codesandbox.io/s/dynamic-components-z69n5

I'm wrapping the dynamic component in a closure in order to make it reactive:

const view = html`
    <h3>Dynamic Components</h3>
    <button onclick=${addToList}>
      Add to List
    </button>
    <button onclick=${switchComponent}>
      Switch Component
    </button>
    <hr />
    ${() =>
      html`
        <${list()} items=${items} />
      `}
  `;

Is there a more idiomatic way of dealing with dynamic components? And if not, would it be worth considering some alternative to a closure with a nested html call?

This feels related to #6 on Conditional Rendering, which faced a similar closure and nested html call issue. As RyanSolid pointed out, the template must be recreated repeatedly.

global data store ?

Hi, What is proper way to share data between components? In vue, react there are state stores, but I dont want to use external libraries for it ? I think I should make global data container for it. But im not sure what is proper way. Thanks

Hydrate docs

Add a section to the docs on the hydrate module.

Event handler syntax

Currently attaching event handlers can look a bit strange if done without observables.

onclick="${() => () => count(count() - 1)}"

This is necessary because if the function was declared as an observable.

const countDown = observable(() => count(count() - 1));

it would look like

onclick="${countDown}"

in Sinuous this will be called to get the function from the observable

Event handler is countDown()


If the top code snippet was written as

onclick="${() => count(count() - 1)}"

it would execute the function immediately.

Usage with script tag?

I am getting Uncaught ReferenceError: o is not defined

<!DOCTYPE html>
<html lang="en">

<head>
</head>

<body>
  <script src="https://unpkg.com/sinuous"></script>
<script>
const counter = o(0);
const view = () => {
    return html`
    <div>Counter ${counter}</div>
  `;
};

document.body.append(view());
setInterval(() => counter(counter() + 1), 1000);
</script>
</body>
</html>

How ready is this?

Just the kind of simplicity and ergonomics that I was looking for in a JS library. However I have a few questions:

  • Can this be used in production?
  • If not, what features are missing?
  • Is there a roadmap?

Hydration method

Would be great to be able to "hydrate" DOM trees that are generated statically by the server or a static site generator aka HTML from a HTML file.

Question: reusable components

I'm struggling to figure out how to create and consume reusable components.

In React, with JSX, you'd create one like this:

const Button = (props) => {
   return (<button>{props.children}</button>)
}

and use like this:

<Button><Icon /></Button>

In Svelte, you'd create one like this:

<button>
  <slot />
</button>

and use like this:

<Button><Icon /></Button>

What is the idiomatic way to do this in sinuous with tagged templates?

Same key observable tags in `template` override

@ryansolid #4 (comment) brought up a good issue that same key observable tags would override the property on the data object. This would result in only the last tag action being called on that property change.

Potential solution is to keep an array of actions per key and run all on property change.

Hydration robustness

Adjacent text nodes are difficult to hydrate, check for a solution.
Good test by @ryansolid

<div>${greeting} ${name}<span>!</span></div>. From the server <div>Hi John Snow<span>!</span></div>

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.