0.0.16 • Published 1 year ago

bindom v0.0.16

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

bindom

Tiny, powerful data binding & web application framework

Hello world!

<html>
	<body>
		<span data-bind=greet:§> </span> <span data-bind=whom:§> </span>!
		<script type=module>
			import $ from "../bindom.min.mjs"
			$(document.body).bind({greet: "Hello", whom: "world"});
		</script>
	</body>
</html>

Features

  • 2-Way binding between DOM and JavaScript Objects
  • full DOM access
  • unified handling of events, attribute-, property-, and text-changes
  • scoped binding
  • web component based view-modules with JS-code, HTML-templates and styles
  • dynamically binding added DOM and cleaning up removed DOM bindings
  • conditional rendering
  • iterative rendering (rendering from array-data)
  • ponyfill-style non-intrusive behavior, that only ever applies where you invoke it

Why?

Yet another frickin' web framework??? Yup, sorry. Size matters. Standards matter. Simplicity matters. Performance matters. Style and power matter.

I want a framework that is so small, that it's easy to maintain. Because if you use it, you own it. You have to update it until it falls out of fashion and then take over maintenance completely. Bindom is 800 lines plus Quary (another few hundred lines). This gives me a complete web application framework and DOM manipulation toolkit (Quary) at maybe 1200 lines. More than I'd like, but still maintainable.

I want a framework, that leverages standards. As little framework specific syntax as possible. Bindom ships three ECMAScript methods and three web components. The web components use six custom attributes, and two of those are for edge cases. Bindom code will be easily readable by developers 20 years from now.

