Coder Social home page Coder Social logo

unknot's Introduction

unknot

npm version

Unknot is an experiment combining FRP patterns with DOM querying, to attempt to remove the complexity of the page lifecycle when declaring relationships between events and DOM manipulation.

Unknot is implemented using the Kefir, and the examples below assume some knowledge of Functional Reactive Programming patterns (TODO: make this not so).

The goal is to create a declarative environment for smaller applications that don't require the complexity of React (or the view rendering); where jQuery might be the tool of choice for adding a handful of interactions to the page.

Why?

Often, when setting up any client-side JavaScript, program setup is deferred until after the DOM content is loaded on the page to ensure that elements exist before they are queried.

// With jQuery as $:
$(document).on("ready", () => {
  const $foo = $(".foo");

  $foo.css({
    backgroundColor: "red"
  });
});

Unknot takes an alternative approach, allowing queries and transformations to be defined before page load, and delaying any manipulations until it is possible to do so.

// With Unknot as $:
const $foo = $(".foo");

$foo.style({
  backgroundColor: Kefir.constant("red")
});

Unknot understands when it is possible to apply the declared styles, and doesn't attempt to assign them until the appropriate moment. In addition, styles are defined as observables, so any time the style-state is updated, the new styles are applied.

Installation

npm install @unknot/unknot

Setup

The above example is somewhat simplified, as it omits two lines of required setup code necessary for unknot programs. Unknot still needs to know when queries should be attempted, and doesn't make assumptions about this by default. This information is supplied as an event stream passed to the unknot function. In most cases, the following setup code is sufficient to begin your unknot program:

import Kefir from "kefir";
import unknot from "unknot";

// Watch the document for the first `DOMContentLoaded` event as an event stream.
const loaded = Kefir.fromEvents(document, "DOMContentLoaded");

// Supply the `loaded` event stream to `unknot` to create the query function.
// $ can be named anything that makes sense for your application.
const $ = unknot(loaded);

From this point, you can use the query function $ (or the name your choice) in the rest of your program.

API

Styles

CSS styles are applied to an element using the style method.

// With jQuery as $:
$(document).on("ready", () => {
  const $foo = $(".foo");

  $foo.css({
    backgroundColor: "red"
  });
});

// With Unknot as $:
const $foo = $(".foo");

$foo.style({
  backgroundColor: Kefir.constant("red")
});

Rather than suppling an object literal of CSS rules to be applied once, unknot expects each value in the object to be a property stream (this is why Kefir.constant is used above, as it returns a property stream with a single, unchanging value). As the property's value changes, the rules are reapplied to the element with the new style definition.

// With jQuery as $:
$(document).on("ready", () => {
  const $foo = $(".foo");
  const colors = ["red", "green", "blue"];

  setInterval(() => {
    const randomColor = colors[Math.floor(Math.random() * colors.length)];

    $foo.css({
      backgroundColor: randomColor
    });
  }, 1000)
});

// With Unknot as $:
const $foo = $(".foo");
const colors = ["red", "green", "blue"];
const randomColor = Kefir.fromPoll(
  1000,
  () => colors[Math.floor(Math.random() * colors.length)]
);

$foo.style({
  backgroundColor: randomColor
});

When a property stream emits null, the corresponding CSS attribute is removed.

// Sets initial value to `red`, then removes `backgroundColor` after 5 seconds
$foo.style({
  backgroundColor: Kefir.later(5000, null).toProperty(() => "red")
});

The style method should only be called once per element.

Events

Event handlers streams can be defined using the events method. Event handlers can also be defined before the DOM has finished loading.

For example, a small program that changes the opacity of an element to zero when it is clicked:

// With jQuery as $:
$(document).on("ready", () => {
  const $foo = $(".foo");

  $foo.on("click", () => {
    $foo.css({
      opacity: 0
    });
  });
});

// With Unknot as $:
const $foo = $(".foo");
const clicks = $foo.events("click");

$foo.style({
  opacity: clicks.map(() => 0)
});

A shorthand for calling preventDefault on this event stream is available. The following two lines are equivalent:

