0.1.0 • Published 6 years ago

@joecritch/behave.js v0.1.0

Weekly downloads
-
License
MIT
Repository
github
Last release
6 years ago

behave.js

Gradually layer a website's UI with JavaScript behavior, and keep it easy to understand and predict.

Useful for websites that need a bit of JS behavior. Less useful for applications where the UI changes intricately over time, where more functional components may be required (see React for that).

Contents

Installation

  • npm install @joecritch/behave.js --save

How-To

Knowledge required for this how-to:

  • HTML & JavaScript
  • ES modules
  • ES6 arrow functions

Initialize a behavior

Firstly, we need to connect the HTML to JavaScript, in order to initialize the correct behavior.

<!-- index.html -->

<!-- + the usual DOCTYPE, etc. -->
<button data-behavior="Toggle">
  Toggle me
</button>
// src/Toggle.js

import { createBehavior } from 'behave.js';

const Toggle = createBehavior('Toggle', {
  init() {
    console.log('Hello?');
  },
});

export default Toggle;

(If you prefer, you can write behaviors as ES6 classes.)

// src/index.js

import { manageBehaviors } from 'behave.js';
import Toggle from './Toggle'; // (The behavior you created before)

document.addEventListener('DOMContentLoaded', () => {
  manageBehaviors({
    Toggle,
  });
});

Render

Each behavior supports a render property. This object can "describe", and therefore affect, the values of DOM attributes (in this case, the style attribute).

const Toggle = createBehavior('Toggle', {
  render: {
    attributes: {
      style: {
        color: () => 'red',
      },
    },
  };
});

^ Here, the button's text turns red immediately.

Each of the object's functions (e.g. color) is called whenever the behavior is updating. The values they return will be used with the respective DOM API operation. In this case: node.style.color = "red";.

Of course, the example above doesn't do anything that vanilla JS couldn't. (Or bog-standard HTML for that matter!) So let's dig into some features that might convince you...

Change state

Behaviors can store their own state, which can be changed by various means.

const Toggle = createBehavior('Toggle', {
  getInitialState() {
    return {
      isOn: false,
    };
  },

  init() {
    setTimeout(() => {
      this.setState({
        isOn: true,
      });
    }, 1000);
  },

  render: {
    attributes: {
      style: {
        color: _ => _.state.isOn ? 'red' : null,
      },
    },
  };
});

^ The isOn state changes to true after 1 second. Therefore, the button's text turns red after 1 second.

Now, as promised, the color function serves a purpose! Rather than always returning "red", it now depends on the state of the component. If isOn is true, it'll be red; else, the color attribute will be removed (when it returns null).

