Skip to content

◌ Declarative DOM manipulation, alpha-quality

Notifications You must be signed in to change notification settings

unknot-js/unknot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

55 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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);