clicks.observe(e => e.preventDefault());
// vs.
clicks.preventDefault();

Classes

Using the className method, classes are added or removed by supplying a property stream that emits boolean values. The supplied class name will be applied when the property stream is true, and removed when it is false.

// With jQuery as $:
$(document).on("ready", () => {
  const $foo = $(".foo");

  $foo.on("click", () => {
    $foo.toggleClass("foo--active");
  });
});

// With Unknot as $:
const $foo = $(".foo");
const clicks = $foo.events("click");
const active = clicks.scan(previous => !previous, false);

$foo.className("foo--active", active);

Attributes

The attribute method provides access to element attributes as a property stream.

// With jQuery as $:
$(document).on("ready", () => {
  const $foo = $(".foo");
  const $bar = $(".bar");

  $foo.style({
    height: $bar.height()
  });
});

// With Unknot as $:
const $foo = $(".foo");
const $bar = $(".bar");
const barHeight = $bar.attribute("offsetHeight");

$foo.style({
  height: barHeight.map(h => `${h}px`)
});

NOTE: Using attribute will only read the attribute once when the document is loaded.

Optional elements

If elements are not present in the document, Unknot will throw an error when they are queried. If there are certain elements that may not be present in the document, using $.maybe instead of $ makes the query optional. Any manipulations or attribute accesses on a $.maybe query are ignored, and dependent code is not executed.

For example, using jQuery, you might check if an element exists before getting some information about it, and then conditionally applying the result:

// With jQuery as $:
$(document).on("ready", () => {
  const $slideshow = $(".slideshow");

  if ($slideshow.length) {
    complexSetup($slideshow);
  }
});

// With Unknot as $:
const $slideshow = $.maybe(".slideshow");

$slideshow.observe(complexSetup);

Using $.maybe, if the element is not found, any downstream code is skipped, without needing to check for the element's existence using a conditional.

Extending unknot

The unknot query object can be extended with additional functions by supplying a member parameter when creating the query function. Additional functions must take the form of a function that accepts a stream of elements as the only argument, and returns the function that will be added to the return value of the unknot query.

For example, if you wanted to add a function "data" for reading dataList attributes, it could be added like this:

const $ = unknot(loaded, {
  member: {
    data: element => name => element.map(e => e.dataList[name])
  }
});

// Given: <div class="foo" data-bar="baz"></div>
const $foo = $(".foo");
const bar = $foo.data("bar");

bar.log();
// <value> "baz"

Setting values can also be added the same way. A function for setting the text of an element could be implemented like this:

import { Kefir as K } from "kefir";

const $ = unknot(loaded, {
  member: {
    text: element => text =>
      K.combine([element, text]).observe(([e, t]) => {
        e.innerText = t;
      })
  }
});

const $foo = $(".foo");
const countdown = K.sequentially(1000, [3, 2, 1, "Go!"]);

$foo.text(countdown);

unknot's People

Contributors

jackjennings avatar

Stargazers

 avatar

Watchers

 avatar  avatar  avatar

unknot's Issues

Add function for assigning text content of an element

For example, I wanted to react to a slideshow changing the current slide by writing the slide's caption into another element on the page. This could be something like:

$foo = $(".foo");
bar = K.constant("bar");

$foo.text(bar);
// or, to mirror regular JS interface…
$foo.innerText(bar);

Differentiate getter and setter streams

Does there need to be a differentiation between functions that set/mutate something, and functions that return a property stream that reads something?

For example, there could be a function children that appends a set of child nodes to an element. There could also be a function children that returns a property stream of child elements. How to avoid that name collision?

Extend query result object

It should be possible to supply the query constructor function with an additional object of functions to extend the streams that are returned, so that it's possible to easily augment the functionality of unknot for a specific application.

Something like:

const $ = unknot(loaded, {
  extend: {
    child: element => selector => element.map(e => queryChild(e, selector))
  }
});

$foo = $(".foo");
$bar = $foo.child(".bar");

Not really convinced that extend is the correct key to supply for this argument.

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.