Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/plotdb/block


https://github.com/plotdb/block

Last synced: about 2 months ago
JSON representation

Awesome Lists containing this project

README

        

# @plotdb/block

Frontend module manipulation library with following features:

- HTML-based module definition
- scoped JS / CSS for vanilla libraries with no bundling required.
- reuseable, extendable components

## Usage

install `@plotdb/block` along with all necessary js libraries:

npm install @plotdb/block @plotdb/rescope @plotdb/csscope @plotdb/semver proxise

and include them:





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:



module.exports = {};


hello world!


## Concept

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:


Hello World!


/* plain CSS ... */
/* plain javascript ... */

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.

### Block Identifier and File Accessing

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 as `npm` or `github`. 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 as `main`, `latest`.

Additionally, files in a block are identified with `path` and `type fields:

- `path`: path of the block definition file inside the module `name@version`.
- if omitted, inferred by `type` field, or decided by block manager.
- `type`: type of the requested file. if omitted, inferred from `path`, 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/[email protected]: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 object `obj`.
- see below for more options.
- `block.id2obj(id)`: return the corresponding object represetnation of an string identifier `id`.

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) -> ...

### Manager, Class and Instance of Block

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 from `block.class`

## Core modules

As described above, `@plotdb/block` contains following basic elements:

- `block.manager` - to access, register, get and cache `block.class`
- `block.class` - representing the definition of a block, and is used to generate `block.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`: if `id` exists, it will be returned directly.
- `url`: if `id` is not found abut `url` 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 to `main`, `path` default to `index.html` if not provided.
- `block.id2obj(id)` - reversely convert id into block with `ns`, `name`, `version` and `path` fields.
- `block.i18n`
- `use(obj)`: use `obj` to replace `module`.
- `language`: current used language.
- `changeLanguage` and `addResourceBundle`: see below in `module`.
- TODO we may want to remove these, and rely on `module` directly.
- `module`: default i18n module object, which should support following APIs:
- `t(..)`: return translated text based on given input.
- `on(name, cb)`: listen to event `name` with listener `cb`. expected events:
- `languageChanged`: fired when language changes.
- `off(name, cb)`: remove listener `cb` from event `name`.
- `changeLangauge(ns)`: set default language to `ns`.
- `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 add
- `deep`: default true.
- `overwrite`: default true. whether overwrite existing resource or not.

- `block.env(win)` - set current environment to `win`.

### block.manager

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`.

#### constructor options

Create a `block.manager` instance with

mgr = new block.manager(opt);

where the constructor options are as below:

- `registry`: either function or string, tell `block.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///`
- `object`:
- either an object with `url` and `fetch` (optional) field, or `lib` and `block` field.
- `url` and `fetch`:
- `url({ns,url,name, ...})` will be used to transform given bid to an URL.
- `fetch({ns,url,...}))` will be used to fetch `url` or bid ir provided.
- if `url()` is provided, bid will be transformed first.
- `lib` and `block`:
- 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 if `registry.block` is omitted.

- `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.

#### APIs

A `block.manager` instance provides following methods:

- `registry(v)`: update `registry` 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`, `version` and `path`.
- `block`: a `block-class` object, explained below.
- `set` also accepts Array of {name,version,block} object for batching `set`.
- `getUrl({ns,name,version,path})`: get url for a block corresponding to the given block identifier.
- `get({ns,name,version,path,force,ctx})`: return a `block-class` object corresponding to the given block identifier.
- `force`: by default, `block.manager` caches result. set `force` to true to force `block.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,
new `ctx` provided for `manager.get` won't work as expected. To re-initialize, set `force` to true.
- `get` also accept an array of `{ns,name,version,path,force}` tuples for batching `get`.
- in this case, `get` returns an array of `block.class`.
- `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 instance
- `interface`: created interface
- `block-id-obj`: block identifier object. see `get()` and above description.
- `attach-opt`: attach options. see `block.instance`'s `attach()` function.
- `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 for `block.id`
- `id2obj`: shortcut for `block.id2obj`

### block.class

`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( ... );

