JSPM

  • Created
  • Published
  • Downloads 13
  • Score
    100M100P100Q53213F
  • License ISC

Simple and declarative data binding for the DOM

Package Exports

  • synergy

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (synergy) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

synergy

npm Build Status Coverage Status gzip size

Simple and declarative data binding for the DOM.

Table of Contents

Features

  • Simple and declarative way to bind data, events, and markup
  • Small footprint (~3.6k)
  • No special tooling required (e.g., compilers, plugins)
  • Minimal learning curve (almost entirely standard HTML, JS, and CSS!)

Browser Support

Works in any modern browser that supports JavaScript Proxy.

Install

Using npm:

$ npm i synergy

Using unpkg CDN:

<script type="module">
  import synergy from 'https://unpkg.com/synergy';
</script>

Render

The render() method combines an HTML template with a JavaScript object and then mounts the rendered HTML into an existing DOM node.

Syntax

let view = synergy.render(targetNode, viewmodel, template);

Parameters

  • targetNode An existing HTML element node where the rendered HTML should be mounted.

  • viewmodel A plain JavaScript object that contains the data for your view.

  • template Either an HTML string or a <template> node.

Return value

A proxied version of your JavaScript object that will automatically update the UI whenever any of its values change

let view = synergy.render(
  document.getElementById('app'),
  { message: 'Hello World!' },
  `<p>{{ message }}</p>`
);

view.message = '¡Hola Mundo!';

In the example above, we initialise the view with a paragraph that reads "Hello World!". We then change the value of message to '¡Hola Mundo!' and Synergy updates the DOM automatically.

Data binding {{ ... }}

Use the double curly braces to bind named properties from your JavaScript object to text or attribute values within your HTML template.

<p style="background-color: {{ bgColor }}">{{ message }}</p>

As far as text nodes are concerned, the values you bind to them should always be primitives, and will always be cast to strings unless the value is null or undefined, in which case the text node will be empty.

Attributes, on the other hand, support binding to different data types in order to achieve different goals...

Attributes & Booleans

Any attribute that is bound to a boolean value will be treated as a boolean attribute, unless it is an ARIA attribute, in which case the boolean value will be cast to a string.

CSS Classes

You can bind multiple values to an attribute with an array.

{
  classes: ['bg-white', 'rounded', 'p-6'];
}
<section class="{{ classes }}">
  <!-- class="bg-white rounded p-6" -->
</section>

You can use an array to bind multiple values to any attribute that accepts them

Conditional Classes

You may wish to conditionally apply CSS classes to an element. You can do this by binding to a an object. Only the keys with truthy values will be applied.

{
  classes: {
      'bg-white': true,
      'rounded', false,
      'p-6': true
    },
}
<section class="{{ classes }}">
  <!-- class="bg-white p-6" -->
</section>

Inline Styles

As well as binding the style attribute to a string or an array, you can also bind this attribute to an object representing a dictionary of CSS properties and values.

{
  goldenBox: {
    backgroundColor: 'gold',
    width: '100px',
    height: '100px'
  }
}
// -> "background-color: gold; width: 100px; height: 100px;"

Getters

Define any property as a standard JavaScript getter in order to derive a value from other values within your viewmodel.

{
  width: 100,
  height: 100,
  get styles() {
    return {
      backgroundColor: 'gold',
      color: 'tomato',
      width: this.width + 'px',
      height: this.height + 'px',
    }
  }
}
<section style="{{ styles }}"><!-- ... --></section>

JavaScript Expressions

Synergy doesn't allow you to write arbitrary JavaScript expressions inside your templates. This helps to keep a clearer separation of concerns between your JavaScript and your HTML. That being said, there are a couple of simple expressions that are supported to make working with attributes a little easier...

Logical Not ( ! )

You can prefix a property name with an exclamation mark in order to negate it.

<h3>
  <button id="{{ id }}" aria-expanded="{{ expanded }}">{{ title }}</button>
</h3>
<div aria-labelledby="{{ id }}" hidden="{{ !expanded }}">
  <!-- ... -->
</div>

Object Spread ( ... )

You can prefix a property name with an ellipsis to spread all of the keys and values of an object onto an element as individual attributes.

      {
        slider: {
          name: 'slider',
          type: 'range',
          min: '0',
          max: '360',
        },
      }
<input {{...slider}} />

Repeated Blocks

