A lightweight framework for non SPA websites.
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
dist
docs
example
src
tests
.babelrc
.browserslistrc
.editorconfig
.eslintignore
.eslintrc
.gitignore
.nvmrc
.travis.yml
LICENSE
Makefile
README.md
build.js
package-lock.json
package.json

README.md

Pacto

Build Status Coverage Status on Coveralls Coverage Status on Codecov Known Vulnerabilities

A lightweight framework for non SPA websites.

Why?

There are a lot of great frameworks out there which are supposed to be used for single page applications (SPA). When using them on regular websites it's hard to apply those frameworks to an already server-side rendered DOM or to enhance certain sections with some interaction candy. On the other hand, the network payload to ship an SPA framework is quite huge when using for example only the state management or virtual DOM of that framework. This may results in a higher time to interactive due to network traffic, parse, interpret and execution time.

Pacto tries to reduce those problems by shipping small features which are using latest browser features like IntersectionObserver and WeakMap.

The Concepts

In contrast to other libraries, pacto follows the traditional approach of an MVC framework. The core of pacto is a context instance. This is based on a typical event bus where events can be added, removed and triggered (pubsub pattern). The context instance also allows adding actions to certain events. An action is designed to hold a part of the application logic. Each time a relevant event occurs, an added action will run to execute a logic like to update a state, to fetch or to recalculate data.

These actions allow creating modules. Each module should contain at least one initialize action but can be composed of multiple actions, stores, states, services, views etc. This initialize action is meant to be the entry point of each module. It setups and executes its module:

pacto app module structure

The state management is not solved by pacto, but there is small backbone/mobx inspired model and collection extension for pacto called pacto.model.

Installation

Pacto is available on NPM:

npm install pacto --save

Requirements

Pacto is dependency free, but it requires latest browser features. So you may need to add a polyfill for WeakMap. When using InitializeLazy you can also add a polyfill for IntersectionObserver.

Using dynamic imports, this boilerplate can be used to load all required polyfills before loading and running the app:

(function(src){
	Promise.all([
		(!!window.WeakMap || import('weakmap-polyfill')),
		(!!window.IntersectionObserver || import('intersection-observer')),
	]).then(() => {
		var script = document.createElement('script');
		script.type = 'text/javascript';
		script.charset = 'utf-8';
		script.async = true;
		script.defer = true;
		script.src = src;
		document.body.appendChild(script);
	});
})('/path/to/app-using-pacto.js');

Documentation

Context

An instance of pacto's Context has typical known properties of an EventEmitter like .on(), .off() and .trigger(). It also allows to handle Actions and store/receive Values in each instance.

import {Context} from 'pacto';

const context = new Context();
context
	.on('event:type', (event) => console.log('The event occurred.', event))
	.trigger('event:type', {foo: 'bar'})
	.off('event:type');

Actions

An Action is a class which can bound to a specific event. Each action class needs to contain at least a .run() method. When an action relevant event is dispatched through the context, an instance of the action class will be created and executed. The instance of each action has access to the context and the passed event data which triggered the execution of that action.

Action management is done by the .actions property of the context instance.

import {Context} from 'pacto';

class Action {
	run() {
		console.log('I am an action', this.context, this.event);
	}
}

const context = new Context();
context.action.add('event:type', Action);
context.trigger('event:type', {foo: 'bar'}); // logs: 'I am an action', {context}, {event}

Read more about the actions API.

Initialize

The initialize action setups a module and wires a view to a DOM element. Each initialize action is described by its settings: selector, view, namespace. The selector is a CSS valid selector to define which elements to use for each view instance. The created view instance is grouped in a list of views by the initialize action. This list is stored inside the context values using a given namespace (take a look at Values).

// Initialize.js
import {Initialize} from 'pacto';
import {View} from 'mymodule/views/View';

export class Action extends Initialize {
	get settings() {
		return {
			selector: '.mymodule',
			namespace: 'mymodule:views'
			view: View
		};
	}
}

// App.js
import {Context} from 'pacto';
import {Action as MyModule} from 'mymodule/actions/Initialize';

const context = new Context();
context.action.add('app:start', [
	MyModule,
	// Add more modules here...
]);
context.trigger('app:start');

The initialize action of pacto ships some hooks which are called while executing and creating views. These hooks can be used by overwriting them:

  • beforeAll()
  • beforeEach(options, el, index)
  • afterEach(view, el, index)
  • afterAll(views)

beforeAll, beforeEach and afterEach can return false to skip the current execution phase.

InitializeLazy

Using an app bundler like webpack, parcel or rollup allows using code splitting by defining dynamic imports. Using them creates a smaller app build by separating them into chunks. The InitializeLazy action of pacto offers the possibility to simply use that feature and only load a certain module when its corresponding element exists inside the users DOM. If at least one of these elements is found and visible, the initialize action of that module will be imported, instantiated and executed. Once loaded the specific action will replace the lazy action.

pacto app module structure with lazy initialize action

// Initialize.js
import {Initialize} from 'pacto';
import {View} from 'mymodule/views/View';

export class Action extends Initialize {
	get settings() {
		return {
			selector: '.mymodule',
			namespace: 'mymodule:views'
			view: View
		};
	}
}

// InitializeLazy.js
import {InitializeLazy} from 'pacto';

export class Action extends InitializeLazy {
	get settings() {
		return {
			selector: '.mymodule',
		};
	}

	get import() {
		return import('mymodule/actions/Initialize');
	}
}

// App.js
import {Context} from 'pacto';
import {Action as MyLazyModule} from 'mymodule/actions/InitializeLazy';

const context = new Context();
context.action.add('app:start', [
	MyLazyModule,
	// Add more modules here...
]);
context.trigger('app:start');

Values

The .values property of a context instance is a key/value storage. Each type of value can be stored using a unique namespace (key).

import {Context} from 'pacto';

const context = new Context();
context.values.add('name:space', {foo: 'bar'});
console.log(context.values.has('name:space')); // logs: true
console.log(context.values.get('name:space')); // logs: {foo: 'bar'}

context.values.remove('name:space');
console.log(context.values.has('name:space')); // logs: false
console.log(context.values.get('name:space')); // logs: undefined

Read more about the values API.

View

A view is a simple wrapper class for DOM elements. It holds the references to its DOM element and context. An instance of this class is meant to be the communicator between user interactions and pacto framework actions. It is also the right place to do complex renderings using virtual DOM libraries by overwriting the render() function.

import {View} from 'pacto';

class ToggleButton extends View {
	render() {
		this.el.addEventListener('click', (event) => {
			this.el.classList.toggle('foo');
			this.context.trigger('togglebutton:toggle');
		});
	}
}

EventEmitter

All objects that emit events are instances of the EventEmitter class. These objects expose an .on() function that allows one or more functions to be attached to named events emitted by the object. To remove those attached functions the .off() function can be used. When a specific event is dispatched on an EventEmitter instance, all attached functions by that event are called synchronously.

License

LICENSE (MIT)