(Note: the _ argument of the function. This is the behavior's instance that is currently being updated. Of course, this can be called something else of course, if required. Also, if you want to avoid arrow functions and rely on this context instead, you can: function() { console.log(this) }, where this is the behavior instance.)

Listen to events

Behaviors can also listen to native DOM events, using a similar declarative syntax.

const Toggle = createBehavior('Toggle', {
  getInitialState() {
    return {
      isOn: false,
    };
  },

  handleClick() {
    this.setState({
      isOn: !this.state.isOn,
    });
  },

  render: {
    attributes: {
      style: {
        color: _ => _.state.isOn ? 'red' : null,
      },
    },
    listeners: {
      click: _ => _.handleClick,
    },
  },
});

^ Here, when the button is clicked, its text turns red.

This is a convenient way to manage event listeners. Bonus: if the click function above returned null, it would remove any previous listener. (So, whether a listener is active could also be based on _.state.)

Define children

You can describe child nodes for a behavior, using a data-BehaviorName-childname syntax:

.hide { display: none; }
<button data-behavior="Toggle">
  Turn me
  <span data-Toggle-ontext>on</span>
  <span data-Toggle-offtext class="hide">off</span>
</button>
// ... rest of Toggle.js
  // render: {
    children: {
      'ontext': {
        attributes: {
          style: {
            display: _ => _.state.isOn ? 'none' : null,
          },
        },
      },
      'offtext': {
        attributes: {
          style: {
            display: _ => _.state.isOn ? null : 'none',
          },
        },
      },
    },
  // },
// ... rest of Toggle.js

This also works if you have multiple children of the same name.

Connect a child to another behavior

A child can also reference another behavior via the markup:

<div data-behavior="Panel">
  <button data-Panel-btn="Toggle">
    <span data-Toggle-ontext>on</span>
    <span data-Toggle-offtext class="hide">off</span>
  </button>
</div>

We no longer reference Toggle from data-behavior, but instead define it as a child of the new Panel behavior.

Here is the new Panel behavior. (No changes would be required to Toggle at this point.)

const Panel = createBehavior('Panel', {
  render: {
    children: {
      btn: {},
    },
  },
});

Send data to a child

Behaviors can send data to their child behaviors using props.

Imagine the situation where Panel also needed to change its appearance when you click the toggle button. To do this, we'll move the isOn state to the Panel behavior instead. Then, we'll send isOn as a prop down to Toggle.

Furthermore, props support most common data types, including functions. By passing down functions, this allows the child to call it, like a callback. We'll use this pattern below, for the click event.

const Panel = createBehavior('Panel', {
  getInitialState() {
    return {
      isOn: false,
    };
  },

  handleBtnClick() {
    this.setState({
      isOn: !this.state.isOn,
    });
  },

  render: {
    children: {
      btn: {
        isOn: _ => _.state.isOn,
        onClick: _ => _.handleBtnClick,
      },
    },
  },
});
.panel { background-color: #ccc; padding: 10px; }
.panel.is-on { background-color: yellow; }
const Toggle = createBehavior('Toggle', {
  handleClick() {
    this.props.onClick();
  },

  render: {
    attributes: {
      style: {
        color: _ => _.props.isOn ? 'red' : null,
      },
    },
    listeners: {
      click: _ => _.handleClick,
    },
    children: {
      'ontext': {
        attributes: {
          style: {
            display: _ => _.state.isOn ? 'none' : null,
          },
        },
      },
      'offtext': {
        attributes: {
          style: {
            display: _ => _.state.isOn ? null : 'none',
          },
        },
      },
    },
  },
});

ES6 Classes

There is also an ES6 class alternative for defining behaviors.

You access it like so:

import { Behavior } from 'behave.js';

export default class Toggle extends Behavior {
  constructor(...args) {
    super(...args);
    // Unlike when using `createBehavior()`, custom methods aren't auto-bound
    this.handleClick = this.handleClick.bind(this);

    this.state = {
      // (Or use getInitialState)
    };

    this.render = {
      // ... You render object
    };
  }

  // Your custom methods
  handleClick() {

  }
}

export default Toggle;

Class properties

Optionally, if you have babel-preset-stage-2 or similar installed, you can use class properties too, for a terser syntax:

import { Behavior } from 'behave.js';

export default class Toggle extends Behavior {
  state = {
    // (Or use getInitialState)
  };

  // (`this` is properly bound, as its an arrow function)
  handleClick = () => {

  };

  render = {
    // ... You render object
  };
}

export default Toggle;

Another demo

There is another demo which is slightly more "real world". It's located in the demo folder of this repo. Here's how to access it:

  1. Clone this repo
  2. cd into the project root
  3. $ yarn && yarn build to compile behave.js
  4. $ cd demo && yarn && yarn demo
  5. & open http://localhost:8081

FAQ

How FAST is it?

TBC!

Why "behaviors"?

This project doesn't use the term "component", because it doesn't take full responsibility for its DOM structure.

More suitably, the term "behavior" comes from AREA 17's concept of referencing a function from a DOM attribute, which in itself originates from elementaljs. You should check both those out too.

How does it compare to React?

Unlike React or other functional UI libraries, the render property not a function. Instead, its an object, because its structure is not designed to change.

This also greatly reduces complexity in the internal "diff". However, the values within the object are functions, because they are designed to change, based on its input.

Why are these FAQs so bad?

Please contribute via issues or PRs! I'd like to make this as useful as possible.

TODO

  • Write tests
  • Add Flow annotations
  • Test overall performance