MISPAF is a minimalist framework for developping single page applications in pure JavaScript. It is loosely inspired by JQuery, React, and the MVC architecture, among others. It is designed to offer a very simple and straightforward solution to common use cases, without abstracting too much away from pure DOM manipulations. In other words, while typical frameworks offer abstractions that hides away the DOM mechanic, and makes you think in other terms, this framework keeps the DOM as the central abstraction point, while making it much easier to work with.
mispaf.addPage({
id: 'login',
class:"center",
html: `
<div class="box">
<form data-id="form">
<div class="boxtop">
<div>
<label for="inputlogin">Authentifiant</label> : <input type="text" name="login"
id="inputlogin">
</div>
<div>
<label for="inputpassword">Mdp</label> : <input type="password" name="password"
id="inputpassword">
</div>
<div class="error"></div>
</div>
<hr>
<input type="submit" data-id="cancel" value="Annuler" class="roundedButton">
<input type="submit" data-id="ok" value="Ok" class="roundedButton">
</form>
</div>
`,
type: 'modal',
enter(event) {
if ("leavePage" in event) this.old=event.leavePage;
mispaf.reset(this.form);
},
'click:[data-id="cancel"]'() {
mispaf.page(this.old);
},
async 'click:[data-id="ok"]'() {
try {
await mispaf.api('auth/login', this.form);
mispaf.page('home');
} catch (e) {
mispaf.page('error',e.message);
}
}
});
In this example, we add a page which can be shown by mispaf.page("login")
, or by an HTML <a href="#login">
tag. The keys of the object define this page :
id
sets its HTMLid
attribute, and is used to identify the page.class
sets its CSS class, here center ensures it is displayed in the center of the window.html
sets the HTML content of this page, using a multi-line ES6 template string. Note thedata-id="form"
HTML attribute: it bindsthis.form
to this DOM element, in the context of the functions defined here. Alternatively, you can embed the page HTML directly in index.html: make sure the HTMLid
attribute is set to this objectid
property, and do not provide thishtml
property at all.type
sets this page so that it is displayed as if it was a modal dialog box.enter
defines a function that is called each time this page is displayed. Theevent
parameter is an object gives context to the display of this page. For example,leave
contains the id of the page we just left. Also, in the context of execution of the function,this
is the DOM of the page, augmented by properties bound by thedata-id
HTML attributes of this page. Here,data-id="info"
is set on the div where the message should be displayed thanks tothis.info.innerText=message
.click:...
is a shortcut way to bind theclick
event on the...
element in this page. Here, the Cancel button goes back to the page we where coming from, while the Ok button submits the login form to the server. Also note that we did not need to prevent the browser from submitting the form on its own (which makes no sense for a single page application): when bound this way,event.preventDefault()
is always called implicitly.event.leavePage
contains the id of the page that was shown before this modal dialog box. It is saved inthis.old
so that the function for the cancel button sends back to it by usingmispaf.page(this.old)
.mispaf.reset(this.form)
clears all inputs, selects, and textareas of the form.this.form
is automatically set to the DOM element of the form, because it has this HTML attributedata-id="form"
.mispaf.api('auth/login', this.form)
emits an Ajax call to theauth/login
endpoint. The payload of the HTTP request is defined bythis.form
: as this is bound to a form DOM element, the content of the form is serialized as the payload.mispaf.api
eventually returns the response of the back-end, which is ignored in this example. If the status code of the reply is not OK (200), then an exception is thrown, whosemessage
contains the response from the server.
As a single page application, the role of the server is limited to:
- Serve a single index.html page, and its resources
- Process HTTP request from the front-end. They are organized in the controlers folder, and automatically loaded when the server starts.
- Database access code is centralized in the models folder, and also automatically loaded when the server starts.
Here is an example with the models/users.js file:
module.exports = ({ db }) => {
return {
register(user) {
db.prepare("INSERT INTO users(login,password) VALUES (?,?)").run(user.login, user.password);
},
getByLogin(login) {
return db.prepare("SELECT * FROM users WHERE login=?").get(login);
},
list() {
return db.prepare("SELECT * FROM users").all();
},
delete(id) {
return db.prepare("DELETE FROM users WHERE id=?").run(id);
},
updatePassword(id,password) {
db.prepare("UPDATE users SET password=? WHERE id=?").run(password,id);
}
}
}
The db
object is a better-sqlite3 object already opened by the server. The SQLite3 database file can be configured in config.js
.
Here is an example with the controlers/auth.js file:
const bcrypt = require("bcrypt");
module.exports = {
requireAuth: false,
async login({ params, model, setUser, HTTPError }) {
let user = model.users.getByLogin(params.login);
if (user && await bcrypt.compare(params.password, user.password)) {
setUser({ login: user.login });
} else {
throw new HTTPError("Invalid user/password", 403);
}
},
async register({ params, model }) {
let hash = await bcrypt.hash(params.password1, 10);
model.users.register({login:params.login, password:hash});
},
async whoami({ user }) {
return user;
},
async logout({clearUser}) {
clearUser();
}
}
Each function can be triggered by an HTTP request whose path is auth/function name
. requireAuth
is an exception, which tells if an authentified user is required before being able to call the functions, at all. Unless explicitly set to false
, an authentified user is required.
When called, each function receives an object as parameter, which contains many useful keys:
params
is an object containing the payload of the request, typically the content of a form.model
references the model files. Here, thegetByLogin
function from themodel/users.js
file is called bymodel.users.getByLogin
.setUser
is a function that provides authentification information to the server. This enables the server to protect the controler functions requiring an authentification from being called, at all.clearUser
removes the authentification information from the server.user
is the user object provided by callingsetUser
before.HTTPError
is an error class that embeds the HTTP status code of the error.
As you see, controler functions do not need to process the HTTP request and response objects directly. Anything returned by the function is JSON encoded as the HTTP reply. If an HTTPError exception is thrown, then the HTTP status and response are set according to the exception.
module.exports={
dbfile:"app.db", // SQLite3 database file
secret:"mqldsjkf mqlsdkfj mlqsdf kqmslfj", // JWT secret token
port:8080, // Web server's port
maxupload:"200mb", // Maximum size of Ajax calls payload
uploadDirectory:"uploads" // Directory to put uploaded files
}
Model files are put in the server/models
directory. Each file must export a function that takes an object as parameter, and returns an object whose keys/values define the model functions. The object parameter contains the value of the configuration object with an added db
key, which contains the SQLite3 Database object (see better-sqlite3 documentation).
The model functions of each file XXX.js
are given to the controler functions through the model.XXX
key of their parameter.
Controler files are put in the server/controlers
directory. Each file must return an object whose keys/values define the endpoints for the HTTP requests whose path is composed of name of the controler file/name of the function
. The one exception key is requireAuth
which can be set to false
to disable the requirement of having an authentified user before calling the function.
When called, each function receives a parameter object with these keys:
params
: The payload of the request, as an associative object.request
: The HTTPrequest
object.response
: The HTTPresponse
object. If you use it to respond to the request, then the return of the function is ignored. Otherwise, the return of the function is JSON encoded as the response to the request.model
: An object to access the model functions.setUser(u)
,getUser()
,user
: manages the authentified user.config
: the config object,HTTPError
: a custom class that manages HTTP errors. Its constructor takes two parameters: 1. string that is the response message to the HTTP request, and 2. an integer that is the status code of the response.
Views files are put in the www/views
directory. Each file is dynamically loaded by index.html, there is no need to add <script>
tags.
Typically, each view calls mispaf.addPage
to set up a single page of the application.
The API of mispaf
is composed of several parts:
-
Form management:
mispaf.serializeObject(form)
: encodes the interactive elements ofform
into an object. Keys are the names of the elements. Checkboxes are encoded as booleans; radios are encoded as null when none checked, or the value of the one checked; files, reset, submit, are ignored and not encoded at all; the rests are encoded using their values.mispaf.deserializeObject(form,data)
: decodes the content of the data object into the interactive elements ofform
.mispaf.reset(form)
: clears the form by resetting all interactive elements.mispaf.validateNotEmpty(form,fields,msg)
: validates that the fields ofform
whose name are in thefields
array are not empty. When this is not the case, the error messagemsg
is inserted after the field. Returnstrue
is all fields are not empty, otherwisefalse
.mispaf.setFieldError(form,name,msg)
: inserts the error messagemsg
in the page after the field withname
inform
. Ifmsg
is null, then it removes the error message.mispaf.clearErrors(form)
: removes all error messages inform
.
-
HTML utility functions:
mispaf.escape(string)
: replaces special HTML characters in String to their HTML entities equivalent, ensuring the string can be inserted into source HTML and still be displayed as intended.mispaf.unescape(string)
: reverse ofescape
.mispaf.parentElement(element,selector)
: returns the closest ancestor element ofelement
that corresponds to the CSSselector
.
-
Ajax calls:
mispaf.ajax({url,type,data,success,error,mimeType})
: emits an Ajax call.url
is the endpoint,type
is the HTTP method type (GET by default),data
is the payload,success
anderror
are callbacks in case of a succesful response (HTTP status 200) or not, andmimeType
can be used to enforce a specific mime type for the payload. The payload can either be an object, or a form DOM element. In the later case,<input type="file">
are supported (by using a base64 encoding).mispaf.api(url,data)
: emits an Ajax call, and returns a promise.url
is the endpoint, the HTTP method is always set to POST, anddata
is the payload. The payload can either be an object, or a form DOM element. In the later case,<input type="file">
are supported (by using a base64 encoding).
-
Navigation between the pages of the application:
mispaf.page()
returns theid
of the currently displayed page.mispaf.page(id, data)
changes the currently displayed page to the one defined byid
. The optionaldata
parameter is passed as the second parameter of theenter
function of this page.
-
Menu management:
mispaf.setMenu(menuElement)
manages<a href="#page">
elements inside the rootmenuElement
, so that the links effectively display the pages, and so that the link corresponding to the currently displayed page is highlighted.
-
Page creation:
mispaf.addPage(page)
adds a page, using the configuration specified inpage
:id
sets its HTMLid
attribute, and is used to identify the page.class
sets its CSS class.html
sets the HTML content of this page. Alternatively, you can embed the page HTML directly in index.html: make sure the HTMLid
attribute is set to this objectid
property, and do not provide thehtml
property at all in that case.type
when set tomodal
, this page appears as a modal dialog box, with whatever previously page displayed blurred behind it. Otherwise, each page replaces the previous one.create
defines a function that is called after the insertion of this page HTML in the current document. Use this function if you need to further initialize the components displayed for example.enter
defines a function that is called each time this page is displayed. The first parameter (event
) is an object that gives context to the display of this page. For example,leave
contains the id of the page we just left, andleavePage
has the same information but only for pages that are not modal.target
contains the id of this page, as it is the target of this navigation operation. If you callevent.preventDefault()
, you cancel the navigation to this page. The second parameter is furtherdata
that is transmitted when callingmispaf.page(id,data)
. Also, in the context of execution of the function,this
is the DOM of the page, augmented by properties bound by thedata-id
HTML attributes of this page.leave
defines a function that is called when this page is currently displayed, and we are in the process of navigating to another page. Once again, callingevent.preventDefault()
on the firstevent
parameter cancels the migration.event:selector
is a shortcut way to bind the DOMevent
to the elements of this page that follow this CSSselector
. The listener function does not need to callevent.preventDefault()
to prevent the navigator from doing its own form submission. As this makes no sense for a single page application, this is done bymispaf
automatically.