Probably not the last JS framework you'll live to see
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.
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',
//...
});
mount
- Executed once, upon creationThe modifier is what we need in order to add, edit and remove values of data objects.
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() {}
}
});
mount
- Executed once, upon creationThe 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
}
});
x-sepcon-mycomponent
render
lifecycle that’s triggered when the component is mounted, and whenever a change of the component state occursEvery component has its own state. You cannot share this state with other components, even of the same definition.
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) {}
}
}
});
mount
- Executed once - when a new component is added to the DOM for the first timeattach
- Executed on every time the component is added to the DOM (whether it was a result of re-rendering a parent component, by changing pages on a single-page-application, etc)change
- Executed on every state property change, whether it’s a local, external nor global property.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
}
}
null
.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
.
The local properties and methods of a component state are declared and handled ONLY by itself.
setProps
method.
state: {
props: {
local: {
counter: 0
}
}
}
counter
.state: {
methods: {
local: {
increaseCounter(next) {
this.setProps({
counter: this.props.local.counter++
});
next();
}
}
}
}
increaseCounter
will increase the local property named counter
by 1, every time it will get executed. We’ll use the setProps
method of the component state in order to alter state’s local properties.next
method - it’s always get passed as the first argument at each local method. In our component we have the abillity to control the methods’ flow, we will elaborate on this later on.The external properties cannot be altered, they are read-only and are set at (or passed from) the parent Component.
next
argument.The declarations under the global segregation are references to data (properties) and modifiers (methods).
Properties The global properties are references to data properties, and are also read-only like externals, but on referenced data properties’ changes - they will be changed automatically.
state: {
props: {
global: {
userName: {
data: 'user',
key: 'name'
}
}
}
}
userName
is derived from a data object with the ID: user
, under the property name
.
We could also subscribe to data properties on run time by using the setGlobalProps
method:
state: {
methods: {
local: {
changeGlobalProps(newKey) {
this.setGlobalProps({
myGlobalProp: {
data: 'some-data-object',
key: newKey
}
});
}
}
}
}
global
definitionsmyGlobalProp
null
:
this.setGlobalProps({
myGlobalProp: null
});
Methods Global methods are references to modifier methods.
state: {
methods: {
global: {
changeUserName: {
modifier: 'user',
key: 'setName',
pass(newName) {
newName = 'Mr. ' + newName;
return [newName];
}
}
}
}
}
changeUserName
points to a method named setName
located at a modifier named user
.pass
that enables to pass arguments to the modifier itself. If the arguments are expected to be passed directly from the component view without any special formatting or handling, there is no need to define a pass
function.Every component has its own view. Its role is simply to create the HTML representation of a given component
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() {}
}
}
});
render
- Executed with the component state’s mount
and change
Lifecycles. The value that will be returned should be a string that represents html. The returned value will be inserted to the component’s representing DOM element.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>';
}
}
mount
, resume
or change
lifecycles.mount
or resume
lifecycles - then the value of this arguent will be true
, otherwise it will get the changed objectTo 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
}
}
event
- the event type - click
, keyup
, etc.selector
- the element selector to bind this event to.callback
- the name of a function in the component view’s scope to invoke as the handler. This method will get the Event object.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.
We can get this by two ways.
import SepCon from 'sepcon';
const myComponentTag = SepCon.createTag('myComponent');
createComponent
method:
import SepCon from 'sepcon';
const myComponent = SepCon.createComponent({id: 'myComponent'}, {});
const myComponentTag = myComponent.createTag();
The component tag object has a few methods in order to pass relevant data into the new component instance.
props
, methods
These are premitives or objects as one through props
, and functions through methods
.
The only thing to remember is that these values will be completely dettached of any original logic (wether it’s a local/global property/method of the parent component state).
myComponentTag
.props({
prop1: 'abc'
})
.methods({
method1: function() {}
});
refProps
, refMethods
These are directly derived of the parent component state.
In other words - we will supply a key, and the child’s component state will be bind to the original property of the parent (if passed through refProps
), or be able to execute the passed method throughout all of its segregations - Local > External > Global (if passed through refMethods
).
myComponentTag
.refProps({
prop2: 'propFromState'
})
.refMethods({
method2: 'methodFromState'
});
myComponentTag.html('<div>Hello World</div>');
This html will be available only to the component itself via the html
property, to get the string itself, nor via the children
property which holds an array of DOM elements.
render() {
return `<div>
${this.html}
<hr />
${this.children.length
? this.children.map(el => el.outerHTML).join('')
: ''}
</div>`;
}
id
method, to pass an identifier to distinguish from each other.
If we won’t use an id
- performance might get affected (harder coupling between an existing component instance to a re-rendered DOM element).
let componentTags = [];
for(let key in this.props.someArray) {
const myComponentTag = myComponent.createTag();
myComponentTag.id(key);
componentTags.push(myComponentTag);
}
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:
myComponentTag.render();
Will return:
<x-sepcon-mycomponent
data-properties="..."
data-methods="..."
data-identifier="...">
Passed HTML
</x-sepcon-mycomponent>
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>
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.
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;
}
}
});
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');
});
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.
mount
- Executed once, upon creationrequest
- Executed whenever someone makes a request
. Gets the request name as first argument, and an array of arguments as the second one (empty array if no arguments);channel
- Executed whenever a service executes a channel
. Gets the request name as first argument, and an array of arguments as the second one (empty array if no arguments);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.
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');
}
}
}
}
});
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
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
});
params
that will have both param1: 'abc'
and url: 'my-cool-url
.getData
method, then it will go through the service’s getSomeInfo
method, and then it will be returned to the Promise’s then
method from the initial executer (in this case it was directly from the window, but most probably will be handled via a modifier.mount
- Executed once, upon creationThe goal behind the lifecycles can be splitted into three:
state: {
lifecycle: {
mount() {} //executed 1st
}
},
view: {
lifecycle: {
render() {} //executed 2nd
}
}
pre
) and after (post
) it, systematically:
pre: {
mount() {}
},
on: {
mount() {}
},
post: {
mount() {}
}
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
}
}
});
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: {}
}
});
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'
},
]
}
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);
}
}
}
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: []
}
The router has (currently) two main methods:
pushState
. Therefore it’s recommended to use the router’s navigate
method.
this.router.navigate(url);
window.location
object, we can simply use the getFragment
method
let url = this.router.getFragment();
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'
}
});