Swayer – schema based frontend framework ?
UI web framework for controllable and low overhead development.
Description
Pure JavaScript framework, which enables plain objects to describe document
structure, styling and behavior. Swayer developers provide initial data to be
rendered and get dynamic components for further management. This instrument
is provided for low-level development and delivering fully declarative
specific DDLs describing
application domains.
Why not to stick with modified HTML like JSX?
While HTML syntax is really well known – it was created for describing static
web documents, not interactive apps. In any case we have to create abstractions
to make web page dynamic, so we use plain objects with the full power of
JavaScript to create DOM tree with almost no overhead in the fastest way.
Why not to stick with CSS preprocessors like Stylus or Sass?
You simply don’t need to use different CSS-like syntax with Swayer. JavaScript
is more powerful and standardized language than any other style preprocessors.
Moreover, Swayer provides extended standard style declaration for convenience
and brings selector abstraction, so you can just share or toggle styles as a
simple JavaScript object.
Important: Do not assume HTML or CSS to be legacy languages!
Swayer compiles application down to the pure HTML and CSS while making it
consistent with JavaScript.
Features:
- Prue JavaScript everywhere
- Fast asynchronous rendering
- No need to use HTML/CSS preprocessors
- No 3rd party dependencies
- Declarative schema based components
- Configurable styles and animations
- Inline/preload/lazy component loading
- Module encapsulation
- Framework API injection
- Reflective component features
- Local state and methods
- System and custom bubbling events
- Scoped intercomponent messaging
- Component lifecycle hooks
Quick start
- See
online Todo Application demo - Play with examples to
investigate how it works
Swayer component
example: examples/todo-app/app/features/todo/todo.component.js
export default () => ({
tag: 'section',
meta: import.meta,
styles: todoSectionStyle(),
state: {
isMainAdded: false,
},
methods: {
addMain() {
this.children.push(createMain(), createFooter());
this.state.isMainAdded = true;
},
removeMain() {
this.children.splice(1, 2);
this.state.isMainAdded = false;
},
updateRemaining() {
const footer = createFooter();
this.children.splice(2, 1, footer);
},
addTodo(todo) {
const scope = './main/main.component';
this.emitMessage('addTodoChannel', todo, { scope });
},
},
events: {
addTodoEvent({ detail: todo }) {
if (this.state.isMainAdded) this.methods.updateRemaining();
else this.methods.addMain();
this.methods.addTodo(todo);
},
todoChangeEvent() {
if (todoStore.todos.length > 0) this.methods.updateRemaining();
else this.methods.removeMain();
},
},
hooks: {
init() {
if (todoStore.todos.length > 0) this.methods.addMain();
},
},
children: [{ path: './header/header.component', base: import.meta.url }],
});
Swayer documentation
1. Terminology
- Developer – you.
- Framework – Swayer framework.
- Schema – an object partially implementing component property interface.
Includes initial data provided by a developer.- Initial data – data set associated with corresponding html node to be
rendered. - Schema config – an object describing configuration data for lazy
loaded schema. - Lazy schema – a schema loaded from a different module on demand.
- Initial data – data set associated with corresponding html node to be
- Component – an object instantiated by the framework using schema. Provides
access to component API for developer.- Children – an object extending Array class. Provides methods for
updating component children as a part of API. - API – a set of properties and methods to help developer with component
management. - Hook – a component lifecycle handler.
- Children – an object extending Array class. Provides methods for
- Intercomponent messaging – a way of organizing data flow between different
components based on channels feature.- Channel – a pub/sub entity, that provides a name for scoped data
emission and subscription based on event emitter.
- Channel – a pub/sub entity, that provides a name for scoped data
- Event management – a way of organizing children-to-parent data flow based
on native bubbling DOM events. - Reflection – a technique of metaprogramming. Enables instant data updates
of underlying DOM while changing component properties. - Styles – an object extending native CSSStyleDeclaration interface. Enables
component styling by abstracting CSS selectors and providing convenient
properties for style management.
2. Startup
Application starts by serving static files from the app folder.
Entry point: index.html – a single piece of html in the whole app.
<!DOCTYPE html>
<script async type="module" src="./app/main.js"></script>
Bootstrap point: app/main.js
Import bootstrap function from Swayer package and pass a schema or schema config
object:
bootstrap({
path: './pages/index.component',
base: import.meta.url,
});
Important: you have to bootstrap with html component to be able to manage
components like title or meta.
3. Swayer component system
Basically all schemas in Swayer are converted into components during
runtime. These components represent N-ary tree data structure and are
traversed with
Depth first preorder tree traversal algorithm. With some performance
optimizations this approach delivers fast asynchronous rendering for best
user experience.
As the application grows it becomes hard to manage all component schemas in a
single file. To address this issue Swayer uses ES6 standard modules to separate
application parts and load them on demand.
-
Schema config is used to lazily load schema and pass input arguments.
- Schema config declaration syntax:
interface SchemaConfig { path: string; // absolute or relative path to module base?: string; // module url, usually import.meta.url, mandatory only if relative path is used args?: any; // optional arguments for component factory }
- Schema config usage examples:
{ path: '/app/features/header/header.component'; }
{ path: './header/header.component.js', // skipping .js extension is available base: import.meta.url, args: { title: 'Header title', }, }
{ path: '/app/features/header/header.component', args: 'Header title', }
- Schema config declaration syntax:
-
Schema factory is used to construct lazily loaded schemas with input
arguments. It should be declared in a module as a default export. Then it will be
available for sharing with other components.- Schema factory declaration syntax:
export default (args: any) => Schema;
- Schema factory usage examples:
export default () => ({ tag: 'h1', text: 'Here is my own title', });
export default ({ title }) => ({ tag: 'h1', text: title, });
- Schema factory declaration syntax:
-
Tag is an HTML element name – the simplest possible schema.
- Tag declaration syntax:
tag: string; // any HTML element name
- Tag usage example:
{ tag: 'div'; }
- Tag declaration syntax:
-
Meta is a configuration object for the component being created. You can
useimport.meta
standard metadata object to pass some instructions to Swayer component. There
is a module url declared inside module metadata by default. At the moment
it is only used by channels feature, but it can be extended with other options
in the future.- Meta declaration syntax:
meta: ComponentMeta; // see types/index.d.ts for type info
- Meta usage example:
{ tag: 'div', meta: import.meta, }
- Meta declaration syntax:
-
Text property corresponds to element’s text node.
- Text declaration syntax:
text: string;
- Text usage example:
{ tag: 'button', text: 'Click me', }
- Text declaration syntax:
-
Children include schemas, that belong to particular parent schema. Such
approach is dictated by the tree-like nature of any web document. This
extended array can hold schema, which is declared inside the same module,
or schema config containing the path to the module with schema.- Children declaration syntax:
children: ComponentChildren<Schema>;
- Children usage examples:
{ tag: 'div' children: [ { tag: 'span', text: 'Hello ' }, { tag: 'span', text: 'world' }, ], }
{ tag: 'div' children: [ { path: '/absolute/path/to/hello.component' }, { path: './relative/path/to/world.component', base: import.meta.url, args: { title: 'A simple title' }, }, ], }
- Children declaration syntax:
-
Attrs object corresponds to a set of element’s attributes.
- Attrs declaration syntax:
interface Attrs { // key-value attribute, see types/index.d.ts for more type info attrName: string; }
- Attrs usage example:
{ tag: 'input', attrs: { name: 'age', type: 'text', }, }
- Attrs declaration syntax:
-
Props object corresponds to a set of element’s properties.
- Props declaration syntax:
interface Props { // key-value property, see types/index.d.ts for more type info propName: string; }
- Props usage example:
{ tag: 'input', props: { value: 'Initial input value', }, }
- Props declaration syntax:
-
State is a custom object, where developer should store component related
data.- State declaration syntax:
state: object;
- State usage example:
{ tag: 'button', state: { clickCounter: 0, }, }
- State declaration syntax:
-
Methods are used to share some UI related code between listeners,
subscribers and hooks.- Methods declaration syntax:
interface Methods { methodName(args: any): any; }
- Method usage example:
{ tag: 'form', methods: { prepareData(data) { // `this` instance is a reference to component instance // do something with data }, }, }
- Methods declaration syntax:
-
Events are used to listen to system or synthetic DOM events. There is a
native event mechanism used under the hood, so it’s good to leverage
event delegation for bubbling events. Common usage is reacting for user
actions and gathering user information. Additionally, you can transfer data to
parent components with custom events, what is a bit simpler than using
channels.- Listeners declaration syntax:
interface Events { eventName(event: Event): void; }
- Listeners usage example:
{ tag: 'input', events: { // event name matches any system events like click, mouseover, etc input(event) { // `this` instance is a reference to component instance // do something with event }, }, }
{ tag: 'ul', events: { // event name matches emitted custom event name removeTodoEvent({ detail: todo }) { // `this` instance is a reference to component instance // do something with todo data }, }, }
- Custom event emission declaration syntax:
// component API emitCustomEvent(name: string, data?: any): boolean;
- Custom event emission usage example:
this.emitCustomEvent('removeTodoEvent', todo);
- Listeners declaration syntax:
-
Channels feature implements pub/sub communication pattern and is used
for intercomponent messaging. The implementation leverages EventEmitter
under the hood to manage subscriptions. This is a powerful way of creating data
flow between components whenever they are located in the project. To prevent
channel name conflicts, what is highly possible in big apps, a sender has to
provide a scope of subscribers, so that only selected components receive
emitted messages.Important: you have to add { meta: import.meta } into schema if using
channels.- Subscribers declaration syntax:
interface Channels { channelName(dataMessage: any): void; }
- Subscribers usage example:
{ tag: 'form', meta: import.meta, channels: { // channel name matches developer defined name on emission addTodoChannel(todo) { // `this` instance is a reference to component instance // do something with todo data }, }, }
- Message emission declaration syntax:
// component API emitMessage(name: string, data?: any, options?: ChannelOptions): void;
// Component API interface MessageOptions { // path or array of paths to folder or module // defaults to current module scope?: string | string[]; }
- Message emission usage examples:
// subsribers declared only in the same module will receive todo message this.emitMessage('addTodoChannel', { todo });
// subsribers declared only in main.component.js module will receive todo message const scope = './main/main.component'; this.emitMessage('addTodoChannel', { todo }, { scope });
// subsribers declared in all modules under main folder will receive todo message const scope = '/app/main'; this.emitMessage('addTodoChannel', { todo }, { scope });
// subsribers declared in header and footer modules will receive todo message const scope = ['./header/header.component', './footer/footer.component']; this.emitMessage('addTodoChannel', { todo }, { scope });
- Subscribers declaration syntax:
-
Hooks are the special component handlers. They are typically used to run
code at some point of component lifecycle. For example, it’s possible to
initialize some data when component and its children are created and ready to
be managed. Right now init hook is available.- Hooks declaration syntax:
interface Hooks { init(): void; }
- Hooks usage example:
{ tag: 'form', hooks: { init() { // `this` instance is a reference to component instance // run initialization code }, }, }
- Hooks declaration syntax:
4. Component styling
Styles in Swayer are simple JavaScript objects extending CSSStyleDeclaration
standard interface. All CSS properties are available in camelCase. It’s possible
to add inline styles via attrs.style attribute or create CSSStyleSheets.
Swayer extends styling syntax by adding intuitive properties like hover as
it would be another set of CSS. Such approach enables CSS selector
abstraction, so that developer’s cognitive work is reduced. Pseudo-classes,
pseudo-elements and animations are implemented with this abstraction.
Styles declaration syntax see in types/index.d.ts.
Styles usage examples:
- Inline style (not preferred):
{ tag: 'p', attrs: { style: { // these props will be inlined fontSize: '14px', color: 'red', }, }, }
- CSS style properties:
{ tag: 'p', styles: { // simply add some CSS properties fontSize: '14px', color: 'red', }, }
- Pseudo classes/elements:
{ tag: 'p', styles: { transition: 'backgroundColor 0.2s ease', // make this component blue on hover hover: { backgroundColor: 'blue', }, // make the first-of-type text red first: { color: 'red', }, }, }
{ tag: 'p', styles: { color: 'red', // make the first-of-type blue on hover first: { transition: 'backgroundColor 0.2s ease', hover: { backgroundColor: 'blue', }, }, }, }
{ tag: 'p', styles: { position: 'relative', // add before pseudo-element before: { content: `''`, position: 'absolute', right: '0', }, }, }
- Functional pseudo-classes:
{ tag: 'p', styles: { // apply style rule equivalently to nth-of-type(2n) nth: { arg: '2n', rule: { borderBottom: '1px solid red', color: 'red', }, }, }, }
- Animations:
{ tag: 'div', styles: { // create multiple animations and apply them to component animations: [ { name: 'fadeIn', props: 'linear 3s', keyframes: { 'from': { opacity: 0, }, '50%': { opacity: 0.5, }, 'to': { opacity: 1, }, }, }, { name: 'fadeOut', props: 'linear 3s', keyframes: { from: { opacity: 1, }, to: { opacity: 0, }, }, }, ], }, }
{ tag: 'p', styles: { // apply existing animations to component animations: [ { name: 'fadeIn' }, { name: 'fadeOut', props: 'ease-out 2s' }, ], }, }
5. Component reflection
Component properties are meant to be live. This behavior makes updates to be
automatically applied to underlying HTML elements. At the moment reflection is
supported for the following features:
- Text
- Attrs, including inline style
- Props
- Events
Reflection for other features is going to be added in future releases.
6. Component API
Swayer creates some instruments for component management enabling dynamic
application development. While bootstrapping a component Swayer enriches
context used in developer-defined methods, events, channels and hooks.
Only object method declaration syntax is applicable as it’s impossible
to change the context of arrow functions. Basically this reference is
a reference to a component, not schema. Right now the list of component API
is the following:
- Properties:
original
– reference to original schema.
- Methods:
emitCustomEvent(name: string, data?: any): boolean
– emits a synthetic
DOM event bubbling up through the component hierarchy, see Events section
for more details. Returns the result of
nativedispatchEvent(event: Event): boolean
emitMessage(name: string, data?: any, options?: ChannelOptions): void
–
emits a data message to the channel by name. See Channels section for more
details. Returns void.click(): void
– native click method.focus(): void
– native focus method.blur(): void
– native blur method.
- Children methods:
push(...schemas: Schema[]): Promise<Component[]>
– adds a new component
to the end of children.pop(): Component
– removes the last child component.splice(start: number, deleteCount: number, ...replacements: Schema[]): Promise<Component[]>
–
deletes or replaces several component children.
See types/index.d.ts for more type information. This API will be extended with
new properties and methods in future releases.
7. Application architecture and domain code
Swayer does not provide any restrictions of creating domain logics, but it’s
very likely that the framework will implement some architectural best practises.
At the moment it’s recommended to design apps with feature components and
separated domain code. Conventionally, developers should use only UI related
code in components and business code in separate modules using class instances
as singletons.
See examples to learn
how it works.
Browser compatibility
- Chromium based browsers (v80+)
- Firefox (v90+)
- Safari (v14.1+)
- Opera (v67+)
License & Contributors
Copyright (c) 2021 Metarhia contributors.
See GitHub for
full contributors list
.
Swayer framework is MIT licensed.
Project coordinator: <[email protected]>