Repeat a block of HTML for each item in an Array or Set by surrounding it with the each opening (#) and closing (/) comments.

{
  names: ['kate', 'kevin', 'randall'];
}
<ul>
  <!-- #each name in names -->
  <li>Hello {{ name }}</li>
  <!-- /each -->
</ul>

Access the current index with the dot character

<ul>
  <!-- #each todo in todos -->
  <li>
    <p>todo {{ . }} of {{ todos.length }}</p>
  </li>
  <!-- /each -->
</ul>

Repeated blocks can have multiple top-level nodes

<!-- #each drawer in accordion.drawers -->
<h3>
  <button id="{{ id }}" aria-expanded="{{ expanded }}">{{ title }}</button>
</h3>
<div aria-labelledby="{{ id }}" hidden="{{ !expanded }}">
  <!-- ... -->
</div>
<!-- /each -->

Keyed Arrays

Keys help Synergy identify which items in an Array have changed. Using keys improves performance and avoids unexpected behaviour when re-rendering.

The key can be any primitive value, as long as it is unique to that item within the Array.

By default, if the Array item is an object, then Synergy will look for an id property and assume that to be the key if you haven't said otherwise.

Set the key parameter if you need to override the default behaviour...

<ul>
  <!-- #each person in people (key=whatever) -->
  <li>Hello {{ person.name }}</li>
  <!-- /each -->
</ul>

Note that #each works the same for both Arrays and Sets.

Events

Use standard DOM Event names to bind directly to named methods on your data.

{
  sayHello: function() {
    alert("hi!");
  }
};
<button onclick="sayHello">Say hello</button>

The first argument to your event handler is always a native DOM Event object

{
  handleClick: function(event) {
    event.preventDefault();
    console.log("the link was clicked");
  }
};

If the target of the event is within a repeated block, then the second argument to your handler will be the datum for that particular item.

{
  todos: [
    /* ... */
  ],
  todoClicked: function(event, todo) {
    /*... */
  };
}
<ul>
  <!-- #each todo in todos -->
  <li>
    <h3 onclick="todoClicked">{{ todo.title }}</h3>
  </li>
  <!-- /each -->
</ul>

Forms

Named inputs are automatically bound to properties of the same name on your data.

<input name="color" type="color" />
{
  color: '#4287f5';
}

Submitting Form Data

By default, a HTML form will browse to a new page when the user submits the form. Submission happens when the user actives either a) an input[type="submit"], or b) a button[type="submit"].

In some browsers, a button without a [type] will be assumed to be [type="submit"] if it resides within a form element, so you should always set a buttons type attribute when it lives within a form.

If you wish to override the browsers default behaviour, perhaps to execute some JavaScript before submitting the form data, then you would do that by binding to the forms submit event, and calling preventDefault on the event object inside your handler function to stop the browser from submitting the form.

<form onsubmit="handleForm">
  <input name="formData.name" />
  <input name="formData.email" type="email" />
  <input type="submit" value="Submit" />
</form>
{
  formData: {},
  handleForm: function(event) {
    console.log(this.formData);
    event.preventDefault();
  }
};

Select

Simply name the <select>...

<label for="pet-select">Choose a pet:</label>
<select name="pets" id="pet-select">
  <option value="">--Please choose an option--</option>
  <option value="dog">Dog</option>
  <option value="cat">Cat</option>
  <option value="hamster">Hamster</option>
  <option value="parrot">Parrot</option>
  <option value="spider">Spider</option>
  <option value="goldfish">Goldfish</option>
</select>

...and the value of the property will reflect the value of the currently selected <option>:

{
  pets: 'hamster';
}

The standard HTML <select> element also supports the ability to select multiple options, using the multiple attribute:

<select name="pets" id="pet-select" multiple></select>

A <select> with [multiple] binds to an Array on your data:

{
  pets: ['hamster', 'spider'];
}

Radio Buttons

Add a name to each radio button to indicate which group it belongs to.

<input type="radio" name="filter" value="all" id="filter.all" />
<input type="radio" name="filter" value="active" id="filter.active" />
<input type="radio" name="filter" value="complete" id="filter.complete" />

As with <select>, the value of the named property will reflect the value of the selected <input type="radio">.

{
  filter: 'active';
}

Side Effects

propertyChangedCallback

Synergy updates the DOM once per animation frame if there are any changes in the viewmodel to reflect.

If you implement a propertyChangedCallback method on your viewmodel, then this method will be invoked once for each property that has changed since the last update.

{
  todos: [],
  propertyChangedCallback(path) {
    if (path.match(/^todos.?/)) {
      localStorage.setItem('todos', JSON.stringify(this.todos));
    }
  }
}

Invocations of propertyChangedCallback are already debounced with requestAnimationFrame, so you'll only get one invocation per property per animation frame.

Pre-rendering

Pre-rendering is useful when you need to get content rendered immediately as part of the initial page load, without having to wait for JavaScript to build the page first.

Synergy supports pre-rendering and hydration and doesn't care where or how you pre-render your content. In order to pre-render your page, you only need to load it in a browser (or within a synthetic DOM environment) and that's it! Load the same page again in the browser and Synergy will hydrate the bindings without modifying the DOM.