I want a framework, that just works and works without magic. No compilation, no custom language, nothing (if you actually understand, how whisker/handlebars binding works, congratulations, you're one of the few - I consider this magic). And if I bind some nodes, I want the rest of the DOM left alone.

I want a framework, that gives me awesome performance where I need it. A decent web app should clock in at maybe 100K, not megabytes. Bindom is below 8K gzipped (including Quary). And this is not optimized for size. No magic means I know what's going on and can optimize for CPU or RAM where needed.

Finally I want a framework that helps me structuring script and DOM but does not get in my way. I want easy power, native DOM APIs where I need them and nice bindings where I don't.

Demo

Bindom comes with an app demo, that shows most features you'll need to write web apps. You may

git clone https://github.com/schrotie/bindom.git
cd bindom
npm install
npm start

This should open the demo in your browser. Note this will only work in Chrome as of this writing. Other browsers need importmaps support as described below! Check out the source code in demo/app/ and see in your browser, what it does. Change it! The browser should automatically reload. I guess, that's the quickest way to learn Bindom for most of you.

You may come back to the documentation in order to understand, how the features work, what's actually going on there.

Documentation

Quary is a "micro-library for writing vanilla web components". Check it out, if you care. Bindom extends Quary by two methods: bind and undbind. When Bindom gives you DOM-nodes, they are usually wrapped in a Quary object. While this can be quite handy, you may ignore it, if you don't care: Quary extends Array and is thus just an array of nodes with some utility methods for DOM manipulation and monitoring.

Quary can help you with selecting nodes, but you can also just pass it a node and then call the bind method on it. The bind method expects one argument - a JavaScript object to which to bind the selected DOM. For example (see Hello world! above):

import $ from "bindom"
$(document.body).bind({greet: "Hello", whom: "world"});

Bindom will then search the respective DOM for data-bind attributes and bind each HTMLElement with such an attribute to the object as specified in the attribute.

Note that the bound object may be an instance of a specific class, designed to interact with the specific bound DOM. Only thus you can leverage the full power of Bindom.

import 'bindom', Development & Deployment

I hedge a deep and well-fostered hate against having a build step in my development cycle and my apps work without one. If you want to import bindom you have two options: import the bundled bindom.min.mjs. You may use absolute paths to its position in your node_modules folder and be somewhat fine.

However, if you want to import its unmingled source version from bindom.mjs, you will need to tell it where to find Quary, on which it depends. The solution to making this work without a build step is import-maps which are supported in the major browsers with the exception of IE 12 (aka "Safari").

Put something like this into the head of your HTML:

<script type="importmap">{"imports": {
	"bindom": "./node_modules/bindom/bindom.mjs",
	"quary": "./node_modules/quary/quary.mjs"
}}</script>

and you're good to go for development. You can then just import 'bindom' in your code and develop on your sources, not some weird artifact that resembles what you developed after having that artifact and the browser jump throw a dozen or so hoops.

For production you absolutely want to have a build step that bundles your app. I recommend rollup for this. It's the perfect tool for this task.

Methods & Events

Bindom adds two methods to Quary:

const bindToThisObject = {};
$(document.getElementById('bindThisElement'))
	.bind(bindToThisObject)
	.unbind(bindToThisObject);

bindToThisObject is optional in both call.

bind will scan bindThisElement and its decendants for data-bind attributes and bind accordingly (see below). If no argument is passed, it will bind to a default {} object. When done, it will emit a bound event on bindThisElement. The event will contain the bound bindToThisObject as detail.

unbind will revert the binding. If no argument is passed, it will remove all bindings, otherwise only those to the passed object. Before unbinding it will emit a beforeUnbind event on bindThisElement. When done unbinding it will emit an removedBound event on bindThisElement.

data-bind

Introduction

Each binding consists of two parts that are separated by a colon: The bound property or method of the object on the left and the bound DOM-stuff on the right.

What the respective DOM-stuff is, is determined by the leading character, or by dangling parantheses if it's a DOM method:

  • @ attribute value
  • . DOM property
  • ! event
  • § child text node
  • () method
  • * the bound HTMLElement itself

So assume, you want to change the class attribute of a node from JavaScript. You could give it a data-bind=myClass:@class. If you then do bound.myClass = 'foo', the node's class attribute will become foo.

However, sometimes you want to toggle some class, while leaving other classes alone. Then you could do data-bind=drawer:.classList;fold:!click. Note that you can combine several binding statements by concatenating them with semicolons. This binding binds the classList property and the click event. A class for dealing with this might look like this:

class Bound {
	set fold(clickEvent) {this.drawer.toggle('open');}
}

You could also trigger the click event yourself like this:

class Bound {
	triggerClick() {this.fold = new CustomEvent('click');}
}

Binding Types

So here are examples for all binding types:

TypeBinding
Attributedata-bind=objPropA:@data-dom-attribute
Propertydata-bind=objPropP:.domProperty
Textdata-bind=objPropT:§
Eventdata-bind=objPropE:!domEventName
Methoddata-bind=objMethod:domNodeMethod()
Nodedata-bind=objPropN:*
Atributes, Properties and Text

Note that on the object everything translates to properties, except for DOM methods, which become methods on the object, too. You can always access bound stuff from the JavaScript object.

If you take the attribute binding from the table above,for example, object.objPropA will return the current value of the attribute of the bound node. The attribute value is not stored somewhere, but read from the DOM when you call the property getter object.objPropA. It will then call node.getAttribute('data-dom-attribute').

You can also always do object.objPropA = 'x'. This will invoke node.setAttribute('data-dom-attribute', 'x'). All this also works if you bind the same object property to several nodes. In this case the getter will return the value of the "first" bound element.

This works exactly the same with property and text bindings.

Events, Methods and Nodes

You cannot get events (object.objPropE will raise an exception) and you cannot set methods or nodes (object.objMethod = 'x' and object.objPropN = 'x' will each raise exceptions).

Nodes you can only get and then have full access to their DOM APIs.

Methods can only be called. object.objMethod() will call the node's domNodeMethod with the same arguments.

You can assign to events, but you should assign DOM events to them, e.g. object.objEvent = new CustomEvent('foo', {detail: 'bar'}). This will emit the event on the bound node. Since Bindom uses Quary's emit method for this, you may assign anything, that Quary's emit method supports, see Quary's documentation for details.

Change Notifications

Bindom can also track changes and events in the DOM for you. It will do that, if it finds a setter for the bound property on the bound object. Let's assume the JavaScript class of the object bound in the table above looks like this:

class MyDomHandler {
	set objPropA(a) {}
	set objPropP(p) {}
	set objPropE(e) {}
	set objPropT(t) {}
}

Then Bindom will call the respective setter when the bound attribute, property, or text changes, or (objPropE!) if the event domEventName is triggered on the bound DOM node.

Quoting Bindings & Multiple Bindings

You may have noticed, that the data-bind attributes are usually listed without quotes. Quotes for attributes are only required in some situations by the HTML standard and Bindom's data-bind is designed to not require quotes. You may add them if you like, though.

You can define several bindings by concatenating them with ;, e.g.

<span data-bind=text:§;className:@class;style:.style> </span>

When you do use quotes, though, you may also concatenate bindings with newlines for better readability, especially when you have a lot of bindings. The example above may also be written:

<span data-bind="
                 text:§
                 className:@class
                 style:.style
                "
> </span>

Scoping

The DOM is organized in a tree. When you bind a node, Bindom will also bin all its decendants, i.e. the whole tree-branch of that node. However, if Bindom encounters a node that has a data-class attribute, Bindom will stop there. The node with the data-class attribute will also be bound, but not its decendants. And special rules will apply to that last node.

The significance of data-class will become clear, when discussing <bind-dom>. For now let's suffice it to say, that data-class signifies the opening of a new scope. Only the node with the data-class attribute may receive a binding from the ancestor scope above in the tree. The decendants of that node are out of scope for the ancestor.

Such a parent-/ancestor-binding is signified by a leading ^ in the object property name. For example

<div
	data-class=X
	data-bind=^parentObjProp:!someEvent;newScopeObjProp:@class
><!-- ... --></div>

will bind the parentObjProp of the parent-scope to the someEvent-event of the new scope, and newScopeObjProp of the new scope to the @class attribute of the host node of the new scope. The host node is always the node (or collection of nodes) on which bind was called. And only host nodes can have parent scope bindings.

Dynamic Binding

Bindom tracks additions to and removals from the DOM tree. When a node is removed that was bound by Bindom, it will then be unbound. And under certain ciscumstances, Bindom will bind new nodes to existing scopes.

The contents of HTMLTemplateElements is parsed but mostly ignored by the browser. When binding scopes, Bindom looks for template elements and tags the content as belonging to the respective scope. When the template content is instantiated, Bindom will bind it to the respective scope/object, regardless of where the nodes are inserted!

This will usually look quite nice and intelligible in your code, but depending on where you insert stuff, the live DOM could look somewhat labyrinthine (it usually doesn't but it may).

<bind-dom>

The <bind-dom> custom element is where Bindom got its name. <bind-dom> is how you write web apps and organize your code using Bindom.

<bind-dom> does what it says - it binds DOM. And it binds it to an object of the class you give it in its data-class attribute. That's why Bindom considers data-classto be the harbinger of a new scope.

The order of events is like this: 1. you tell Bindom, that you have a nice class you'd like to use 2. you put a <bind-dom> element into the DOM that references that class 3. <bind-dom> does its thing

Note that 1. may also come after 2.. Bindom will wait three seconds for a class it should instantiate to be declared and will then raise an exception, if the class was still not declared. That should be ample time because usually you should bundle your production code. For real dynamic loading of modules you should implement a component, that does what you need and only sets data-class once you're good to go.

You declare a class to Bindom by calling Bindom's addClass method:

import {addClass} from 'bindom';
addClass(class MyDomHandler {});

Bindom will call the constructor of your class without arguments and bind it to the DOM inside (and including) the <bind-dom> element.

<bind-dom> will add the bound class instance as property proxy to the bound <bind-dom> element.

Note that you can use bindom's lifecycle events as lifecycle callbacks in your class. Just subribe to the host node's bound and beforeUndbind events, preferably using bindHost (see below).

template

addClass actually accepts an optional argument besides your (mandatory) class: a configuration object, that may (and usually should) contain a template property. template must be a string defining the HTML for your class to manage. <bind-dom> will 1. transform the template string into an actual HTMLTemplateElement 2. clone the template's content and append it to itself 3. bind a new instance of your class to that.

Please check out the app demo for several examples of this.

style

A second optional property of the configuration object accepted by addClass is style. There you may add CSS for your module. The style-string will be only ever added once to the page's styles.

Bindom will also add the name of the bound class to the class attribute or the respective <bind-dom> node. You could also use [data-class=MyDomHandler] as a CSS selector to scope the CSS to your module, but .MyDomHandler is a much nicer selector.

bindHost

I recommend putting template, style, and JavaScript class of your modules together into one file. When that gets unwieldly you should cut up the module instead of separating DOM, style, and JavaScript.

However, that means, you'll usually not see you host node together with the rest of the code of a module. And if you need to data-bind anything on your host node you would have to remember to put that down, where that node is instantiated. And if that happens in more than one place ...

In order to circumvent this madness you may pass the third and last supported property to addClass's configuration object: the bindHost configuration. This will be added to the data-bind of your host node. If there already are parent bindings, these will be left alown and still work.

Dynamic data-class

You may change the data-class attribute of a <bind-dom> element anytime. <bind-dom> will then clean up the old bound stuff (remove the template, unbind ...) and invoke the new stuff.

<template is=if-dom>

if-dom is Bindom's conditional rendering facility. It is a custom HTMLTemplateElement (you'll need a polyfill for this to work in IE 12, aka "Safari") that renders its content before the template, when its .if property is true.

Thus you'll usually want to bind to if-dom's .if-propety:

<template is=if-dom data-bind=render:.if>...</template>

data-class

You may add data-bind-attributes to elements inside the template and they will be bound to the parent scope when instantiated, except if you give if-dom an optional data-class attribute. In the latter case it will behave like a <bind-dom> that only renders, if its .if property becomes true.

data-parent & data-insert

These two attributes allow you to customize where the template content is inserted. If you give if-dom a data-parent attribute it will walk up the DOM-tree and call querySelector(this.dataset.parent) until it finds a matching node. Then it will append its content or call the DOM insertion method (as supported by Quary!) you gave it with the data-insert attribute.

<template is=loop-dom>

loop-dom is somewhat similar to if-dom. But it will instantiate its content once for each element of the array you give it in its .array property. Thus you'll usually bind to its .array property. However, since loop-dom always opens a new scope, you must always use a parent binding for this:

<template is=loop-dom data-bind=^myData:.array>...</template>

The array you pass must contain objects, and each template instance will be bound to one such object.

You may give loop-dom a data-class attribute. If you don't it will just bind to the objects you pass to it, but if you do, it will instantiate the respective class (and additionally instantiating style and template string if applicable), Object.assign(newInstance, arrayObject) and then bind newInstance.

loop-dom also supports data-parent and data-insert attributes.

Custom Elements

Bindom is intended to help with code organization, facilitate separation of easily testable pure JavaScript code and DOM API and mostly implement views for (sub-)pages.

It may also help with complex custom elements in some cases, but usually you'll be better off using something like Quary or native APIs for such. Since Bindom comes with Quary included, you already got a library intended for implementing vanilla web components.

State Management

State management is the final part missing for writing modern web applications without leveraging god-frameworks like AngularVueReact. If you intend to write a somewhat complex web application you should probably use Redux from the start. Redux is React-agnostic, lean, powerful and just great for writing complex apps.

However, most apps are much simpler than the complexity level calling for Redux. And if you write something simple, you should use something simple for state management. Since I didn't find something that satisfied my likings, I implemented xt8.

I found that with the triumvirate of Quary, Bindom, and Xt8 I can very quickly implement high performance quality web apps that are built to last, because they are built on standards, are very lean and play nice with anything else you want to use.

I encourage you to find your own stack that is not godframework XY. And I hope you find Bindom useful.

Cheers, Thorsten

0.0.15

1 year ago

0.0.16

1 year ago

0.0.11

2 years ago

0.0.12

2 years ago

0.0.13

2 years ago

0.0.14

1 year ago

0.0.10

2 years ago

0.0.9

2 years ago

0.0.8

2 years ago

0.0.7

2 years ago

0.0.6

2 years ago

0.0.5

2 years ago

0.0.4

2 years ago

0.0.3

2 years ago

0.0.2

2 years ago

0.0.1

2 years ago