SepCon JS

Probably not the last JS framework you'll live to see


Project maintained by ronstovsky Hosted on GitHub Pages — Theme by mattgraham

Index

Classes

Features

Data

The data object is a static object that every component of SepCon can gain access to. It has no abillity to add, remove and change any of its properties. We have the modifier for these matters.

Declaration

Once declared - the data object exists at the SepCon ecosystem. No need to handle any returned value of the creation method createData. No need to create instances of that data object.

import SepCon from 'sepcon';
SepCon.createData({
  id: 'user'
}, {
  name: 'Mr. Man',
  age: 99,
  gender: 'female',
  //...
});

Lifecycle

Modifier

The modifier is what we need in order to add, edit and remove values of data objects.

Declaration

The modifier has a methods property, which holds down all methods that are used as an API for all components. It could also use routes in order to increase its abillity to manage data. It has a mount lifecycle support.

import SepCon from 'sepcon';
SepCon.createModifier({
  id: 'user'
}, {
  methods: {
    setName(name) {
      this.setProps('user', { name });
    }
  },
  routes: [],
  lifecycle: {
    mount() {}
  }
});

Lifecycle

Component

Declaration

The declaration is what basically registers a new customElement to the DOM. Once a representing element is appended to the DOM - it initializes a new Component derived from the Component Definition.

import SepCon from 'sepcon';
SepCon.createComponent({
  id: 'myComponent'
}, {
  state: {
    //the Component State is defined here
  },
  view: {
    //the Component View is defined here
  }
});

The Component State

Every component has its own state. You cannot share this state with other components, even of the same definition.

Declaration

The component state is declared as per as the component itself. It won’t be declared seperately.

import SepCon from 'sepcon';
SepCon.createComponent({
  id: 'myComponent'
}, {
  state: {
    props: {
      //the Properties Segregation is defined here
    },
    methods: {
      //the Methods Segregation is defined here
    },
    lifecycle: {
      mount() {},
      change(changed) {}
    }
  }
});

Lifecycle

The changed object

The change lifecycle method will get 1 argument, the changed object - a map of the changed properties in a component state.

{
  changedProp1: {
    newValue: 1,
    oldValue: 0
  },
  changedProp2: {
    newValue: 'hello',
    oldValue: null
  }
}

The State Segregations

The state has an internal segregation that’s hidden from the component’s scope itself - Local, External and Global. The key concept of this division is to enforce order and easier understanding of the code. In the Component scope itself, there won’t be any reference to such segregation, there will only be this.props and this.methods.

Local

The local properties and methods of a component state are declared and handled ONLY by itself.

External

The external properties cannot be altered, they are read-only and are set at (or passed from) the parent Component.

Global

The declarations under the global segregation are references to data (properties) and modifiers (methods).

The Component View

Every component has its own view. Its role is simply to create the HTML representation of a given component

Declaration

The component view is declared as per as the component itself. It won’t be declared seperately.

import SepCon from 'sepcon';
SepCon.createComponent({
  id: 'myComponent'
}, {
  view: {
    events: [],
    lifecycle: {
      render() {}
    }
  }
});

Lifecycle

Render

