starting-blocks
v5.0.7
Published
A JS page and blocks framework written in ES6 to go along Roadiz CMS or any other great CMS.
Downloads
28
Readme
Starting Blocks
Starting Blocks is a framework for ajax page, transitions and blocks written in vanilla JS, made by Rezo Zero.
- Features
- Install with Yarn or NPM
- Usage
- Services
- DOM structure
- Page
- Block
- Options
- Events
- Docs
- Naming conventions
- Improving Starting Blocks
- Compatibility
- Demo
- Go further with Starting Blocks
- Contributors
Features
- Pjax : fetch pages and handling
History
state - Transition manager : enhance your website with custom page transitions
- Page/Blocks pattern : build dynamic and interactive sections (map, slideshow, Ajax forms, canvas…)
And more...
- In view detection
- Splashscreen
- Prefetch
- Cache
Install with Yarn or NPM
yarn add starting-blocks
npm install starting-blocks --save
Usage
Starting Blocks is bundled with NPM in order to use native ES6 import
syntax.
We highly recommend to use a module bundler and to write your code in ES6 syntax. If you use a bundler like
Webpack, it will be able to remove dead code (i.e. when a Starting Blocks service is not used) if you use curly brace syntax.
Minimal configuration :
import StartingBlocks from 'starting-blocks'
import HomePage from './pages/HomePage'
import UsersBlock from './blocks/UsersBlock'
// Create a new Starting Blocks instance
const startingBlocks = new StartingBlocks({
// ...options
})
// For each page or block sections: you must map the service name
// with the data-node-type attribute (see DOM Structure section).
// «c» parameter stands for Starting-Blocks service container
// Register page services factory
startingBlocks.instanceFactory('HomePage', c => {
return new HomePage(c)
})
// Register block services factory
startingBlocks.instanceFactory('UsersBlock', c => {
return new UsersBlock(c)
})
// 🚀 Boot the whole thing
startingBlocks.boot()
Services
Starting Blocks use a dependency injection container to delegate object creation and handle dependencies. You can use or provide these standard services to enhance your website :
| Service name | Init type | Ready to use | Dependencies | Description
| --- | --- | --- | --- | --- |
| Pjax
| bootableProvider
| true | History
| Enable Ajax navigation on all your website internal links
| History
| provider
| true | | | Helps to keep track of the navigation state
| Prefetch
| bootableProvider
| true | Pjax
| Prefetch links on mouse enter (useful with Pjax
)
| CacheProvider
| provider
| true | | Cache ajax requests (useful with Pjax
)
| Splashscreen
| bootableProvider
| false | | Add a splash screen for the first init
| TransitionFactory
| bootableProvider
| false | | Instantiate page transitions objects according to your Pjax context
Pjax
import StartingBlocks, { History, Pjax } from 'starting-blocks'
// ...Instantiate starting blocks
// Add service
startingBlocks.provider('History', History)
startingBlocks.bootableProvider('Pjax', Pjax)
// ...Boot
⚠️ Don't forget to prepare your DOM adding specific data attributes and required classes, see DOM structure section
Splashscreen
We implemented a Splashscreen service that is triggered at the first website launch.
Create your own class that extends our AbstractSplashscreen
:
import { AbstractSplashscreen, Dispatcher, EventTypes } from 'starting-blocks'
export default class Splashscreen extends AbstractSplashscreen {
constructor (container) {
super(container, 'Splashscreen')
//... custom values
}
// You need to override this method
hide () {
return new Promise(resolve => {
// custom logic, animations...
resolve()
})
}
}
import Splashscreen from './Splashscreen'
// ...Instantiate starting blocks
// Add service
startingBlocks.bootableProvider('Splashscreen', Splashscreen)
// ...Boot
TransitionFactory
When AJAX navigation is enabled, transitions are triggered to manage animations between pages.
To choose a custom transition, you can set data-transition
attribute with a transition name on each link:
<a href="/contact" data-transition="fade">Contact</a>
Then, create a TransitionFactory
class and register it into Starting Blocks as a service.
In this class, you must implement getTransition (previousState, state)
method.
This method will be called on each transition and will give you access to history
state informations:
previousState
andstate
- transitionName :
data-transition
attributes of the clicked link - context : equal to
"history"
,"link"
- transitionName :
Example:
// src/factories/TransitionFactory.js
import DefaultTransition from './transitions/DefaultTransition';
import FadeTransition from './transitions/FadeTransition';
export default class TransitionFactory {
getTransition (previousState, state) {
switch (state.transitionName) {
case 'fade':
return new FadeTransition()
default:
return new DefaultTransition()
}
}
}
How to register your Transition factory service?
import TransitionFactory from '../factories/TransitionFactory'
// ...Instantiate starting blocks
// Add service
startingBlocks.provider('TransitionFactory', TransitionFactory)
// ...Boot
To create a new transition you need to write a new class extending our AbstractTransition
boilerplate. Implement start()
method and use Promises
to manage your animation timeline.
⚠️ Be careful, you have to wait for this.newPageLoading
Promise resolution to make sure the new page DOM is ready.
Then, you have access to old and new Page
instances.
Example with fade animation (we use TweenLite from gsap for this example):
// src/transitions/FadeTransition.js
import AbstractTransition from '../AbstractTransition'
import { TweenLite } from 'gsap'
/**
* Fade Transition example. Fade Out / Fade In between old and new pages.
*/
export default class FadeTransition extends AbstractTransition {
/**
* Entry point of the animation
* Automatically called on init()
*/
start () {
// Wait new content and the end of fadeOut animation
// this.newPageLoading is a Promise which is resolved when the new content is loaded
Promise.all([this.newPageLoading, this.fadeOut()])
// then fadeIn the new content
.then(this.fadeIn.bind(this))
}
/**
* Fade out the old content.
* @returns {Promise}
*/
fadeOut () {
return new Promise(resolve => {
TweenLite.to(this.oldPage.rootElement, 0.4, {
alpha: 0,
onComplete: resolve
})
})
}
/**
* Fade in the new content
*/
fadeIn () {
// Add display: none on the old container
this.oldPage.rootElement.style.display = 'none'
// Prepare new content css properties for the fade animation
this.newPage.rootElement.style.visibility = 'visible'
this.newPage.rootElement.style.opacity = '0'
// Scroll to the top
document.documentElement.scrollTop = 0
document.body.scrollTop = 0
// fadeIn the new content container
TweenLite.to(this.newPage.rootElement, 0.4, {
autoAlpha: 1,
onComplete: () => {
// IMPORTANT: Call this method at the end
this.done()
}
})
}
}
DOM structure
This ES6 javascript framework has been designed to handle either complete HTML responses or
partial HTML responses to lighten backend process and bandwidth.
One of the most useful Page and Block property is rootElement
that will always refer to the current page/block main-section.
In a page context, rootElement
is the DOM element which is extracted at the end of each completed AJAX requests.
When it detects that HTTP response is partial, it initializes rootElement
using the whole response content. Every new DOM content will be appended to the #sb-wrapper
HTML section in order to enable cross-transitions between old and new content.
In a block context, rootElement
will store the DOM element with [data-node-type]
attribute.
When Pjax
service is used and window.fetch
supported, all links inside document body are listened to fetch pages asynchronously and make transitions.
To declare a partial DOM section as the rootElement
you must add some classes and
data to your HTML tags.
<!-- Page (HomePage.js) -->
<div id="page-content-home"
class="page-content"
data-node-type="HomePage"
data-node-name="home"
data-is-home="1"
data-meta-title="Home page">
<!-- Block (GalleryBlock.js) -->
<div id="home-page-gallery"
class="page-block"
data-node-type="GalleryBlock">
<img src="https://media.giphy.com/media/6LBVNUvqzY3tu/giphy.gif" alt="Display me…">
</div>
</div>
id
attribute is obviously mandatory as it will be used to update your navigation and some other parts of your website. Make sure that your ID is not the same as to yourdata-node-name
.page-content
class is essential in order to extract your DOM element during AJAX request. You can customize this class name inStartingBlock
options (pageClass: "page-content"
).data-node-type
attribute will be used to map your element to the matching JS class (in this example:HomePage.js
). Every class must extend theAbstractPage
orAbstractBlock
class. Then you have to declare your pages and blocks services in your Starting Blocks instance viainstanceFactory
method.data-node-name
is used to name your page object and to rename body class and ID after it.data-is-home
: 0 or 1data-meta-title
attribute will be used to change your new page title (document.title
), it can be used in other cases such as some social network modules which require a clean page-title.
You’ll find index.html
and page1.html
examples files. You can even test them
by spawning a simple server with npm run serve
command.
Then go to your favorite browser and type http://localhost:8080
.
Page
Each custom page needs to extend our AbstractPage
class to be registered as a service in your Starting Blocks instance.
Be careful that data-node-type
attribute matches with your service declaration.
By default Starting Blocks will instantiate the DefaultPage
class if no data-node-type
attribute is found.
Best practice : Create your own DefaultPage
with your common features then override the default
service to use it as a common base for your custom pages :
import { AbstractPage } from 'starting-blocks'
export default class CustomDefaultPage extends AbstractPage {
//... common methods
}
// Custom page example
export default class HomePage extends CustomDefaultPage {
//... home page custom methods
}
import CustomDefaultPage from './pages/CustomDefaultPage'
// ...Instantiate starting blocks
// Override the DefaultPage service with your own
startingBlocks.instanceFactory('DefaultPage', c => {
return new CustomDefaultPage(c)
})
// ...Boot
Page overridable methods
| Method | Description |
| --- | --- |
| onResize
| On viewport resize, this method is debounced. |
Block
A block is a section of your page. It can be a Slideshow
, an ajax form, a map...
Starting Blocks automatically maps those DOM elements with a custom ES6 class in the same way the future CustomElementRegistry
will perform.
Create your own class extending our AbstractBlock
or AbstractInViewBlock
then register them as a service.
data-node-type
attribute content and your service name must match.
Common block overridable methods in AbstractBlock
| Method | Description |
| --- | --- |
| onResize
| On viewport resize, this method is debounced of 50ms. |
| onPageReady
| Triggered once all page blocks have been created. |
In-view block overridable methods in AbstractInViewBlock
| Method | Description |
| --- | --- |
| onIntersectionCallback
| Triggered when in view block state changed (in or out). |
| onScreen
| Called when block is in the viewport. |
| offScreen
| Called when block is out of the viewport. |
AbstractInViewBlock
extends AbstractBlock
and thus implements each of its methods too.
Options
You can pass some options when instantiating StartingBlocks
object:
| Parameter | Type | Default | Description | | --- | --- | --- | --- | | wrapperId | string | 'sb-wrapper' | | pageBlockClass | string | 'page-block' | | pageClass | string | 'page-content' | | objectTypeAttr | string | 'data-node-type' | | noAjaxLinkClass | string | 'no-ajax-link' | | noPrefetchClass | string | 'no-prefetch' | | manualDomAppend | boolean | false | To manually manage page build directly in transition instances
Events
| Const name | Event name | Description |
| --- | --- | --- |
| BEFORE_PAGE_LOAD
| SB_BEFORE_PAGE_LOAD
| Before Router initialize XHR request to load new page. |
| AFTER_PAGE_LOAD
| SB_AFTER_PAGE_LOAD
| After window.fetch
XHR request succeeded. |
| AFTER_DOM_APPENDED
| SB_AFTER_DOM_APPENDED
| After Router appended new page DOM to wrapperId
. |
| AFTER_PAGE_BOOT
| SB_AFTER_PAGE_BOOT
| After Router create new page instance. |
| CONTAINER_READY
| SB_CONTAINER_READY
| |
| TRANSITION_START
| SB_TRANSITION_START
| |
| TRANSITION_COMPLETE
| SB_TRANSITION_COMPLETE
| |
| BEFORE_SPLASHSCREEN_HIDE
| SB_BEFORE_SPLASHSCREEN_HIDE
| |
| AFTER_SPLASHSCREEN_HIDE
| SB_AFTER_SPLASHSCREEN_HIDE
| |
Docs
To generate documentation, you’ll at least NodeJS v4.4 and to install ESDoc.
npm run doc;
Documentation will be available in doc/
folder.
Naming conventions
We dropped jQuery in Starting-blocks v5 and we changed several variables names.
We suffixed every DOMElement
variable with Element
, Container
, Elements
or Containers
.
Examples:
let mainContainer = document.getElementById('main-container')
let imageContainer = document.getElementById('image-container')
let imageElements = imageContainer.querySelectorAll('.image')
Improving Starting Blocks
To work locally on Starting Blocks, you’ll find some HTML files in examples/
folder.
- Install dependencies:
yarn
. - Type
npm run dev
to improve Starting Blocks locally. - Type
npm run build
to optimize project in one file as:main.js
. - Type
npm run demo
to build demo project inexamples/
folder.
Compatibility
Starting Blocks use native Promise
, fetch
, IntersectionObserver
and MutationObserver
browser features.
Don't forget to use some polyfills for old browsers.
Demo
To launch the example you need to change the examples/srv/js/config/config.example.js
file with your own informations.
Go further with Starting Blocks
If you use Webpack you will be able to dynamically lazy-load your blocks with a custom BlockBuilder
.
Create a custom BlockBuilder and override the default one:
import { AbstractBlockBuilder } from 'starting-blocks'
export default class WebpackAsyncBlockBuilder extends AbstractBlockBuilder {
// Dynamic import
async getBlockInstance (nodeTypeName) {
try {
const Block = await this.getModule(nodeTypeName)
if (!this.hasService(nodeTypeName)) {
this.container.$register({
$name: nodeTypeName,
$type: 'instanceFactory',
$value: c => {
return new Block(c)
}
})
}
return this.getService(nodeTypeName).instance()
} catch (e) {
console.error(e.message)
return null
}
}
async getModule (nodeTypeName) {
return import(`../blocks/${nodeTypeName}` /* webpackChunkName: "block-" */)
.then(block => {
return block.default
})
}
}
Then override the default PageBuilder
service:
// Custom block builder (dynamic import)
startingBlocks.provider('BlockBuilder', WebpackAsyncBlockBuilder)