@joecritch/behave.js v0.1.0
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
- Here's a sandbox I made earlier.
- Follow along below to understand what's what.
- or, if you're familiar with React, perhaps skip to Send data to a child to compare.
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:
- Clone this repo
cd
into the project root$ yarn && yarn build
to compilebehave.js
$ cd demo && yarn && yarn demo
& 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
6 years ago