mgschoen / prosecco-js

Super light and fizzy data binding
0 stars 0 forks source link

Prosecco JS

Prosecco JS is a library that provides super light (and super fizzy 🥂) data binding – and just that. It is intentionally kept as minimal as possible. All you can do is:

That said, it's the perfect choice if you want that and only that. So let's see if this is for you!

Content

Install Prosecco

As webpack module

Install the latest version via npm

$ npm install prosecco-js

and import it in your JavaScript:

import Prosecco from 'prosecco-js';

Via CDN

For development purposes, include the following script in the <head> of your HTML:

<script src="https://cdn.jsdelivr.net/npm/prosecco-js@v0.2.1/dist/prosecco.js">

For production releases, use the minified version:

<script src="https://cdn.jsdelivr.net/npm/prosecco-js@v0.2.1/dist/prosecco.min.js">

Quickstart

Somewhere in your HTML, define a root for your Prosecco app.

<!-- index.html -->

<!DOCTYPE html>
<html>
<head>...</head>
<body>
    <h1>My sparkling site</h1>
    <div id="#app-root">
        <!-- Prosecco's scope -->
        ...
    </div>
</body>
</html>

Then, initalise your app in your JavaScript.

// index.js

var appRoot = document.getElementById('app-root');
var app = new Prosecco(appRoot, {
    text: 'May I bring you some beer?'
});

That's it! Your app is ready.

The object with the text property is your app's data model. You can bind DOM elements to the properties in there.

Let's bind some elements to our text property:

<!-- index.html -->
...
<div id="#app-root">
    <input ps-bind="text:value:input">
    <p ps-bind="text"></p>
</div>
...

I'll cover the details of that ps-bind attribute later. For the moment, let's just see what happens here:

Documentation

Prosecco's constructor

When you import Prosecco in your JavaScript, what you get is Prosecco's constructor function.

import Prosecco from 'prosecco-js';

// ...

var app = new Prosecco(rootElement, model)

To instantiate your Prosecco app, you pass a DOM element and an object that will act as your app's data model.

The rootElement can be any Element in the DOM. It defines the scope of your app: Your app will only manipulate children of that root element and be deaf to anything that happens outside its scope.

The model can be any Object you like. The object's properties represent your app's data model. Note that it does not make much sense to nest other objects inside your model. The only data types you can bind to are Primitives such as Strings, Numbers and Booleans and Arrays. But as long as you don't attempt to bind to one, you can have as many objects as you like in your model.

Multiple apps on one page

You can instantiate multiple Prosecco apps in the same document. For each app, call the constructor with a different rootElement.

There is just one restriction: You may not nest apps in the DOM. If you try to pass an element to the constructor that is part of another app's scope, Prosecco will fail.

Here is a working example:

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>...</head>
<body>
    <div id="app-root-one">
        ...
    </div>
    <div id="app-root-two">
        ...
    </div>
    <script src="https://github.com/mgschoen/prosecco-js/raw/master/index.js">
</body>
</html>
// index.js
import Prosecco from 'prosecco-js';

var rootOne = document.getElementById('app-root-one');
var rootTwo = document.getElementById('app-root-two');

var appOne = new Prosecco(rootOne, {
    // model of app one
});
var appTwo = new Prosecco(rootTwo, {
    // model of app two
});

Binding to primitive values: ps-bind

To bind an element to a variable means to create a direct connection between them: Whenever the value of the variable changes, this change will be reflected in the element.

In Prosecco JS you create this connection with the ps-bind attribute.

<p ps-bind="greeting"></p>

The paragraph above is bound to the value of the variable greeting.

In Prosecco, you can only bind to variables that you have defined in your model. So let's assume you have instantiated your app with the following model.

var app = new Prosecco(rootElement, {
    greeting: 'Cheers, world!'
}); 

The paragraph that is bound to greeting will then render as

If you want to bind to an attribute instead of the content of an element, specify the desired attribute behind a colon in your ps-bind.

<input type="text" ps-bind="greeting:value">

In this input field, the value of the variable greeting will be bound to the input's value attribute. So, with our model from above, this would render as:

<input type="text" value="Cheers, world!">

If you don't specify an attribute, Prosecco will default to binding to the element's textContent.

