gotob
v2.3.1
Published
One .min.js to rule 'em all.
Downloads
58
Maintainers
Readme
gotoв
"The only software that I like is one that I can easily understand and solves my problems. The amount of complexity I'm willing to tolerate is proportional to the size of the problem being solved." --Ryan Dahl
gotoв is a framework for making the frontend of a web application (henceforth webapp).
Current status of the project
The current version of gotoв, v2.3.1, is considered to be stable and complete. Suggestions and patches are welcome. Besides bug fixes, and the completion of the tutorial in one of the appendixes, there are no changes planned.
gotoв is part of the ustack, a set of libraries to build webapps which aims to be fully understandable by those who use it.
Why gotoв?
gotoв is a framework optimized for understanding. Its purpose is to allow you to write webapps in a way that you can fully understand what's going on.
In my experience, understanding leads to short and beautiful code that can last for years in a production setting. It is my sincere hope that you'll be able to use gotoв to create reliable webapps and have a lot of fun while at it.
Because gotoв is optimized for understanding, if anything on this readme strikes you as unclear or confusing, please let me know. I'll be glad to improve the explanation and make it better for you and everyone else.
Installation
gotoв is written in Javascript. You can use it in the browser by loading the pre-built file, gotoB.min.js
, in a <script>
tag at the top of the <body>
:
<script src="gotoB.min.js"></script>
Or you can use this link to use the latest version - courtesy of jsDelivr.
<script src="https://cdn.jsdelivr.net/gh/fpereiro/gotob@d599867a327a74d3c53aa518f507820161bb4ac8/gotoB.min.js"></script>
gotoв uses non-ASCII symbols, so you also must specify an encoding for your document (for example UTF-8) by placing a <meta>
tag in the <head>
of the document: <meta charset="utf-8">
.
gotoв is exclusively a client-side library. Still, you can find it in npm: npm install gotob
Browser compatibility has been tested in the following browsers:
- Google Chrome 15 and above.
- Mozilla Firefox 3 and above.
- Safari 4 and above.
- Internet Explorer 6 and above.
- Microsoft Edge 14 and above.
- Opera 10.6 and above.
- Yandex 14.12 and above.
The author wishes to thank Browserstack for providing tools to test cross-browser compatibility.
Index
- Examples
- Introduction
- Frequently Asked Questions
- API reference
- Internals
- Annotated source code
- License
- Appendix: A brief history of the frontend
- Appendix: Lessons from the quest for IE6 compatibility
- Appendix: Tutorial: So you want to write a frontend?
Examples
Hello world
var helloWorld = function () {
return ['h1', 'Hello, world!'];
}
B.mount ('body', helloWorld);
Counter
var counter = function () {
return B.view ('counter', function (counter) {
counter = counter || 0;
return ['div', [
['h2', 'Counter'],
['h3', ['Counter is: ', counter]],
['button', {
onclick: B.ev ('set', 'counter', counter + 1)
}, 'Increment counter']
]];
});
}
B.mount ('body', counter);
Todo list
B.respond ('create', 'todo', function (x) {
var todo = prompt ('What\'s one to do?');
if (todo) B.call (x, 'add', 'todos', todo);
});
var todoList = function () {
return [
['style', [
['span.action', {color: 'blue', cursor: 'pointer', 'margin-left': 10}],
]],
['h2', 'Todos'],
B.view ('todos', function (todos) {
return ['ul', dale.go (todos, function (todo, index) {
return ['li', ['', todo, ['span', {'class': 'action', onclick: B.ev ('rem', 'todos', index)}, 'Remove']]];
})];
}),
['button', {onclick: B.ev ('create', 'todo')}, 'Create todo']
];
}
B.mount ('body', todoList);
Input
var input = function () {
return B.view ('input', function (input) {
return ['div', [
['input', {value: input, oninput: B.ev ('set', 'input'), onchange: B.ev ('set', 'input')}],
['p', ['Value of input is ', ['strong', input]]]
]];
});
}
B.mount ('body', input);
Textarea
var textarea = function () {
return B.view ('textarea', function (textarea) {
return ['div', [
['textarea', {value: textarea, oninput: B.ev ('set', 'textarea')}],
['p', ['Value of textarea is ', ['strong', textarea]]]
]];
});
}
B.mount ('body', textarea);
Select
var select = function () {
var options = ['Select one', 'Elephant Island', 'South Georgia'];
return B.view ('select', function (select) {
return ['div', [
['select', {onchange: B.set ('set', 'select')}, dale.go (options, function (option) {
return ['option', {value: option !== 'Select one' ? option : ''}, option];
})]
]];
});
}
B.mount ('body', select);
Radio
var radio = function () {
var options = ['Clics', 'Peperina', 'Bicicleta'];
return B.view ('radio', function (radio) {
return ['div', [
dale.go (options, function (option) {
return [
['input', {type: 'radio', name: 'radio', checked: radio === option, onchange: B.ev ('set', 'radio'), value: option}],
['label', ' ' + option],
['br'],
];
}),
['p', ['Value of radio is ', ['strong', radio]]]
]];
});
}
B.mount ('body', radio);
Checkboxes
B.respond ('toggle', 'checkboxes', function (x, option) {
var index = (B.get ('checkboxes') || []).indexOf (option);
if (index === -1) B.call (x, 'add', 'checkboxes', option);
else B.call (x, 'rem', 'checkboxes', index);
});
var checkboxes = function () {
var options = ['O\'ahu', 'Maui', 'Kauai'];
return B.view ('checkboxes', function (checkboxes) {
checkboxes = checkboxes || [];
return ['div', [
dale.go (options, function (option) {
return [
['input', {type: 'checkbox', checked: teishi.inc (checkboxes, option), onclick: B.ev ('toggle', 'checkboxes', option)}],
['label', ' ' + option],
['br'],
];
}),
['p', ['Selected islands: ', ['strong', checkboxes.sort ().join (', ')]]]
]];
});
}
B.mount ('body', checkboxes);
Table
B.call ('set', 'table', [
{id: 1, name: 'Top of line', price: 100},
{id: 2, name: 'Value for money', price: 65},
{id: 3, name: 'Last resort', price: 24}
]);
var table = function () {
return B.view ('table', function (table) {
table = table || [];
return ['table', [
['tr', dale.go (table [0], function (v, k) {
return ['th', k];
})],
dale.go (table, function (v) {
return ['tr', dale.go (v, function (v2) {
return ['td', v2];
})];
})
]];
});
}
B.mount ('body', table);
You can find more examples here.
Introduction
gotoв is a framework for writing the frontend of a webapp. In the case of a webapp, the frontend consists of an user interface implemented with HTML and (almost always) some js that runs on the browser.
gotoв provides a solution to the two main things that a frontend framework must do:
- Generate HTML.
- Manage state.
Let's take the example of a shopping cart. A shopping cart is an HTML page that displays a list of products that an user is interested in purchasing. The user can interact with the page to add and remove articles - and other parts of the page (for example, the total amount) will change accordingly.
To implement the shopping cart, we need to generate HTML to make it appear on the user's screen. Some parts of the shopping cart will change according to the selection of products (list of products, amounts), whlie others will remain the same (like the header or footer). It follows that the HTML must take into account both "fixed" elements and "variable" elements.
Besides generating HTML, we need also to keep track of the products and quantities the user has entered. This is the state. This state is essential, because without it the HTML page would be static and would not respond to user input! This state also has to be sent to the server to be processed when the purchase is finalized.
The HTML and the state are deeply interlocked. They interact in a yin-yang manner:
- The state determines how the HTML will look.
- Certain elements in the HTML (buttons, inputs) will perform changes to the state.
For example:
- When the user loads the shopping cart for the first time, the HTML shows an empty cart (state -> HTML).
- The user clicks on a button and adds a product (HTML -> state).
- Because a product was added, the HTML is changed (state -> HTML).
An interface can be understood as a function of the state, which returns HTML. This HTML, in turns, contains elements that can trigger further changes to the state. The hard part of implementing frontends is to fully close the circle, and make sure that when the state is updated, the HTML also changes. This is also the reason why frontend frameworks exist and are widely used.
All of the above is valid for any type of webapp. Let's explore now how gotoв solves these problems:
- gotoв creates all the HTML in the browser using js: the presentation logic is fully separated from the server and the full power of js is available to generate HTML.
- gotoв centralizes all the state into a js object: instead of having data spreaded out in different places (DOM elements, js variables), it centralizes all the state in a single location that can be easily queried and updated.
- gotoв uses events to update the state and to update the HTML: by using events, the app can be updated efficiently without having to manually track dependencies between parts of the app.
Let's see each of these in turn:
Generating HTML using js
gotoв uses js object literals to generate HTML. Object literals are mere arrays ([...]
) and objects ({...}
) that conform to certain shapes. We call these literals liths
. Let's see a few examples of some liths and their corresponding HTML:
['p', 'Hello'] -> <p>Hello</p>
['div', {class: 'nice'}, 'Cool'] -> <div class="nice">Cool</div>
['div', ['p', {id: 'nested'}, 'Turtles']] -> <div><p id="nested">Turtles</p></div>
In general, a lith is an array with one to three elements. The first element is a string indicating the tag
. There can be a second element for specifying attributes, which is an object. Finally, you can add contents
to the lith; these contents can be a string, a number or another lith.
Besides liths, we also can write an array containing multiple liths, which is affectionally called lithbag. For example:
[['p'], ['p']] -> <p></p><p></p>
A lithbag can also be a collection of text and number fragments. For example:
['i am', 'a', 1337, 'lithbag'] -> i ama1337lithbag
You can put a lithbag as the contents to another lith:
['div', ['Some', ' ', 'text']] -> <div>Some text</div>
Rather than writing standalone liths or lithbags, gotoв expects you to write functions that return liths or lithbags. For example, this function returns HTML for a hello world
page:
var helloWorld = function () {
return ['h1', 'Hello, world!'];
}
If you come from other frontend frameworks, these functions are called views. To emphasize the fact that in gotoв views are always functions, we'll call them vfuns
(short for view functions).
It is possible and even handy (but not required) to generate CSS with gotoв (see the details here).
Using js object literals to generate HTML has two advantages:
- Because object literals are part of js, the views can live together with the rest of the code.
- Because object literals are just data, they're very easy to manipulate. You can write conditionals and loops in js that will output different object literals.
Takeaway: use vfuns to generate HTML.
A single store for all the state
gotoв stores all the state of the application (rather, all the state that belongs to the frontend) into a single object. This is a plain js object. What makes it powerful is the fact that it is the single source of truth of the application. We call this object the store
.
{
// here is all the state!
}
The store is located at B.store
and gotoв automatically creates it when the app is loaded. B
, by the way, is the global variable where gotoв is available.
The following are examples of what can (and should!) be contained on the store:
- Data brought from the server.
- Name of the page that is currently being displayed.
- Data provided by the user that hasn't been submitted yet to the server.
Takeaway: if it affects what's displayed on the screen or what is submitted to the server, it belongs in the store.
Using events
gotoв structures all operations through events. All actions to be performed on the webapp can be modeled as events. This includes updating B.store
, which is updated by gotoв's event system instead of being modified directly.
The function for triggering an event is B.call
. We prefer the term call instead of other terms normally used with events (such as trigger or fire) because we see events as a form of communication. An event is a call to one or more parts of your code that might in turn respond to that call.
B.call
receives as arguments a verb
, a path
and optional extra arguments
.
Events are useless until another part of the program responds to them. Traditionally, these are called event listeners but we call these responders, since they respond to an event being called. To create responders
, we will use the function B.respond
, which we'll cover in a later section. For now, all you need to know is that responders
are defined with a verb
and a path
(exactly like events) and are matched (triggered) by events with matching verbs
and paths
.
gotoв provides three built-in responders
for modifying B.store
: set
, add
and rem
. These responders are already created and allow you to modify the store. Let's see them through examples:
// At the beginning, B.store is merely an empty object
// We now call an event with verb `set`, path `username` and `mono` as its first argument.
B.call ('set', 'username', 'mono');
// Now, B.store is {username: 'mono'}
// We now call an event with verb `set`, path `['State', 'page']` and `main` as its first argument.
B.call ('set', ['State', 'page'], 'main');
// Now, B.store is {username: 'mono', State: {page: 'main'}}
// We now call an event with verb `rem`, path `[]` and `username` as its first argument.
B.call ('rem', [], 'username');
// Now, B.store is {State: {page: 'main'}}
// We now call an event with verb `rem`, path `State` and `page` as its first argument.
B.call ('rem', 'State', 'page');
// Now, B.store is {State: {}}
// We now call an event with verb `set`, path `['Data', 'items']` and `['foo', 'bar']` as its first argument.
B.call ('set', ['Data', 'items'], ['foo', 'bar']);
// Now, B.store is {State: {}, Data: {items: ['foo', 'bar']}}
// We now call an event with verb `add`, path `['Data', 'items']` and `boo` as its first argument.
B.call ('add', ['Data', 'items'], 'boo');
// Now, B.store is {State: {}, Data: {items: ['foo', 'bar', 'boo']}}
// We now call an event with verb `rem`, path `['Data', 'items']` and `0` as its first argument.
B.call ('rem', ['Data', 'items'], 0);
// Now, B.store is {State: {}, Data: {items: ['bar', 'boo']}}
It is important to note that events can be used for things other than updating B.store
, as we will see later.
Takeaway: modify B.store
through events, using B.call
.
Updating the page when the store changes
gotoв provides B.view
, a function for creating views that automatically update themselves when the store changes. To make the app more understandable (and efficient), views can depend on a specific part of the store, instead of depending on the whole state. This means that if a view depends on a part X of the store, then if Y is modified (and Y is not contained inside X, nor X inside Y), the view will remain unchanged.
Let's see an example:
var counter = function () {
return B.view ('counter', function (counter) {
counter = counter || 0;
return ['h2', 'The counter is ' + counter];
});
}
B.mount ('body', counter);
Whenever B.store.counter
is updated, the h2
element will be automatically updated.
B.call ('set', 'counter', 1);
// <h2>The counter is 1</h2>
B.call ('set', 'counter', 2);
// <h2>The counter is 2</h2>
Updating the state from the page
You might be wondering: how can we trigger events from the DOM itself? The example above doesn't show how to place a button that could increase the counter. One way of doing it would be the following:
var counter = function () {
return B.view ('counter', function (counter) {
counter = counter || 0;
return ['div', [
['h2', 'The counter is ' + counter],
['button', {
onclick: "B.call ('set', 'counter', " + (counter + 1) + ")"
}, 'Increment counter']
]];
});
}
B.mount ('body', counter);
But it is much better to use B.ev
, which will create a stringified call to B.call
that we can put within the onclick
attribute directly.
var counter = function () {
return B.view ('counter', function (counter) {
counter = counter || 0;
return ['div', [
['h2', 'The counter is ' + counter],
['button', {
onclick: B.ev ('set', 'counter', counter + 1)
}, 'Increment counter']
]];
});
}
B.mount ('body', counter);
Summary
And that, in a nutshell, is how gotoв works:
- Views are functions that return object literals (liths) to generate HTML.
- The global store centralizes all of the state.
- Events perform all actions, including updating the global store.
- Views depend on parts of the store and are automatically updated whenever the relevant part of the store changes.
- Views can contain DOM elements that can call events.
An app written with gotoв will mostly consist of views and responders, and most of its logic will live in vfuns
(view functions) and rfuns
(responder functions).
FAQ
Why did you write another javascript framework?!?
I experience two difficulties with existing javascript frontend frameworks:
- They are hard to understand, at least for me.
- They are constantly changing.
The combination of these two characteristics mean that I must constantly spend an enormous amount of time and effort to remain an effective frontend developer. Which makes me unhappy, because complex things frustrate me and I am quite lazy when it comes to things I don't enjoy.
Rather than submit to this grind or reject it altogether (and missing out the possibility of creating my webapps), I took a third way out, by deciding to write a frontend framework that:
- Is optimized for understanding.
- Built on fundamentals, so that the framework will change less and less as time goes by.
And, of course, gotoв must be very useful for building a real webapp.
Is gotoв for me?
gotoв is for you if:
- You have freedom to decide the technology you use.
- Complexity is a massive turn-off for you.
- You like old (ES5) javascript.
- You miss not having to compile your javascript.
- You enjoy understanding the internals of a tool, so that you can then use it with precision and confidence.
- You like technology that's a bit strange.
- You want to build a community together with me.
gotoв is not for you if:
- You need to support browsers without javascript.
- You need a widely supported framework, with a large community of devs and tools.
- You are looking for a framework that is similar to Angular, Ember or React.
- You need a very fast framework; gotoв chooses simplicity over performance in a couple of critical and permanent respects.
What does gotoв care about?
- Ease of use: 90% of the functionality you need is contained in four functions (one for calling an event (
B.call
), one for setting up event responders (B.respond
), one for stringifying an event call into a DOM attribute (B.ev
) and one for creating dynamic DOM elements which are updated when the store changes (B.view
)). There's also three more events for performing data changes that you'll use often. But that's pretty much it. - Fast reload: the edit-reload cycle should take under two seconds. No need to wait until no bundle is completed.
- Smallness: gotoв and its dependencies are < 2048 lines of consistent, annotated javascript. In other words, it is less than 2048 lines on top of vanilla.js.
- Batteries included: the core functionality for building a webapp is all provided. Whatever libraries you add on top will probably be for specific things (nice CSS, a calendar widget, etc.)
- Trivial to set up: add
<script src="https://cdn.jsdelivr.net/gh/fpereiro/gotob@27c65b4484500ec70f175dfff998cdd7d1b0208e/gotoB.min.js"></script>
at the top of the<body>
. - Everything in plain sight: all properties and state are directly accessible from the javascript console of the browser. DOM elements have stringified event handlers that can be inspected with any modern browser.
- Performance: gotoв itself is small (~15kB when minified and gzipped, including all dependencies) so it is loaded and parsed quickly. Its view redrawing mechanism is reasonably fast.
- Cross-browser compatibility: gotoв is intended to work on virtually all the browsers you may encounter. See browser current compatibility above in the Installation section.
What does gotoв not care about?
- Browsers without javascript: gotoв is 100% reliant on client-side javascript - if you want to create webapps that don't require javascript, gotoв cannot possibly help you create them.
- Post-2009 javascript: everything's written in a subset of ES5 javascript. This means no transpilation, no different syntaxes, and no type declarations. You can of course write your application in ES6 or above and gotoв will still work.
- Module loading: gotoв and its dependencies happily and unavoidably bind to the global object. No CommonJS or AMD.
- Build/toolchain integration: there's no integration with any standard tool for compiling HTML, CSS and js. gotoв itself is pre-built with a 50-line javascript file.
- Hot-reloading: better get that refresh finger ready!
- Plugin system: gotoв tries to give provide you all the essentials out of the box, without installation or configuration.
- Object-oriented programming: gotoв uses objects mostly as namespaces. There's no inheritance and no use of
bind
. Classes are nowhere to be found. - Pure functional programming: in gotoв, side-effects are expressed as events. The return values from event handlers are ignored, and every function has access to the global store. There's no immutability; the global state is modified through functions that update it in place.
API reference
Before reading this section, it is highly recommended that you read the introduction to have a conceptual overview of gotoв.
The gotoв object: B
gotoв is automatically loaded on the global variable B
.
B.v
contains a string with the version of gotoв you're currently using. B.t
contains a timestamp indicating the moment when the library is loaded - which can be an useful reference point for performance measurements.
While B
is a global variable, I suggest assigning B
to a local variable to make your code clearer:
var B = window.B;
gotoв automatically loads its five dependencies on the following global variables:
You can use these libraries at your discretion. If you do so, I recommend also assigning local variables to them, for clarity's sake:
var dale = window.dale, teishi = window.teishi, lith = window.lith, c = window.c;
You may have noticed I omitted recalc in the line of code above. This is because you'll most likely use this recalc through gotoв's functions instead of using it directly.
B.mount
B.mount
is the function that places your outermost view(s) on the page. This function takes two arguments: the target
(the DOM element where the HTML will be placed) and a vfun
(the function that generates the liths that will be converted to HTML). For example:
var helloWorld = function () {
return ['h1', 'Hello, world!'];
}
B.mount ('body', helloWorld);
target
must always be a string. It can be either 'body'
or a string of the form '#ID'
, where ID
is the id of a DOM element that is already in the document. If target
is not present in the document, the function will report an error and will return false
.
B.mount
will execute the vfun
passing no parameters to it. This function must return either a lith or a lithbag. If the function doesn't return a valid lith or lithbag, B.mount
will report an error and return false
.
The HTML generated will be placed at the bottom of the target. In the example above, the <body>
will look like this:
<body>
<h1>Hello, world!</h1>
</body>
Optionally, the target
string can have the form TAG#ID
. For example, if you have an element <div id="container"></div>
already inside the <body>
, you can use either '#container'
or 'div#container'
as the target
.
// Create first a `div` with `id` `container`
document.body.innerHTML += '<div id="container"></div>';
B.mount ('#container', function () {
return ['p', 'Hello'];
});
B.unmount
is a function to undo what was done by B.mount
. It receives a target
which is just like the target
passed to B.mount
. It will remove all of the HTML contained inside target
- not just the HTML added there by B.mount
. If an invalid or non-existing target
is passed to B.unmount
, the function will report an error and return false
.
B.unmount ('#container');
// `document.body.innerHTML` will now be an empty string.
Both B.mount
and B.unmount
will return undefined
if the operation is successful.
In case you're wondering what's going under the hood, B.mount
does very little: it just validates its inputs, executes fun
and places the resulting HTML in the DOM. B.unmount
is almost equally simple, except that it is in charge of removing the responders of the deleted views through B.forget
. Which takes us to the following topic - events!
Introduction to the event system: B.call
, B.respond
, B.responders
, B.forget
gotoв is built around events. Its event system considers events as communication between different parts of the app with each other. Some parts of the program perform calls and other parts of the program respond to those calls.
The two nouns with which we can structure this paradigm is: event and responder. The two corresponding verbs are call and match: events are called (almost always by responders), responders are matched by events.
An event call can match zero, one or multiple responders. When a responder is matched, it is executed.
Events are more general than mere function calls. When a function is called, the function must exist and must be defined only once:
function call -> a function is executed
With events, we can have one-to-none, one-to-one or one-to-many execution relationships:
event call -> (nothing happens, no responders were matched)
event call -> exactly one responder matched
event call -> this responder is matched
|--> this responder is also matched
This generality of events is extremely useful to model and write the code of interfaces, which is highly interconnected, asynchronous and triggered by user interactions. When a responder is matched by an event, its associated function (which we call rfun
or responder function) is executed. The ultimate purpose of events and responders is to execute the rfuns
(responder functions) at the right time; rfuns, together with their responders and with matching events, can replace direct function calls in most of the logic of the frontend.
In particular, if a certain event modifies a part of the data, multiple responders can be matched in response to that change, without the event having to bear the burden of knowing which responders to call. This makes all the difference in a frontend.
Events are called with the function B.call
, which takes the following parameters:
- A
verb
, which is a string. For example:'get'
,'set'
or'someverb'
. - A
path
, which can be either a string, an integer, or an array with zero or more strings or integers. For example,'hello'
,1
, or['hello', '1']
. If you pass a single string or integer, it will be interpreted as an array containing that element (for example,'hello'
is considered to be['hello']
and0
is considered to be[0]
). - Optional extra arguments of any type (we will refer to them as
args
later). These arguments will be passed to matching responders.
If invalid parameters are passed to B.call
, the function will report an error and return false
.
An invocation to B.call
will call an event once.
Responders are created with the function B.respond
, which takes the following parameters:
verb
, which can be a string or a regex.path
, which can be a string, an integer, a regex, or an array containing those types of elements.options
, an optional object with additional options.rfun
, the function that will be executed when the responder is matched.rfun
is short forresponder function
. The responder receives a context objectx
as its first argument and optional extra arguments (args
). We'll see what's insidex
in a later section.
If invalid parameters are passed to B.respond
, the function will report an error and return false
.
If the invocation is valid, B.respond
will create a responder and place it in B.responders
. This responder will be matched (executed) by any matching event calls throughout the course of the program. Seen from this perspective, a single call to B.respond
has a more lasting effect than a call to B.call
.
When does an event match a responder? A full answer is contained here and here. The short answer is: when both the verb
and the path
of the event and the responder match.
Let's define the following events and responders:
B.respond ('foo', 0, rfun) // responder A
B.respond ('foo', '*', rfun) // responder B
B.respond ('foo', ['*', '*'], rfun) // responder C
B.respond ('bar', [], rfun) // responder D
B.call ('foo', 0); // event A
B.call ('foo', 1); // event B
B.call ('foo', [0, 1]); // event C
B.call ('bar', 0); // event D
What will happen?
- Event
A
:- Will match responder A, because both their
verb
('foo'
) andpath
(0
) are identical. - Will match responder B, because their
verb
('foo'
) is identical, and because the responder'spath
('*'
) will be matched by any event'spath
with length 1. - Will not match responder C, because while their
verb
('foo'
) is identical, responder Cpath
's will be matched only by events withpath
s of length 2. - Will not match responder D, because their
verb
is different ('foo'
vs'bar'
).
- Will match responder A, because both their
- Event
B
:- Will not match responder A, because while their
verb
is identical, theirpath
is not. - Will match responder B, because their
verb
is identical and because the responder'spath
will be matched by any event'spath
with length 1. - Will not match responder C, because while their
verb
('foo'
) is identical, responder Cpath
's will be matched only by events withpath
s of length 2. - Will not match responder D, because their
verb
is different ('foo'
vs'bar'
).
- Will not match responder A, because while their
- Event
C
:- Will not match responders A or B, because while their
verb
('foo'
) is identical, theirpaths
don't match. - Will match responder C, because their
verb
('foo'
) is identical and because responder Cpath
's will be matched thepath
of event C, which has length 2. - Will not match responder D, because their
verb
is different ('foo'
vs'bar'
).
- Will not match responders A or B, because while their
- Event
D
:- Will not match responders A, B or C, because their
verbs
are different. - Will not match responder D, because while their
verb
('bar'
) is identical, responder D will only be matched by events withpaths
of length 0.
- Will not match responders A, B or C, because their
Notice that in the example above we called B.respond
before B.call
; if we had done this the other way around, the event calls would have had no effect since the responders would have not been registered yet.
Wildcards ('*'
) and regexes can be used in the verbs
and path
elements of responders, but not of events.
Regarding the optional options
object passed to B.respond
, please check recalc's documentation for a full specification; the most useful ones are:
id
(a string or integer), to determine the responder'sid
priority
, an integer value and determines the order of execution if multiple responders are matched by an event call; by default,priority
is 0, but you can specify a number that's larger or smaller than that (the higher the priority, the earlier the responder will be executed). If two responders are matched and have the same priority, the oldest one takes precedencematch
, a function to let the responder decide whether it should be matched by any incoming event; this function supersedes the defaultverb
andpath
matching logic with your own custom logic; the provided function receives two arguments, an object with theverb
andpath
of the event being called, plus the responder itself.
Responders are stored in B.responders
. To remove a responder, invoke B.forget
, passing the id
of the responder. The id
of the responder will be that provided by you when creating it (if you passed it as an option), or the automatically generated id
which will be returned if the invocation to B.respond
was successful.
Data: B.store
and the built-in data responders ('set'
, 'add'
and 'rem'
)
As we saw in the introduction, all the state and data that is relevant to the frontend should be stored inside B.store
, which is a plain object where all the data is contained.
Rather than modifying the store
directly, gotoв requires you to do it through the three built-in data responders, which have the following verbs: 'set'
, 'add'
and 'rem'
. Whenever you call an event with one of these three verbs (set/add/rem
), these responders will be executed and they will do two things:
- Update the store.
- Call a
change
event.
change
events are very important, because these are the ones that update the page! In fact, B.view
, the function for creating reactive elements (which will cover below), creates event responders that are matched when change
events are called.
Let's see now each of these responders:
set
The first data responder is set
. This responder sets data into a particular location inside B.store
. It takes a path
and a value
. path
can be an integer, a string, or an array containing integers and strings (as any responder's path
, really); path
represents where we want to set the value inside B.store
.
Let's see now a set of examples. In each of these examples, I'll consider that we start with an empty B.store
so that we don't carry data from one example to the other.
B.call ('set', 'title', 'Hello!');
// B.store is now {title: 'Hello!'}
As you can see, we pass 'set'
as the first argument; then we pass the path
('title'
) and finally the value ('Hello!'
). set
also allows you to set nested properties:
B.call ('set', ['user', 'username'], 'mono');
// B.store is now {user: {username: 'mono'}}
Notice how B.store.user
was initialized to an empty object. Because the second element of the path is a string (username
), the set
data responder knows that B.store.user
must be initialized to an object. Contrast this to the following example:
B.call ('set', ['users', 0], 'mono');
// B.store is now {users: ['mono']}
In the example above, B.store.users
is initialized to an array instead, since 0
is an integer and integers can only be the keys of arrays, not objects.
If your path
has length 1, you can use a single integer or object as path
:
B.call ('set', 'foo', 'bar');
// B.store is now {foo: 'bar'}
If you pass an empty path
, you will overwrite the entire B.store
. In this case, value
can only be an array or object, otherwise an error will be reported and no change will happen to B.store
.
B.call ('set', [], []);
// B.store is now []
B.call ('set', [], 'hello');
// B.store still is [], the invocation above will report an error and do nothing else.
B.call ('set', [], {});
// B.store is now {}
set
will overwrite whatever part of the existing store stands in its way. Let's see an example:
B.call ('set', ['Data', 'items'], [0, 1, 2]);
// B.store is now {Data: {items: [0, 1, 2]}}
B.call ('set', ['Data', 'key'], 'val');
// B.store is now {Data: {items: [0, 1, 2], key: 'val'}}
B.call ('set', ['Data', 0], 1);
// B.store is now {Data: [1]}
In the example above, when we set ['Data', 'key']
, ['Data', 'items']
is left untouched. However, when we set ['Data', 0]
to 1
, that assertion requires that Data
be an array. Because it is an object, it will be overwritten completely and set to an array. This would also happen if Data
were an array and a subsequent assertion required it being an object.
In summary, set
will preserve the existing keys on the store unless there is a type mismatch, in which case it will overwrite the required keys with the necessary arrays/objects.
add
The second data responder is add
. This responder puts elements at the end of an array. It takes a path
, plus zero or more elements that will be placed in the array. These elements can be of any type.
B.call ('set', ['Data', 'items'], []);
// B.store is now {Data: {items: []}}
B.call ('add', ['Data', 'items'], 0, 1, 2);
// B.store is now {Data: {items: [0, 1, 2]}}
B.call ('add', ['Data', 'items']);
// B.store is still {Data: {items: [0, 1, 2]}}
If path
points to a location with value undefined
, the array will be created automatically:
B.call ('add', ['Data', 'items'], 0, 1, 2);
// B.store is now {Data: {items: [0, 1, 2]}}
If no elements are passed to add
but path
points to an undefined value, the containing array will still be created.
B.call ('add', ['Data', 'items']);
// B.store is now {Data: {items: []}}
If path
points to a location that is neither undefined
nor an array, an error will be reported and no change will happen to B.store
.
rem
The third and final data responder is rem
. This responder removes keys from either an array or an object within the store. Like the other data responders, it receives a path
, plus zero or more keys that will be removed.
B.call ('add', ['Data', 'items'], 'a', 'b', 'c');
// B.store is now {Data: {items: ['a', 'b', 'c']}}
B.call ('rem', ['Data', 'items'], 1);
// B.store is now {Data: {items: ['a', 'c']}}
B.call ('rem', 'Data', 'items');
// B.store is now {Data: {}}
B.call ('rem', [], 'Data');
// B.store is now {}
If path
points to an array, the keys must all be integers. If path
points to an object, the keys must instead be all strings. If path
points to neither an array nor an object, rem
will report an error and do nothing.
B.call ('add', ['Data', 'items'], 'a', 'b', 'c');
// B.store is now {Data: {items: ['a', 'b', 'c']}}
B.call ('rem', ['Data', 'items'], 'a');
// The last invocation will report an error and make no change on B.store
B.call ('rem', 'Data', 0);
// The last invocation will also report an error and make no change on B.store
B.call ('rem', ['Data', 'items', 0], 'foo');
// The last invocation will also report an error and make no change on B.store
An exception to the above rule is that if path
points to undefined
, rem
will not produce any effect but no error will be printed.
B.call ('rem', ['Data', 'foo'], 'bar');
// Nothing will happen.
Nothing will happen also if you pass no keys to remove.
B.call ('rem', ['Data', 'items']);
// Nothing will happen.
You can pass multiple keys to remove in one call.
B.call ('set', ['Data', 'items'], ['a', 'b', 'c']);
B.call ('rem', ['Data', 'items'], 0, 1);
// B.store is now {Data: {items: ['c']}}
Instead of passing the keys as arguments, you can also pass them all together as an array of keys.
// These two invocations are equivalent:
B.call ('rem', ['Data', 'items'], 0, 1);
B.call ('rem', ['Data', 'items'], [0, 1]);
// These two invocations are equivalent:
B.call ('rem', [], 'Data', 'State');
B.call ('rem', [], ['Data', 'State']);
Event calls from the DOM: B.ev
Since gotoв applications are structured around events and responders, user interactions must call events. This means that certain DOM elements need to call gotoв events from from their native event handlers (for example, the onclick
should invoke B.call
). For this purpose, you can use the function B.ev
, which creates stringified event handlers that we can pass to DOM elements, in order to trigger events from them. Let's see an example:
var button = function () {
return ['button', {
onclick: B.ev ('do', 'it')
}, 'Do it!'];
}
B.mount ('body', button);
When the button
above is placed in the DOM, clicking on it will call an event with verb
do
and path
it
- in other words, it's the equivalent of running B.call ('do', 'it')
.
B.ev
takes as arguments a verb
, a path
, and optional further arguments. In fact, it takes the same arguments as B.call
! This is not a coincidence, since B.ev
generates a string that, when executed by a javascript event, will perform a call to B.call
with the same arguments.
Let's now see another example, to illustrate other aspects of B.ev
: we'll create a button that, when clicked, will call an event with verb submit
and path data
.
['button', {onclick: B.ev ('submit', 'data')}]
You can pass extra arguments when calling an event. For example, if you want to pass an object of the shape {update: true}
you can instead write:
['button', {onclick: B.ev ('submit', 'data', {update: true})}]
You can pass all sorts of things as arguments:
['button', {onclick: B.ev ('submit', 'data', null, NaN, Infinity, undefined, /a regular expression/)}]
If you need to access properties that are within the event handler (like event
or this
), you can do so as follows:
['button', {onclick: B.ev ('submit', 'data', {raw: 'this.value'})}]
These are called raw
arguments, because they are passed as they are, without stringifying them.
Any responder matched by this event will this.value
as its first argument, instead of the string 'this.value'
. You could also pass the event instead:
['button', {onclick: B.ev ('submit', 'data', {raw: 'event'})}]
You can pass multiple raw arguments. For example, if you want to pass both this.value
and event
you can write this:
['button', {onclick: B.ev ('submit', 'data', {raw: 'this.value'}, {raw: 'event'})}]
If an object has a key raw
but its value is not a string, it will be considered as a normal argument instead:
['button', {onclick: B.ev ('submit', 'data', {raw: 0})}]
The event responder above will receive {raw: 0}
as its first argument.
If you pass an object with a raw
key that contains a string, other keys within that object will be ignored.
['button', {onclick: B.ev ('submit', 'data', {raw: 'this.value', ignored: 'key'})}]
If you want to call more than one event within the same user interaction, you can do it by wrapping the event arguments into an array, and passing each array as an argument to B.ev
.
['button', {onclick: B.ev (['submit', 'data'], ['do', ['something', 'else']])}]
If the onclick
handler for the button above is called, B.call
will be called twice, first with 'submit', 'data'
as arguments and then with 'do', ['something', 'else']
as arguments.
If you need to submit an event only if a condition is met, you can use an empty array to signal a no-op.
['button', {onclick: B.ev (cond ? ['submit', 'data'], [])}]
The same goes in the context of multiple events, out of which a single one should happen conditionally.
['button', {onclick: B.ev (['submit', 'data'], cond ? ['do', 'something'], [])}]
If invalid inputs are passed to B.ev
, the function will report an error and return false
.
Reactive views: B.view
An essential part of gotoв (or of any frontend framework, really) is the ability to write reactive views. What does reactive mean? It means that the view is automatically updated when the information on which it depends has changed - in other words, it reacts to relevant changes on the store.
Let's go back to the counter
example we saw earlier:
var counter = function () {
return B.view ('counter', function (counter) {
counter = counter || 0;
return ['div', [
['h3', ['Counter is: ', counter || 0]],
['button', {
onclick: B.ev ('set', 'counter', (counter || 0) + 1)
}, 'Increment counter']
]];
});
}
B.mount ('body', counter);
As you can see above, B.view
takes two arguments:
- A
path
. - A
vfun
(view function). Recall thatvfuns
are functions that return liths. This function receives as an argument the value of the store atpath
. This function must always return a lith.
When B.store.counter
is updated, the vfun
will be executed again and the view updated.
If you enter the following command on the developer console to update the store: B.call ('set', 'counter', 1)
, you will notice that the view gets automatically updated!
If you, however, try to update B.store.counter
directly by entering B.store.counter = 2
, you'll notice that... nothing happens! This is because you changed the store directly instead of using an event. Most of the time, you'll change the store through events - though later we'll see how you can sidestep the event system to update the store directly.
B.view
takes a path
and a vfun
as arguments. The path
is exactly like the path
passed to B.call
, B.listen
and B.ev
and can be any of the following:
- A string:
counter
. - An array of strings and integers:
['Data', 'counter']
. - An array of arrays of strings and integers:
[['Data', 'counter'], ['State', 'page']]
.
If the path
is counter
, then the view will be updated when B.store.counter
changes. If the path is instead ['Data', 'counter']
, then the view will be updated when B.store.Data.counter
changes.
By the way, if you passed ['counter']
instead of 'counter'
as the path, the result would be the same: B.view ('counter', ...
is the same as B.view (['counter'], ...
.
If the path
is a list of paths
, as [['Data', 'counter'], ['State', 'page']]
, then the view will be updated when either Data.counter
or State.page
change. If you pass multiple paths, the vfun
will receive multiple arguments, one per path passed, each of them with the value of the relevant part of the store.
If you pass multiple paths
to B.view
, the view will be updated when any of the corresponding store elements change:
var dashboard = function () {
return B.view ([['stockPrice'], ['username']], function (stockPrice, username) {
return ['div', [
['h3', ['Hi ', username]],
['h4', ['The current stock price is: ', stockPrice, 'EUR']]
]];
});
}
B.mount ('body', dashboard);
// Here, the dashboard will have neither a name nor a stock price.
B.call ('set', 'username', 'Oom Dagobert');
// The dashboard now will display an username printed, but no stock price.
B.call ('set', 'stockPrice', 140);
// Now the dashboard will print both an username and a stock price.
The vfuns
must return a single lith, not a lithbag. For example:
var validVfun1 = function () {
return ['h1', 'Hello'];
}
var validVfun2 = function () {
return ['div', [
['h2'],
['h3']
]];
}
// This view is invalid because it returns a lithbag.
var invalidVfun1 = function () {
return [
['h2'],
['h3']
];
}
// The view is invalid because it returns `undefined`.
var invalidVfun2 = function () {
return;
}
By requiring every view to return a lith, there's a 1:1 relationship between a view and a DOM element. This makes both debugging and the implementation of the library simpler. (Why is the simplicity of the implementation important? Because gotoв is also meant to be understood, not just used. And simpler implementations are easier to understand).
If its inputs are valid, B.view
returns the lith produced by the vfun
passed as its second argument. Besides that, it sets up a responder that will be matched when a change
event is fired with a path
that was passed to B.view
.
If it receives invalid inputs, or the vfun
doesn't return a lith, B.view
will report an error and return false
.
Each invocation to B.view
creates a responder with a verb change
. gotoв uses the event system itself to redraw views, so that everything (even redraws) are part of the same event system.
Once gotoв sets a responder for a reactive view, gotoв expects the outermost DOM element of the view to 1) be placed in the DOM; 2) to be in the DOM exactly once, without being repeated. In this way, each view has a 1:1 relationship with a DOM element.
When the view has no corresponding DOM element, gotoв decides it is a "dangling view", which it considers to be an error. If you place the output of B.view
in the DOM through B.mount
, and you do this before calling any change
events which might redraw the view, this will not happen.
A gotoв view can only be placed in the DOM once (and not more than once) because its corresponding outermost element has an id and as such can only exist once in the DOM.
If you want to encapsulate a view in a variable for later reuse in multiple places (even simultaneously), do it as a function that returns an invocation to B.view
, instead of storing a direct invocation to B.view
:
// Please don't do this
var counter = B.view ('counter', function (counter) {
counter = counter || 0;
return ['h1', 'Counter is ' + counter];
});
// Instead, do this
var counter = function () {
return B.view ('counter', function (counter) {
counter = counter || 0;
return ['h1', 'Counter is ' + counter];
});
}
// Then, you can use it like this
B.mount ('body', counter);
// Or instead, you can use it like this
var app = function () {
return [
['h1', 'App'],
counter ()
];
}
B.mount ('body', app);
It is particularly important to be aware of this, since using an invocation to B.view
in multiple places or multiple times can trigger errors that are not immediate and that cannot be detected by gotoв.
By encapsulating the view into a function, you could have two counters simultaneously - each will have its own view:
var counter = function () {
return B.view ('counter', function (counter) {
counter = counter || 0;
return ['h1', 'Counter is ' + counter];
});
}
var app = function () {
return ['div', [
counter (),
counter ()
]];
}
B.mount ('body', app);
It is perfectly possible to nest reactive views:
var app = function () {
return B.view ('username', function (username) {
return ['div', [
['h1', username],
B.view ('counter', function (counter) {
counter = counter || 0;
return ['h2', ['Counter is ', counter]];
})
];
})
}
B.mount ('body', app);
If you pass an id
to the lith returned by a vfun
, an error will be reported. B.view
uses specific ids to track which DOM elements are reactive. B.view
adds also a paths
attribute to the DOM elements, simply to help debugging; the paths
attribute will contain a stringified list of the paths
passed to the reactive view.
var app = function () {
return B.view (['Data', 'counter'], function (counter) {
counter = counter || 0;
// This will generate an error. Don't pass ids to the outermost element of a reactive view.
return ['h1', {id: 'my-counter'}];
});
}
B.mount ('body', app);
// The HTML for the <h1> will be something like <h1 id="в1" path="Data:counter"></h1>
If you nest views, you need to specify different elements for them - that is, one invocation to B.view
cannot simply return another invocation to B.view
.
// This will not work, gotoB will return an error saying that you cannot specify an id on the element returned by the vfun.
var app = function () {
return B.view ('username', function (username) {
return B.view ('counter', function (counter) {
return ['h1', [username, counter]];
});
});
}
// This will work - notice there's a `<div>` and inside of it, an `<h1>`.
var app = function () {
return B.view ('username', function (username) {
return ['div', B.view ('counter', function (counter) {
return ['h1', [username, counter]];
})];
});
}
It is highly discouraged to call events from inside a vfun
, unless you have a great reason to do so (if you do, I'm very curious about your use case, so please let me know!). vfuns
make much more sense as pure functions. Events should be called from rfuns
rather than vfuns
.
var app = function () {
return B.view (['Data', 'counter'], function (counter) {
counter = counter || 0;
// Don't invoke B.call from inside a vfun, unless you have a great reason to!
B.call ('side', ['effects', 'rule']);
return ['h1', counter];
});
}
B.mount ('body', app);
One final point: gotoв requires you to not manipulate the DOM elements of a reactive view. By default, gotoв expects full control over the outermost DOM element of a view and its children - if you modify it directly, gotoв won't be aware of the changes and so errors could happen if those elements are recycled after a redraw. In some situations, however, it is necessary to include an element that you (or more often, a library) will modify. For these cases, you can use the opaque
property (please refer to the advanced topics section).
Writing your own responders & tracking execution chains: x
, B.log
, B.eventlog
, B.get
, B.mrespond
, advanced matching
Most of the logic of a gotoв application will be contained in responders that you write yourself; while you'll still be using the built-in responders (those with verbs set
, add
and rem
), most apps will require you to define responders. In fact, many events will be called from inside responders (with the rest of the events being called directly by user interactions with the DOM).
As we noted above, responders are created with B.respond
and are matched when an event with a matching verb
and path
is called. The logic for a responder goes in the rfun
(responder function). This function receives x
(a context object) as its first argument; it optionally receives further arguments if the matching event was called with extra arguments.
Responders (or more precisely, rfuns
) is where most of the logic of a gotoв app lives.
Going back to the todo example defined above:
B.respond ('create', 'todo', function (x) {
var todo = prompt ('What\'s one to do?');
if (todo) B.call (x, 'add', 'todos', todo);
});
This responder is defined to match events with a verb create
and a path todo
. The rfun
only receives a context object as argument.
Quite often you might need to pass extra arguments to responders. This can be done as follows:
B.respond ('create', 'todo', function (x, important) {
var todo = prompt ('What\'s one to do?');
if (todo) B.call (x, 'add', 'todos', todo);
if (important) alert ('Important todo added.');
});
B.call ('create', 'todo', true);
The true
passed to the event call after the path
gets passed as the second argument to the rfun
. If the event were to be called without extra arguments, important
would be undefined
.
In any case, the rfun
receives always the context object as its first argument. This object contains the following:
verb
, the verb of the event that matched the responder.path
, the path of the event that matched the responder.args
, an array with extra arguments passed to the event. If no extra arguments were passed, this element will be undefined.from
, the id of the event that matched this responder (which is the same value returned by the correspondingr.call
invocation).cb
, a callback function which you only need to use if your responder function is asynchronous.responder
, the matched responder.
Most of these keys are there for completeness sake and are not really necessary most of the time; in fact, args
is actually redundant, since the extra arguments are also passed directly to rfun
. A full description of the context object is available here.
The most useful key of x
is from
. It will contain the id of the event that was called and that in turned matched the responder. This allows to track event chains. For example: event X matches responder Y, then responder Y calls event Z.
To track event chains, pass x
as the first argument to calls to B.call
that you do from inside the rfun
. For example:
B.respond ('foo', 'bar', function (x) {
B.call (x, 'do', 'something');
});
The event call do something
will contain the id of the listener and in this way it will be possible to track where the call came from. More information is available here.
gotoв stores a list of all the events called and all responders matched into B.log
. Since gotoв applications are built around events, This can be extremely useful for debugging an app. Instead of inspecting B.log
with the browser console, you can invoke B.eventlog
, a function which will add an HTML table to the page where you can see all the information about the events. There's a separate section dedicated to logging.
A function you will probably use quite a bit inside responders is B.get
, which retrieves data from B.store
. While you can directly access data from B.store
without it, B.get
is useful to access properties in the store in case they haven't been defined yet. For example, if B.store.user.username
is not defined, if you try to do something like var username = B.store.user.username
and B.store.user
is not present yet, your program will throw an error.
If, instead, you write var username = B.get ('user', 'username')
, if B.store.user
is not present yet then username
will be undefined
.
// B.store is {}
// This will throw an error!
var username = B.store.user.username;
// This will be either `undefined` or bring you the `username` if it's already defined.
var username = B.get ('user', 'username');
B.get
takes either a list of integers and strings or a single array containing integers and strings. These integers and strings represent the path to the part of the store you're trying to access. This path
is the same path
that B.call
(the event calling function) takes as an argument.
If you pass invalid arguments to B.get
, it will return undefined
and report an error to the console.
If you pass an empty path
to B.get
(by passing either an empty array or no arguments), you'll get back B.store
in its entirety.
It is important to notice that B.get doesn't return copies of the referenced objects, but the actual object themselves. If B.get
returns an array or object and you modify it, you'll also be modifying the corresponding object in the store. Most often, you don't want to do this since it can generate an inconsistency between the store and the views. To avoid this problem, you can copy the returned object or array before modifying it using teishi.copy
.
B.call ('set', [], {user: {username: 'foo', type: 'admin'}});
// B.store is {user: {username: 'foo', type: 'admin'}}
var user = B.get ('user');
// If you do this, you'll modify B.user.username!
user.seen = true;
// Better to do this
var user = teishi.copy (B.get ('user'));
user.seen = true;
Note that this is not necessary if 1) you don't need to modify the object or array; or 2) you bring a value that's neither an array nor an object, in which case javascript returns you a copy of the value, not a reference to the value itself.
Responders are active from the moment you create them (with B.respond
) until you remove them with B.forget
(with the exceptions of responders created with the burn
flag, which will be forgotten after being matched once). There's no concept of lifecycle, and most responders will be active for the entire lifetime of you app.
To create multiple responders at once, you can use B.mrespond
, which takes an array of arrays, where each internal array contains the arguments to be passed to B.respond
:
B.mrespond ([
['verb1', 'path1', function (x) {...}],
['verb2', ['another', 'path'], function (x) {...}],
...
]);
You can use regexes on both the verb
and the path
of a responder. For example, if you want to create a responder that is matched by events with verbs
get
and post
you can write it as follows:
B.respond (/^get|post$/, 'bar', function (x) {...});
This responder, however, will only be matched by events with verb
get
or post
and a path
equal to bar
. To make it match all events with a path of length 1, you can use a wildcard for the path:
B.respond (/^get|post$/, '*', function (x) {...});
To make a responder match all events with verbs get
or post
, you need to use the match
property of the responder. For example:
B.respond (/^get|post$/, [], {match: function (ev, responder) {
if (ev.verb === 'get' || ev.verb === 'post') return true;
}}, function (x) {...});
The responder above will be matched by any event with verb get
or post
. The match
parameter effectively supersedes the verb
and path
of the responder. If match
function returns true
, the responder will match the called event.
The change
event, the data functions (B.set
, B.add
, B.rem
) and B.changeResponder
As we saw before, when you call an event with any of the built-in data verbs (set
, add
and rem
), a change
event with the same path
will be called. More precisely, a change
event will be called whenever you call a data verb with 1) valid arguments; and 2) when your invocation actually modifies the sto