#### constructor options

- `manager`: default block manager for this class. mandatory
- `name`: 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` and `script` 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. Overwrite `code`.
- `ctx`: optional context object, providing additional preloaded dependencies for this class.

#### APIs

- `create(opt)`: create a `block.instance` based on this object. options:
- `data`: instance data. defined by user and passed directly to block instance javascript.
- `root` and `before`: parameters passed to `attach`.
- instance will and only will be attached automatically if `root` is provided.
- `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 when `block.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.
- `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` and `version` is not available.
- `\_ctx`: js context object from `rescope`.
- `csscopes`
- `local`: scope list of css for local scope.
- `global`: scope list of css scope name for global scope.
- `extend`: extended block, as a `block.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

`block.instance` is an instance of block created from a `block.class`. It's responsible for maintaining block's state and DOM status.

#### constructor options

- `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.

#### APIs

- `attach({root, before, data, autoTransform})`: attach DOM and initialize this instance.
- block instance is attahed to `root` before `before` if `before` 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.
- 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` when `before` is omitted, and by `insertBefore` otherwise.
- `autoTransform`: default null. set to `i18n` 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.
- `detach()`: detach DOM. return Promise.
- `i18n(text)`: return translated text based on the current context.
- `path(p)`: return url for the given path `p`
- `dom()`: return DOM corresponding to this block. Create a new one if not yet created.
- `run({node,type})`: execute `type` API provided by `block` implementation with `node` as root.
- `transform(cfg)`: (re)transform DOM based on the given `cfg` option, which is:
- string: name of the transform (e.g., `i18n`) to apply.
- `update(ops)`: (TBD) update `datadom` 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.

### Internal JS Context of a block

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 of `block.class` for this block, filled automatically when creating this context.
- `\_instance`: the object of `block.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 information
- `init(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.

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 element
- `ctx`: dependencies in an object.
- `context`: deprecated, use `ctx` 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 with `cb` callback, params from `fire`.
- return value will be passed and resolved to the returned promise of `fire`.
- `fire(event, params): fire `event`. return promise.
- `data`: data passing to `create`. optional and up to user.
- `path(p)`: path transformer to convert `p` 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 of `i18n.t`.
- `i18n`: i18n related helpers including:
- getLanguage()`: return current used language.
- `t(text)`: as described above.
- `addResourceBundles(res)`: dynamically adding i18n resources. sample `res`:

{ "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"}, ...]

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 = { .... };

#### Block Information

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.
- `extend`: optional. block identifier of block to extend.
- `ns`, `name`, `version`, `path`: from parent block's identifier. optional
- `dom`: 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` and `pubsub` ( js ) to work with extended block.
- for more information about `plug`, see `HTML Plugs` section below.
- `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`: default `js`. either `css` or `js`.
- (TBD) support `block` type for preloading block / export block library.
- `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": "名字" }
}

#### Block Events

(TBD) Following are possible events:

- before insert
- init
- after insert
- before destroy
- destroy
- after destroy
- update

## i18n configuration

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](https://www.i18next.com/overview/api) 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); })

## HTML Plugs

Base block may provide slots for child block to plug. use `` tag with `name` attribute:

To plug elements In child block to given slot, use `plug` attribute in child block:

...

## Packed Block with Bundled Packages

To ship bundled packages along with a block, simply append the corresponding `` tag at the end of the block:

...
...

Actually, any `template` tag in this block with `rel` attribute set to `block` will be considered a bundle tag for `@plotdb/block`.

## Why block

At first we just want to make web editing easier across expertise, and *block design* ( see [future of web design comes in blocks](https://thecode.co/block-web-design/), [Editor.js](https://editorjs.io/) ) 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.
* 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.
* 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`.

## Resources

- 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.

## License

MIT