If you want to bind multiple variables to an element, just add a comma and another binding declaration to your ps-bind:

<button ps-bind="buttonLabel,loading:disabled"></button>

This button's label will be set to the variable buttonLabel's value. It will be disabled whenever the loading variable is truthy.

Modifying bound variables

You can modify all variables in your model directly in your JavaScript.

app.model.greeting = 'À votre santé, world!';

Executing the line above will update both your model and all elements that are bound to it.

It also works the other way round: If you want the changes you make in the DOM to be reflected in your model, specify an event name behind a second colon in your ps-bind.

<input type="text" ps-bind="greeting:value:input">

Each time the bound element emits the specified event, Prosecco will update its corresponding value in the model.

In the input field above, whenever the input event occurs - that is, changing the value by typing, cutting, pasting, etc. - the new value ist stored in the model. Try it by typing and looking up the value for app.model.greeting - it will always be the same value as you see in the input field.

Note that you can bind to any event. Should you fancy updating the model only when the user hovers the input field with her mouse, feel free to specify ps-bind="greeting:value:mouseover".

Conditional rules: ps-if

You can hide and show elements depending on the value of a variable.

<p ps-if="error">
    <strong>Oh no!</strong> There was an error.
</p>

Whenever error is truthy, the paragraph above and all its children are displayed. When error ist falsy, they are all hidden.

Iterating over arrays: ps-each

At times you might need to manage a list of similar elements, for example in a shopping list. That's what ps-each is for.

<h3>Drinks to buy</h3>
<ul>
    <li ps-each="drinks"></li>
</ul>

Suppose your model looks like this:

{
    drinks: [ 'Prosecco', 'Bellini', 'Spritz' ]
}

Then the template above would render as

<h3>Drinks to buy</h3>
<ul>
    <li>Prosecco</li>
    <li>Bellini</li>
    <li>Spritz</li>
</ul>

Whenever you manipulate the array in your code, Prosecco updates the ps-each loop in the template - even if you completely reassign it with a new array.

You can not only repeat individual elements, but also subtrees of the document:

<div class="card" ps-each="imageUrls">
    <div class="image-wrapper">
        <img ps-each-value-target="src" alt="">
    </div>
</div>

The ps-each-value-target attribute defines

So the example above would render as

<div class="card">
    <div class="image-wrapper">
        <img src="https://github.com/mgschoen/prosecco-js/raw/master/path/to/image1.jpg" alt="">
    </div>
</div>
<div class="card">
    <div class="image-wrapper">
        <img src="https://github.com/mgschoen/prosecco-js/raw/master/path/to/image2.jpg" alt="">
    </div>
</div>
<!-- ... -->

Note that inside your array you can have all types of values that can be converted to a string. While it wouldn't make much sense to have objects inside your array ({ a: 1, b: 2 } converts to "[object Object]"), nesting other arrays in your array can work perfectly:

{
    drinks: [ ['Prosecco', 'Frizzante'], ['Aperol', 'Spritz'] ]
}

would render as

<h3>Drinks to buy</h3>
<ul>
    <li>Prosecco,Frizzante</li>
    <li>Aperol,Spritz</li>
</ul>

Note, too, that if you're feeling lucky you are free to override your value's toString() method. So if you really need to have objects in your array, you could do something like this:

// .js
var stringifyableObject = { 
    a: 'my string representation', 
    b: 'some business logic'
};
stringifyableObject.toString = function () {
    return this.a
};

var app = new Prosecco(document.getElementById('app-root'), {
    list: [ stringifyableObject ]
});
<!-- .html -->
<ul>
    <li ps-bind="list"></li>
</ul>

Looping over the list variable would then render as:

<ul>
    <li>my string representation</li>
</ul>

Watching value changes: Prosecco.watch()

You can execute your own code whenever a variable value changes, for example to make API calls whenever the user types in an input field. That's what watcher functions are for.

To add a watcher to a variable, simply call the watch() function on your Prosecco instance and pass it the variable name and the function you want to execute each time the value changes.

var app = new Prosecco(appRoot, {
    query: ''
});

app.watch('query', function (oldValue, newValue) {
    // do something
});

The function you pass to .watch() is called watcher. It always has the same signature, with the variable's previous value as its first argument and the new value as its second argument.