In order to render a component, we will use the render lifecycle. This method must return a string in the end, that will be inserted into the component’s representing DOM element (which will be rendered itself by the component tag.

view: {
  render() {
    return '<div>Some HTML</div>';
  }
}

Events

To create interactions with the UI we will need use the events property. This is simply an array of objects that will represent the different events of a given component.

view: {
  events: [
    {
      event: 'click',
      selector: 'button',
      callback: 'handleClick'
    }
  ],
  handleClick(e) {
    //do stuff
  }
}

Component Tag

In order to create and initialize a new Component instance, we simply need to add its representing element into the DOM. But this might get tricky, if we would like to pass properties and methods to it. Theoretically speaking, if we would have insert a new html element directly to the DOM, it would have look something similar to this:

<x-sepcon-mycomponent></x-sepcon-mycomponent>

(And it would actually work)

But - if we would like to pass properties we would need to add some data attribute:

<x-sepcon-mycomponent data-properties="{prop1:'abc', prop2: 123}"></x-sepcon-mycomponent>

It will start to get a bit more difficult to maintain.

And if we would want to pass a reference to one of the component state’s properties, so that on changes it will automatically get updated, without re-rendering the parent component?

And if we would like to pass callbacks to that new component instance? How could we represent it in the HTML? Or even a less “extreme” case - if we would like to pass an object, we will need to parse it in such a way that the HTML won’t break.

So we have a unique method for this ability - the component tag object.

Declaration

We can get this by two ways.

  1. Getting a component tag object directly from SepCon:
    import SepCon from 'sepcon';
    const myComponentTag = SepCon.createTag('myComponent');
    
  2. Using the returned value of the createComponent method:
    import SepCon from 'sepcon';
    const myComponent = SepCon.createComponent({id: 'myComponent'}, {});
    const myComponentTag = myComponent.createTag();
    

Passing Data

The component tag object has a few methods in order to pass relevant data into the new component instance.

Rendering The Tag

After setting up the component tag with all of the relevant properties and methods - it’s time to render it’s representing HTML, in order to set parent component DOM element’s innerHTML.

We have 1 method for that - render, but it can be used in 2 different ways:

  1. Fully rendered DOM element:
    myComponentTag.render();
    

    Will return:

    <x-sepcon-mycomponent
      data-properties="..."
      data-methods="..."
      data-identifier="...">
        Passed HTML
    </x-sepcon-mycomponent>
    
  2. Partial rendered DOM element - open / close. This will be used in case we would like to pass the html manually, rather then using the html method of the component tag.

    Creating the opening tag:

    myComponentTag.render('open');
    

    Will return:

    <x-sepcon-mycomponent
      data-properties="..."
      data-methods="..."
      data-identifier="...">
    

    Creating the closing tag:

    myComponentTag.render('close');
    

    Will return:

    </x-sepcon-mycomponent>
    

Service

The service object is used in order to have a detached logic layer that will ideally deal purely with external resources of your app - e.g. AJAX calls, WebSockets, etc.

Declaration

Once declared - the service object exists at the SepCon ecosystem. No need to handle any returned value of the creation method createService. No need to create instances of that service object. Each method in the service definition will get resolve and reject methods as the first two arguments, all arguments that will be passed to that service’s method will be right after these two.

import SepCon from 'sepcon';
SepCon.createService({
  id: 'user'
}, {
  requests: {
    getUser(resolve, reject, params) {
      someAjaxCall('someUrl')
        .then(response => {
          resolve(response);
          this.channels.user(response);
        })
        .catch((error) => {
          reject(error);
        });
    }
  },
  channels: {
    user(data) {
      return data;
    }
  }
});

Usage

Requests

All service requests are, by definition, asynchronous and returns a promises. Therefore - the returned value of a given method will have the then method, which will get as arguments the “onResolve” and “onReject” handlers.

import SepCon from 'sepcon';
SepCon.service('user').requests.getUser(params)
  .then((user) => {
    console.log('user have been gotten', user);
  }, (error) => {
    console.error('error with getting the user');
  });

Channels

The channels allow [modifiers] to subscribe them, and once the channel is being invoked from within the service, all subscribers will get the returned value of that channel as an argument.

import SepCon from 'sepcon';
SepCon.service('user').channels.user('subscription_id', (user) => {
  console.log('this is the user', user);
});

So for instance, the requests usage above demonstrates an execution of a request, but if the dummy ajax would have been resolved - then also the user channel subscribers could have got that response as a “publish”.
Also important to notice that once a channel have been invoked - from then on all new subscribers will get a response once defining the handler, out of the runtime cache. So the last “publish” will simply repeat itself for new-comers.

Lifecycle

Caching

The service has a caching capability that is based only on configuration (though could be cleaned on demand).

SepCon.createService({
  id: 'user'
}, {
  cache: {
    requests: {
      getUser: {
        storage: false,
      }
    },
    channels: {
      user: {
        storage: 'local',
        duration: 15000
      }
    }
  },
  requests: {
    getUser(resolve, reject, params) {
      someAjaxCall('someUrl')
        .then(response => {
          resolve(response);
          this.channels.user(response);
        })
        .catch((error) => {
          reject(error);
        });
    }
  },
  channels: {
    user(data) {
      return data;
    }
  }
});

This setup means that the getUser will be cached only on runtime - meaning only for the current page lifecycle (e.g. refreshing the page - clears the cache). So when triggered, assuming that without arguments, or with previously-used arguments, the getUser request, for the second time, the method of the service won’t even run, instead the last value that was passed to the onResolve handler, will be the one that will be passed to the new onResolve handler again.

For the channels, in oppose to requests, there is no external way of passing arguments to the method. In channels it’s the service itself that will call it with any arguments at all. But again - the caching will rely on these arguments to determine whether to use the cache or actually run that user method.

Clear Cache

The only one who could clear a service cache is the service itself, and it would be by running the clearCache with the type of requests or channels, and the key. Optional parameter is sn array of arguments - if the cache clearance is meant for a specific result.

SepCon.createService({
  id: 'user'
}, {
  cache: {
    requests: {
      getUser: {
        storage: false,
      }
    },
    channels: {
      user: {
        storage: 'local',
        duration: 15000
      }
    }
  },
  requests: {
    getUser(resolve, reject, params) {
      someAjaxCall('someUrl')
        .then(response => {
          resolve(response);
          this.channels.user(response);
        })
        .catch((error) => {
          reject(error);
        });
    }
  },
  channels: {
    user(data) {
      return data;
    }
  },
  lifecycle: {
    pre: {
      request(name, args) {
        if(name === 'getUser' && !this.isNotAnActualFlag) {
          this.clearCache('requests', 'getUser');
        }
      }
    }
  }
});

Provider

The provider lets us segregate different services. For example - we have several services that are working against a given server, and a new server is being worked on, you could easily set a default provider or explicitly call a service of a specific provider. It also helps us organize the same logic in one place, e.g. - for sending/fetching data in the same specific structure for all different calls

Declaration

Once declared - the provider object exists at the SepCon ecosystem. No need to handle any returned val.

There is only the mount lifecycle natively supported on this object, other then that we can simply define methods and properties straight on the provider object. Services will have a reference to their provider under this.provider.

import SepCon from 'sepcon';

SepCon.createProvider({
  id: 'some-server'
}, {
  /***
  * if any authentication to the server needed
  * this could be a good place to initiate the process
  * you could also consider using 'pre:mount' to make sure it will take place on the current event loop
  ***/
  mount() {},

  /***
  * we might consider writing some generic methods for different actions against this particular provider (i.e. server)
  * this one is just an example!
  ***/
  getData(params) {
    params.url = 'my-cool-url';
    return new Promise(resolve, reject) {
      $.ajax(params)
        .done(res => {
          if(res.error) { reject(res.error); }
          else { resolve(res; }
        })
        .catch(err => {
          reject(err);
        });
    }
  }
});

SepCon.createService({
  id: 'data-fetcher',
  provider: 'some-server'
}, {
  requests: {
    getSomeInfo(resolve, reject, params) {
      this.provider.getData(params)
        .then(response => {
          resolve(response);
        })
        .catch((error) => {
          reject(error);
        });
    }
  }
});

SepCon.service('data-fetcher')
  .requests
  .getSomeInfo({param1: 'abc'})
    .then((res) => {
      //do stuff with the returned data
    }, (err) => {
      //error handling
    });

Lifecycle

Lifecycles

The goal behind the lifecycles can be splitted into three:

  1. Sync of two or more scopes and/or methods and thus creating a sequence.
    state: {
      lifecycle: {
     mount() {} //executed 1st
      }
    },
    view: {
      lifecycle: {
     render() {} //executed 2nd
      }
    }
    
  2. Supply the ability to have hooks for before a given execution (pre) and after (post) it, systematically:
    pre: {
      mount() {}
    },
    on: {
      mount() {}
    },
    post: {
      mount() {}
    }
    
  3. Supply the ability to “break the chain” - if at some point we wouldn’t want the lifecycle to continue, we could stop the sequence by returning a false value:
    state: {
      lifecycle: {
     pre: {
       mount() {} //executed 1st
     }
     on: {
       mount() { return false; } //executed 3rd
     }
      }
    },
    view: {
      lifeyclce: {
     pre: {
       render() {} //executed 2nd
     },
     on: {
       render() {} //won't be executed
     }
      }
    }
    

Shorthanding when setting the lifecycle hooks is possible - if you set the lifecycle methods straight into the lifecycle property - it will be treated as if you wrote them under on:

SepCon.createModifier({id: 'some-modifier'}, {
  lifecycle: {
    mount() {}, //exactly the same
    on: {
      mount() {} //exactly the same  
    }  
  }  
});

Changing The Lifecycles Configurations

We can add more lifecycles, as well as override existing ones, by accessing the SepCon’s setConfiguration method.

import SepCon from 'sepcon';
SepCon.setConfiguration({
  sequencer: {
    myNewMount: {}
  }
});

The Lifecycle Architecture

In order to gain full control we have a function to deal with the arguments that will be passed to each of the lifecycle’s steps, and a function that gets the returned value of each step.

myNewMount: {
  /**
  * the send function returns an array
  * that will be passed to the lifecycle's methods as arguments
  * this.base is the reference to the SepCon's relevant instance
  * @param step - current step { target, action }
  * @param hook - 'pre'/false/'post'
  * @returns []
  */
  send: function(step, hook) {
    return [...];
  },

  /**
  * gets the returned value of a lifecycle's method
  * @param step - current step { target, action }
  * @param hook - 'pre'/false/'post'
  * @param res - the returned value of the current lifecycle step
  */
  retrieve: function(step, hook, res) {},

  //the sequence array holds the steps to iterate over
  sequence: [
    {
      target: 'state',
      action: 'mount'
    },
    {
      target: 'component',
      action: 'render'
    },
  ]
}

Router

SepCon uses a pretty simple, straight-forward router, that is available for use within component states and modifier. They are the only privileged classes to use the SepCon’s router. In their instances’ scopes - you’ll have a אני י עדיrouter property under the this context:

state:{
  lifecycle: {
    change() {
      console.log(this.router);
    }
  }
}

Setting Routes

In oppose to the common use-case of frameworks’ routers, with SepCon it’s a bit different - both component states and modifiers have to set a router property in their declaration that will hold an array of different routes’ RegExps.

routes: [
  {
    match: /^\s*$/,
    handler: function() {
      console.log('This is the root page');
    }
  }
]

This pattern could be applied to both component states and modifiers, in exactly the same way. Once a route will match the RegExp supplied at the match property, the handler function will be executed. The context (this) will be of the instance.

In a component state it will look something similar to this:

{
  state: {
    lifecycles: {}
    routes: []
  }
}

In a modifier it will look something similar to this:

{
  lifecycles: {}
  routes: []
}

Methods

The router has (currently) two main methods:

Changing The Router Configuration

We can change (currently) only the routing mode (‘history’ (default) or ‘hash’), and the root relative URL. This is feasible via SepCon’s setConfiguration method.

import SepCon from 'sepcon';
SepCon.setConfiguration({
  router: {
    mode: 'hash',
    root: '/home'
  }
});