Frontend module manipulation library with following features:
- HTML-based module definition
- scoped JS / CSS for vanilla libraries with no bundling required.
- reuseable, extendable components
install @plotdb/block
along with all necessary js libraries:
npm install @plotdb/block @plotdb/rescope @plotdb/csscope @plotdb/semver proxise
and include them:
<script src="path-to-semver/index.min.js"></script>
<script src="path-to-proxise/index.min.js"></script>
<script src="path-to-csscope/index.min.js"></script>
<script src="path-to-rescope/index.min.js"></script>
<script src="path-to-block/index.min.js"></script>
Load a sample block:
mgr = new block.manager(...);
mgr.from({name: 'block name', version: 'x.y.z', path: "index.html"})
.then ({instance, interface}) -> ...
A sample block may look like this:
<div>
<script type="@plotdb/block">
module.exports = {};
</script>
<h1>
hello world!
</h1>
</div>
Similar to web component
, @plotdb/block
modularizes frontend codes into components called block
. A block is defined with a plain HTML file, containing following 3 parts ( all parts are optional ):
- HTML
- CSS
- JavaScript
This is an example of a block file:
<div>
<h1> Hello World! </h1>
<style> /* plain CSS ... */ </style>
<script type="@plotdb/block"> /* plain javascript ... */ </script>
</div>
Since it's just a valid HTML file, User can use different languages ( Sass, TypeScript, Pug, etc ) and transpile when necessary, once the result file is a plain HTML file.
Following is an example with Pug, LiveScript and Stylus with additional Pug filters ( :stylus
and :lsc
) which can be transpiled directly with srcbuild-pug
command provided in @plotdb/srcbuild
:
div
h1 Hello World!
style: :stylus
h1 { color: #543; }
script(type="@plotdb/block"): :lsc
module.export = { init: -> console.log \loaded. };
Script can either be an object described as below, or a function returning that object. Styles will be automatically scoped and limited in this block.
To use a block, we need to know how to identify it. Like npm modules, blocks are defined with name
, version
and an optional ns
, where:
ns
: Namespace, such asnpm
orgithub
. How this works depends on how registry is implemented.name
: Block name. Use the naming convention as npm. e.g.,@loadingio/spinner
version
: Block version in semver format, or labels such asmain
,latest
.
Additionally, files in a block are identified with path
and `type fields:
path
: path of the block definition file inside the modulename@version
.- if omitted, inferred by
type
field, or decided by block manager.
- if omitted, inferred by
type
: type of the requested file. if omitted, inferred frompath
, or decided by block manager.
This block identifier can either be an object or a string, such as this object:
{ns: "local", name: "@plotdb/konfig", version: "1.2.3", path: "index.html"}
with a identical string representation:
local:@plotdb/konfig@1.2.3:index.html
@plotdb/block
provides two methods to convert between string and object identifier:
block.id(obj)
: return the corresponding string representation of an identifier objectobj
.- see below for more options.
block.id2obj(id)
: return the corresponding object represetnation of an string identifierid
.
To access a block file identified by a block identifier, we can use block.manager
:
manager = new block.manager({
/* indicating where we can find the file */
registry: ({ns,name,version,path}) -> "/block/#name/#version/#path/index.html"
});
mananger.init!
.then -> manager.get({name: "my-block", version: "0.1.0"})
.then (blockClass) -> ...
The resolved object from manager.get
is a instance of block.class
, which represents the definition of the given block file. To use it, we have to create an instance of block.instance
from it, such as:
manager.get( ... )
.then (cls) -> cls.create();
.then (instace) -> ...
A block instance can then be injected into web page:
instance.attach({root: document.body})
.then -> ...
Below is a simplified flow of relationship between the above concepts:
- block file (in HTML)
block.manager
: load, convert and cache block files.block.class
: Object representation of a block file.block.instance
: a JS Instance created fromblock.class
As described above, @plotdb/block
contains following basic elements:
block.manager
- to access, register, get and cacheblock.class
block.class
- representing the definition of a block, and is used to generateblock.instance
.block.instance
- object for manipulating state / DOM of a given block.
Additionally, block
itself provides following functions:
-
block.id(obj)
- return an ID corresponding to input object with following possible fields:id
: ifid
exists, it will be returned directly.url
: ifid
is not found abuturl
exists,url
will be returned instead.ns
,name
,version
,path
: if none of above is found, use these to generate an ID.name
is required in this case.version
default tomain
,path
default toindex.html
if not provided.
-
block.id2obj(id)
- reversely convert id into block withns
,name
,version
andpath
fields. -
block.i18n
use(obj)
: useobj
to replacemodule
.language
: current used language.changeLanguage
andaddResourceBundle
: see below inmodule
.- TODO we may want to remove these, and rely on
module
directly.
- TODO we may want to remove these, and rely on
module
: default i18n module object, which should support following APIs:t(..)
: return translated text based on given input.on(name, cb)
: listen to eventname
with listenercb
. expected events:languageChanged
: fired when language changes.
off(name, cb)
: remove listenercb
from eventname
.changeLangauge(ns)
: set default language tons
.addResourceBundle(...)
: add resource bundle, with following parameters ( in order ):lng
: ns for this resource to add.id
: id if any. default undefined.resource
: resource to adddeep
: default true.overwrite
: default true. whether overwrite existing resource or not.
-
block.env(win)
- set current environment towin
.
Since a block is just a plain HTML, it can be stored anywhere once a string can be stored. Common places to store a block may be:
- local in web page: block HTMLs are served directly along the web page.
- remote in web server: block HTMLs are stored as files and can be accessed via Ajax through specific URL.
either way we have to provide a way to load, register, cache these blocks - that is, to manage them, which can be done with the help of block.manager
.
Create a block.manager
instance with
mgr = new block.manager(opt);
where the constructor options are as below:
-
registry
: either function or string, tellblock.manager
where to find remote blocks.function({ns,url,name,version,path,type})
: return URL for given bid of a block.- should respect url or use/transofmr it if provided.
string
: the registry base url. block.manager will look up blocks under this url with this rule:/assets/block/<name>/<version>/<path>
object
:- either an object with
url
andfetch
(optional) field, orlib
andblock
field.url
andfetch
:url({ns,url,name, ...})
will be used to transform given bid to an URL.fetch({ns,url,...}))
will be used to fetchurl
or bid ir provided.- if
url()
is provided, bid will be transformed first.
- if
lib
andblock
:-
registry set here will be used for both block and libraries. To distinquish them, use:
registry: {lib: (-> ...), block: (-> ...)}
-
registry.lib
will be used for querying block ifregistry.block
is omitted.
-
- either an object with
-
rescope
: optional. should be a@plotdb/rescope
object if provided.- will replace internal rescope object if provided.
-
csscope
: optional. should be a@plotdb/csscope
object if provied.- will replace internal csscope object if provided.
-
chain
: optional. fallback manager for chaining block lookup if requested block is not found in current manager.
A block.manager
instance provides following methods:
registry(v)
: updateregistry
dynamically.v
: can be a function, string or an object, similar to the option in constructor.
set({name,version,path,block}): register a block with
name,
versionand
path`.block
: ablock-class
object, explained below.set
also accepts Array of {name,version,block} object for batchingset
.
getUrl({ns,name,version,path})
: get url for a block corresponding to the given block identifier.get({ns,name,version,path,force,ctx})
: return ablock-class
object corresponding to the given block identifier.force
: by default,block.manager
caches result. setforce
to true to forceblock.manager
re-fetch data.ctx
: optional context object for providing context for the requested block class.- note: class context is initialized when
init()
is called, which means that once a class is inited, newctx
provided formanager.get
won't work as expected. To re-initialize, setforce
to true.
- note: class context is initialized when
get
also accept an array of{ns,name,version,path,force}
tuples for batchingget
.- in this case,
get
returns an array ofblock.class
.
- in this case,
from(block-id-obj, attach-opt)
: shorthand for manager.get + class.create + instance.attach + instace.interface- return a Promise which resolves to an object
{interface, instance}
:instance
: created instanceinterface
: created interface
block-id-obj
: block identifier object. seeget()
and above description.attach-opt
: attach options. seeblock.instance
'sattach()
function.
- return a Promise which resolves to an object
chain(mgr)
: set a fallback manager for chaining lookup of requested block.rescope
: rescope object, either global one or customized one.csscope
: csscope object, either global one or customized one.id
: shortcut forblock.id
id2obj
: shortcut forblock.id2obj
block.class
is for generating block instances. It parses the code of a block based on the block specification and convert them into clonable code, preparing for generating block.instance
objects on demand.
We usually don't have to create a block.class
instance manually since block.manager
does this for us, however to manually create one:
cls = new block.class( ... );
manager
: default block manager for this class. mandatoryname
: block name. mandatory.version
: block version. mandatory.path
: block path. optional.index.html
if omitted.code
: use to create DOM / style / internal object. it can be one of following:- a function. should return either html code or object; returned value will be parsed by corresponding rules.
- a string, providing HTML code. structure of HTML should follow the definition of a block.
- an object, containing
dom
,style
andscript
members.dom
: HTML code string, or a function returning HTML code string.style
: should be string for CSS.script
: function, object or string of code, for interface of the internal object by:- function: return the interface.
- object: as the interface.
- string: evaled to the interface, or a function which return the interface.
- for detail of the "interface", see "interface of the internal object" section below.
root
: optional. root of a DOM tree representing the block HTML code. Overwritecode
.ctx
: optional context object, providing additional preloaded dependencies for this class.
create(opt)
: create ablock.instance
based on this object. options:data
: instance data. defined by user and passed directly to block instance javascript.root
andbefore
: parameters passed toattach
.- instance will and only will be attached automatically if
root
is provided.
- instance will and only will be attached automatically if
context()
: get library context corresponding to this block.i18n(text)
: return translated text based on the current context.
Additional, here are the private members:
name
: name of this block.version
: version of this block.path
: path of this block.manager
: block manager to use when resolving recursive blocks.dom
: block DOM tree.scope
: unique id randomly generated each time whenblock.class
is created mainly for scoping purpose.opt
: raw constructor options.code
: source code for constructing this block.script
: source code for this block's script definition.style
: source code for this block's style definition.link
: reserved for future use.styleNode
: node storing parsed / scoped style of this block.interface
: javascript interface for this block.- This will also be used as prototype of the instance object, created by
factory
method below.
- This will also be used as prototype of the instance object, created by
factory
: constructor for generating the js context for block script. See below.id
: unique name for this block.- "name@version/path" or randomly generated one if
name
andversion
is not available.
- "name@version/path" or randomly generated one if
\_ctx
: js context object fromrescope
.csscopes
local
: scope list of css for local scope.global
: scope list of css scope name for global scope.
extend
: extended block, as ablock.class
object.extendDom
: to extend dom or not.extends
: array of extended blocks.extends[0]
is the direct parent class.
To create an instance from a block.class
:
instance = aBlockClass.create();
While block.class
is used to create instance of block.instance
, JavaScript of a block will be executed when a block class is loaded, in order to prepare for upcoming instance creation. No instance context at this time since we only have the block.class
object.
To access block.instance
context, block JavaScript should be implemented based on the factory interface described in the following section. This will be discussed in following section JS context of block instance
.
block.instance
is an instance of block created from a block.class
. It's responsible for maintaining block's state and DOM status.
block
:block.class
for this instance.name
,ns
,version
,path
: as defined in the object identifier.data
: custom data for this instance. usage and spec of this data is defined by the block file.
attach({root, before, data, autoTransform})
: attach DOM and initialize this instance.- block instance is attahed to
root
beforebefore
ifbefore
is provided. - if a factory interface is exported by block JS, it will be used to create an internal context and be inited.
- see
Internal JS context of a block
below.
- see
- return a Promise which resolves with a list of internal object based on inheriance hierarchy after inited.
- when root is omitted, attach block in headless mode ( for pure script )
- attach DOM by
appendChild
whenbefore
is omitted, and byinsertBefore
otherwise. autoTransform
: default null. set toi18n
to enable auto i18n transformation based on i18n module event.- note: will be by default
i18n
in future release. explicitly set to null if that's what you want.
- note: will be by default
- block instance is attahed to
detach()
: detach DOM. return Promise.i18n(text)
: return translated text based on the current context.path(p)
: return url for the given pathp
dom()
: return DOM corresponding to this block. Create a new one if not yet created.run({node,type})
: executetype
API provided byblock
implementation withnode
as root.transform(cfg)
: (re)transform DOM based on the givencfg
option, which is:- string: name of the transform (e.g.,
i18n
) to apply.
- string: name of the transform (e.g.,
update(ops)
: (TBD) updatedatadom
based on provided ops ( array of operational transformation ).
Additionally, following are the private members:
obj
- list of JS internal context objects created from the exported factory interface.- see below for the detail of the internal context object.
- it's a list of all objects from the inheritant chain. base block comes first.
- each item in this list contains block's data and interface.
While block.instance
represents the block instance itself, block JavaScript is run in a different context to prevent intervention. The interface of this context is as below:
\_class
: the object ofblock.class
for this block, filled automatically when creating this context.\_instance
: the object ofblock.instance
for this block, filled automatically when creating this context.- Note: currently this is not available in base blocks. use it only for dev / debug purpose.
pkg
: block informationinit(opt)
: initialization function of this context.destroy(opt)
: destroy function of this context.interface()
: JS interface for block users to access.parent
: JS Context of parent block, if any.- use
parent.interface()
to reach parent interface if needed.
- use
Except \_class
and \_instance
, functions in above interface should be implemented by block JavaScript and exported via module.exports
:
module.exports = {
init: (opt) ->
interface: ->
};
This interface is used in the factory constructor of block.class
to create the internal JS Context:
context = new aBlockClass.factory(instance);
which is the object stored in obj
member of block.instance
described in the block.instance
section.
The detail of the fields of interface is as below:
-
pkg
: block information, described below. optional. -
init(opt)
: initializing a block. optional.- return a Promise for asynchronous initialization.
opt
is an object with following fields:root
: root elementctx
: dependencies in an object.context
: deprecated, usectx
instead.parent
: object for the direct base block.pubsub
: for communication between block in extend chain.pubsub
is an object with following methods:on(event, cb(parmas))
: handle event withcb
callback, params fromfire
.- return value will be passed and resolved to the returned promise of
fire
.
- return value will be passed and resolved to the returned promise of
fire(event, params): fire
event`. return promise.
data
: data passing tocreate
. optional and up to user.path(p)
: path transformer to convertp
to a local string based on the identifier of this block.t(text)
: translation function based on local, base class and global i18n information. shorthand ofi18n.t
.i18n
: i18n related helpers including:-
getLanguage()`: return current used language.
-
t(text)
: as described above. -
addResourceBundles(res)
: dynamically adding i18n resources. sampleres
:{ "zh-TW": {"string", "文字"}, "en-US": {"string": "string"} }
-
-
destroy({root, context})
: destroying a block. optional. -
interface
: for accessing custom object. optional.- either a function returning interface object, or the interface object itself.
- child block always overwrite parents' interface in an inheritance chain, if available
-
mod
: reserved for block javascript. future implement update of@plotdb/block
should not use it. -
exports(global)
: (TBD) for sharing block as a JS library. return objects to export. optional- user can use a block as a library by adding it in the
dependencies
config, such as:- [{name: "some-block", version: "some-vesion", path: "path-to-file"}, ...]
- user can use a block as a library by adding it in the
All members are optional thus the minimal definition will be an empty object or even undefined
:
{}
Use module.exports
to explicitly export the desired object:
module.exports = { .... };
The pkg
field of a block interface is defined as:
-
ns
,name
,version
,path
: from this block's identifier. optional -
author
: author name. optional -
license
: License. optional. -
description
: description of this block. optional -
syncInit
: default false.- if true, each
init
in extend chain runs only after the returned Promise of the previous method resolves. - otherwise, order of init methods are not guaranteed.
- if true, each
-
extend
: optional. block identifier of block to extend.ns
,name
,version
,path
: from parent block's identifier. optionaldom
: default true. can be any of following:true
: use parent's DOM if set true.false
: completely ignore extended DOM in any ancestor."overwrite"
: overwrite parent DOM but extend DOM from grantparent, if any.
style
: default true. can be any of following:true
: use parent's style if set true.false
: completely ignore extended style in any ancestor."overwrite"
: overwrite parent style but extend style from grantparent, if any.
- use
plug
( for html ),parent
andpubsub
( js ) to work with extended block.- for more information about
plug
, seeHTML Plugs
section below.
- for more information about
-
dependencies
: dependencies of this block.- list or modules, in case of mutual dependencies: ["some-url", {url: "some-url", async: false, dev: true, global: true, type: "css, js or block"}]
- for now,
block
type dependencies are used for hint of bundling. - options in object notation:
- `async: true to load this module asynchronously. true by default.
- `global: for CSS. true if the CSS should also work in global scope. ( under body ). default false.
type
: defaultjs
. eithercss
orjs
.- (TBD) support
block
type for preloading block / export block library.
- (TBD) support
url
: path of required module.- generated from name + version + path if omitted. ( TODO )
name
: name of required module ( TODO )version
: version of required module ( TODO )mode
: use to control when this module should be loaded. ( TODO )
- dependencies will be additive in inheritance chain.
-
i18n
:i18next
style i18n resource. e.g.,{ "zh-TW": { "name": "名字" } }
(TBD) Following are possible events:
- before insert
- init
- after insert
- before destroy
- destroy
- after destroy
- update
use block.i18n.use(...)
to switch the core i18n module, which should at least implement following API:
addResourceBundle(lng, ns, resource, deep, overwrite)
changeLanguage(lng)
t(text)
These API are intentionally aligned with i18next
. Check i18next documentation for more information about these API.
A sample setup with i18next
and @plotdb/block
:
i18next.init({supportedLng: ["en", "zh-TW"], fallbackLng: "en"})
.then(function() { i18next.changeLanguage("zh-TW"); })
.then(function() { block.i18n.use(i18next); })
Base block may provide slots for child block to plug. use <plug>
tag with name
attribute:
<plug name="layout"/>
To plug elements In child block to given slot, use plug
attribute in child block:
<div plug="layout"> ... </div>
To ship bundled packages along with a block, simply append the corresponding <template>
tag at the end of the block:
<div> ... </div><template rel="block"> ... </template>
Actually, any template
tag in this block with rel
attribute set to block
will be considered a bundle tag for @plotdb/block
.
At first we just want to make web editing easier across expertise, and block design ( see future of web design comes in blocks, Editor.js ) seems to be a trend in web design. It's similar to web components but we will have to do more for making visually editing possible.
While what @plotdb/block
( web component & management ) provides is already available in other popular frameworks, @plotdb/block
is actually designed with following criteria thus makes it different with others:
- version management
- blocks are managed with proper versioning.
- blocks should work even using the same lib with different versions without
import
.- popular frameworks use
import
which will have to bundle js within. - even if bundle is not necessary, many libs don't support
import
and will need wrapper.
- popular frameworks use
- framework agnostic
- prevent from abducted by specific framework
- while we seem to invent
yet another framework
:@plotdb/block
is only for components. no state management, no reactive.- thus, any js frameworks are expected to work well with
@plotdb/block
.
- As Simple as Possible
- making a component is extremely easy. ( KISS principle )
- there is no new syntax to learn in
@plotdb/block
, only interface.
- there is no new syntax to learn in
- making a component is extremely easy. ( KISS principle )
- Collaborative
@plotdb/block
is built along with@plotdb/datadom
for DOM serialization.- this makes it by default suitable for serialization, thus also for collaboration
- editing can be described by concepts such as operational transformation
- DOM manipulating with UI ( cross expertise editing )
- this is covered in
@plotdb/editable
.
- this is covered in
- Related modules
- editable: cross-expertise editor interface based on a set of predefined attributes over plain HTML.
- datadom: DOM in JSON, with extension.
- registry: block module storage and delivery.
MIT