Swayer – schema based frontend framework ?

npm version
npm downloads/month
npm downloads
snyk
license

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

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.
  • 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.
  • 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.
  • 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 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,
      });

  • Tag is an HTML element name – the simplest possible schema.

    • Tag declaration syntax:
      tag: string; // any HTML element name
    • Tag usage example:

      {
        tag: 'div';
      }

  • Meta is a configuration object for the component being created. You can
    use import.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,
      }

  • Text property corresponds to element’s text node.

    • Text declaration syntax:
      text: string;
    • Text usage example:

      {
        tag: 'button',
        text: 'Click me',
      }

  • 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' },
          },
        ],
      }

  • 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',
        },
      }

  • 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',
        },
      }

  • 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,
        },
      }

  • 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
          },
        },
      }

  • 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);

  • 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 });
  • 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
          },
        },
      }

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
      native dispatchEvent(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]>

GitHub

View Github