
Good day to all.
I continue the fascinating series of articles about creating powerful Single Page Application on
basis.js .
Last time, we philosophized a bit, as well as we got acquainted with the token - one of the most important things in basis.js.
Today we will talk about working with data.
Immediately make a small comment.
This cycle is a set of
manuals describing the solution of various tasks in the field of building SPA using the basis.js framework.
Manuals do not set themselves the goal - to duplicate the
official documentation , but show the practical application of what is described there.
Yes, and the reader wants to see more specifics and practical examples, rather than a retelling of the documentation.
Some places will still be described in more detail. Basically, these are the moments that I consider necessary to describe in my own way.
Let's imagine the situation:
You make a page with an interactive list:
Opportunities:
- add / delete entries
- saving records on the server
- After the page loads, the saved entries are automatically downloaded from the server and displayed.
- while loading and saving records, the add and save buttons must be locked
- while loading and saving records, the message "loading ..." is displayed
- if you delete all records, the message "no records" is displayed
Perhaps you have already begun to imagine how to solve this problem, write cycles, conditional statements, and add event handlers.
To prove this, you need to get acquainted with some conceptual things in basis.js.
In basis.js there are several wrappers for different data types:
Value - a wrapper for scalar values
DataObject - object wrapper
Dataset - a set of elements of type DataObject
')
Value is very similar to
Token (which we talked about in the last article), but has richer functionality and a number of additional methods.
A DataObject is an object where data changes can be monitored. In addition,
DataObject provides a
delegation mechanism.
Dataset provides convenient mechanisms for working with a collection of objects.
Also, I suggest you
refer to the relevant section of the documentation for a more detailed acquaintance with what data represents in basis.js. And now we will examine another important thing from basis.js arsenal.
Value :: query
The static method
Value :: query is one of the most powerful features of basis.js.
This method allows you to get the actual value through the entire chain of specified properties, relative to the object to which
Value :: query is applied.
In order to understand how this works, let's write the following code:
index.jslet Value = basis.require('basis.data').Value; let DataObject = basis.require('basis.data').Object; let Node = basis.require('basis.ui').Node; let group1 = new DataObject({ data: { name: ' 1' } }); let group2 = new DataObject({ data: { name: ' 2' } }); let user = new DataObject({ data: { name: '', lastName: '', group: group1 } }); new Node({ container: document.querySelector('.container'), template: resource('./template.tmpl'), binding: { group: Value.query(user, 'data.group.data.name') }, action: { setGroup1() { user.update({ group: group1 }) }, setGroup2() { user.update({ group: group2 }) } } });
template.tmpl <div> <div> : {group} </div> <div class="btn-group"> <button class="btn btn-success" event-click="setGroup1"> 1</button> <button class="btn btn-danger" event-click="setGroup2"> 2</button> </div> </div>
There is a user. The user has a group in which he is a member.
Using the buttons on the page, we can change the user group.
As a result of calling
Value :: query, we will get a new
Value , which will contain the current value in the specified sequence of properties relative to the specified object.
In the example shown, we create a
group binding, the value of which is the name of the group specified for the user.
But we can switch the group. How in this case to understand that the value is updated?
In order to answer this question, you need to dig deeper into the basis.js bowels.
In the prototype or instance of any basis.js class, you can specify the special propertyDescriptors
property , with which you can “tell” the
Value :: query method when it should update its value.
Let's look at how the
DataObject class is
described in the source.js source code:
var DataObject = AbstractData.subclass({ propertyDescriptors: { delegate: 'delegateChanged', target: 'targetChanged', root: 'rootChanged', data: { nested: true, events: 'update' } },
From this it follows that, if you specify the
data property in the query, then the
Value :: query mechanism will update the value each time an
update event from this object occurs (that is, when the object data is changed).
And now let's take another look at the request we made:
Value.query(user, 'data.group.data.name')
The
Value :: query mechanism will split the specified request into parts and try to go deep into the object by the specified properties, automatically subscribing to the events specified in the
propertyDescriptors of each participant in the path.
Thus, the result of calling
Value :: query always "knows" about the current value for the specified path, relative to the specified object.
Data state
Let's return to our task.
Items in our list are data that you can add, load, and save.
Loading and saving are data synchronization operations.
The basis.js contains the concept of states. This means that each data type in basis.js has several states:
- UNDEFINED - data status unknown (default state)
- PROCESSING - data in the process of loading / processing
- READY - data loaded / processed and ready to use.
- ERROR - an error occurred while loading / processing data
- DEPRECATED - data is out of date and must be synchronized again.
We can switch these states depending on what is happening now.
Let's look at the sequence of actions on the example of downloading our list from the server:
You can come up with a lot of cases on the use of this mechanism. Here are just some of them:
- when the data set is in PROCESSING state - the save and add buttons must be locked
- when data set is in ERROR state - show error message
Loading and saving data are frequent operations in the SPA, so there is a separate
basis.net module for them in
basis.js .
As mentioned earlier, it is necessary to switch data states depending on the synchronization step.
There are two ways you can switch states:
basis.net.action is designed precisely to create
stub functions for data synchronization.
The bottom line is that these procurement functions themselves know when and to what state they need to switch data.
Let's create a component that will load data from the server and display them as a list of text fields, with the ability to edit and delete.
Seems laborious? By no means!
index.js let Dataset = require('basis.data').Dataset; let Node = require('basis.ui').Node; let action = require('basis.net.action');
That's all, now it remains only to sketch out the markup and to forward the necessary values ​​into it:
list.tmpl <b:style src="./list.css" ns="my"/> <div> <div class="my:list"> <div{childNodesElement}/> </div> </div>
item.tmpl <b:style src="./item.css" ns="my"/> <div class="input-group my:item"> <input type="text" class="form-control input-lg" value="{name}" event-input="input"> <span class="input-group-btn"> <button class="btn btn-default btn-lg" event-click="onDelete"> <span class="glyphicon glyphicon-remove"></span> </button> </span> </div>
CSS leave at your discretion. But, as you probably already guessed, I use bootstrap.
So, we created the
cities dataset and configured it to synchronize with the server - we indicated that the elements of the set should be taken at
/ api / cities .
Data can be taken from any source, but I have already raised a server that gives a list of cities (it will be in the repository for the article).
After receiving the data, they must be placed in a set.
For this we use the
Dataset # set method. It takes an array of
DataObjects to put in the set.
But, as an answer from the server comes an array of ordinary JS-objects and before putting them into a set, you need to convert these objects into a
DataObject .
Record
this.set(response.map(data => new DataObject({ data })))
can be significantly reduced by using the auxiliary function “basis.data.wrap”:
let wrap = require('basis.data').wrap;
wrap takes an array of ordinary objects as input, and outputs an array of the same objects, but wrapped in a
DataObject .
Also note that we added the
dataSource property for our component and switched the
active property to
true .
Based on what is
described in the documentation , our set has
an active subscriber, which means someone needs the contents of this set.
Since the set is initially empty and its state is set to
UNDEFINED , immediately after the registration of the active subscriber, the set begins synchronization according to the rules specified earlier. The resulting set objects will be associated with the DOM nodes of the view.
This behavior is already in
Node . As soon as a set appears in the
dataSource property,
Node starts tracking changes to the specified set.
For each element of the set, it creates a child view (component) that is associated with the set by delegation.
If the set changes the composition of elements, then the visual representation also changes.
So basis.js saves us from cycles and other logic in the templates, while ensuring synchronization of data with their visual presentation.
Data binding implies that the elements of a set and their visual presentation begin to share data using
delegation .
This simplifies the mechanism for updating set elements.
Now we will display the words "loading ..." during the synchronization of the set.
To do this, we will monitor the status of the set and display the inscription "loading ..." only when the set is in the
PROCESSING state
index.js let STATE = require('basis.data').STATE; let Value = require('basis.data').Value;
Use the new binding in the template:
list.tmpl <b:style src="./list.css" ns="my"/> <div> <div class="my:list"> <div class="alert alert-info" b:show="{loading}">...</div> <div{childNodesElement}/> </div> </div>
Now, during the synchronization of the set, the inscription "loading ..." will be displayed
In the example shown, we create a
loading link which should indicate whether the synchronization process is in progress or not. Its value will depend on the state of the data set —
true if the set is in the
PROCESSING state and
false otherwise.
If a
dataSource is specified for the
Node , then the
Node # childNodesState property will duplicate the state of the specified data source.
More details
can be read here .
By the way, as can be seen from the example, if you specify
Value :: query as a binding, but do not specify the object relative to which the specified path is built, then this object becomes the
Node , in which
Value :: query is located.
And even if
Node’s data source changes, then the
loading loading will still keep the current value based on the
currently installed data source. This fact once again shows the benefits of using
Value :: query .
For reference:
Value.query('childNodesState')
could be replaced by
Value.query('dataSource.state')
The result would be the same. But in the case of
childNodesState, we completely abstract away from the data source and rely on the basis.js mechanisms.
Fine! It remains to realize a few points.
If there are no records in the set, then we will show the corresponding message.
But first, let's think - in what case should this message be displayed?
At a minimum, when the set has no elements (the
itemCount property of the set is zero).
Let's create the appropriate binding:
new Node({
But we have a period of time when we still do not know whether there are elements in the list or not. For example, when data is downloaded from the server. While the data is being loaded, we cannot say for sure whether something will be there or not. Therefore, we do not like the option in which we rely on only one value.
A more competent condition for displaying a message is:
show a message if synchronization is complete and the number of elements is zero .
That is, the value of the binding will depend on two
Value .
In basis.js such tasks are usually solved with the help of
Expression .
Expression accepts
Token-like objects as arguments and a function that will be executed when the value of any of the arguments passed has changed.
It looks like this:
index.js let Expression = require('basis.data.value').Expression;
Thus, in
empty binding will be
true as long as there are no elements in the set and the set itself is not in synchronization state. Otherwise,
empty will be
false .
Now we add the created binding to the markup:
list.tmpl <b:style src="./list.css" ns="my"/> <div> <div class="my:list"> <div class="alert alert-info" b:show="{loading}">...</div> <div class="alert alert-warning" b:show="{empty}"> </div> <div{childNodesElement}/> </div> </div>
Now, if you delete all items from the list or from the server comes an empty list, then the screen will display a message - "the list is empty."
It remains for us to implement the last opportunity from our list - adding and saving list items.
Here we will use the already familiar things.
First, add a couple of buttons to the markup:
save and
add . Thus, the final layout will take the following form:
list.tmpl <b:style src="./list.css" ns="my"/> <div> <div class="navbar navbar-default navbar-fixed-top"> <div class="container"> <div class="my:buttons btn-group"> <button class="btn btn-success" event-click="add" disabled="{disabled}"></button> <button class="btn btn-danger" event-click="save" disabled="{disabled}"></button> </div> </div> </div> <div class="my:list"> <div class="alert alert-info" b:show="{loading}">...</div> <div class="alert alert-warning" b:show="{empty}"> </div> <div{childNodesElement}/> </div> </div>
As you can see from the example, the buttons should be disabled when the binding is set to
true .
Now we will process clicks on the buttons, we will implement the addition and saving of elements, and finally, we will look at the final version of the code:
index.js let Value = require('basis.data').Value; let Expression = require('basis.data.value').Expression; let Dataset = require('basis.data').Dataset; let DataObject = require('basis.data').Object; let STATE = require('basis.data').STATE; let wrap = require('basis.data').wrap; let Node = require('basis.ui').Node; let action = require('basis.net.action'); let cities = new Dataset({ syncAction: action.create({ url: '/api/cities', success(response) { this.set(wrap(response, true)) } }),
The
save method is created by analogy with
syncAction . Called
save when you click
save .
Adding elements to the list is made as simple as possible: when you click on
add, it is enough just to add another object to the set, and the internal linking mechanisms will arrange everything so that the new element of the set will be displayed in the visual representation accordingly.
As mentioned above, such problems are solved in basis.js without involving cycles and conditional operators. Everything is implemented on the basis of mechanisms.js.
Of course, inside basis.js there are both cycles and conditional operators, but it is important that basis.js allows us to reduce their use to a minimum. In the client code and especially in the templates.
That's all. I hope it was interesting and informative.
Until the next manual!
Many thanks to
lahmatiy for invaluable advice;)
Some useful links:
UPD: launched
gitter chat on basis.js. Add, ask questions.