A reasonable micro-app framework with practical reuseability.
El.js is a framework built ontop of Riot.js templates for building micro-apps.
Web-frameworks require developers to build most if not all their webpages to be dynamic webapps. This allows developers to make sure everything on their webpage obeys a single consistent, predictable, rendering flow that they can reason about. However, there are also many disadvantages compared to traditional static websites including more complex caching schemes, longer load-times, and SEO problems. Microapps offer a powerful solution for these drawbacks. Instead of building giant monolithic web applications, build small apps and embed them on your otherwise static pages.
A micro-app performs a small and very tightly scoped piece of functionality that can be reused over and over again. Micro-apps don't differ much from the idea of embeddable widgets before frameworks became the standard, but they differ in execution by emphasizing a reliance on reasonable frameworks and practical reuseability.
HTML: index.html
<html>
<head>
<!-- Head Content -->
<link rel="stylesheet" src="https://cdn.jsdelivr.net/gh/hanzo-io/el-controls/theme.css" />
</head>
<body>
<my-form>
<div>
<label>Type Your Name</label>
<!-- bind my-input to parent(my-form).data.name, parent.data is implicit for what is supplied to bind attribute -->
<my-input bind='name' />
</div>
<div>
<span>Your Name Is</span>
<span>{ data.name }</span>
</div
</my-form>
<!-- El.js Library -->
<script src="https://cdn.jsdelivr.net/gh/hanzo-io/el.js/el.min.js"></script>
<script src="my-script.js"></script>
</body>
</html>
JS: my-script.js
// window.El is the global value
// El.Form extends El.View and validates bound El.Inputs
class Form extends El.Form {
constuctor () {
// data contains your state
this.data = {
name: '?',
}
// your custom tag name
this.tag = 'my-form'
super()
}
}
Form.register()
// El.Input extends El.View and binds to updating El.Form values
class Input extends El.Input {
constructor () {
// your custom tag name
this.tag = 'my-input'
// the default this.change function works with all basic html inputs(<input>, <textarea>, ...).
this.html = '<input onkeydown="{ change }" />'
super()
}
}
Input.register()
El.mount('*')
Add this tag to the bottom of before your custom scripts and deps and reference window.El.
<script src="https://cdn.jsdelivr.net/gh/hanzo-io/el.js/el.min.js"></script>
Install via NPM
npm install el.js --save
Supports CommonJS
var El = require 'el.js'
or ES6 imports
import El from 'el.js'
This type is referenced by El.Form to store the information used to validate the field associated with name.
Name | Type | Default | Description |
---|---|---|---|
config | MiddlewareFunction or [MiddlewareFunction] | undefined | This type stores the original MiddlewareFunction or MiddlewareFunctions used to create validate() |
name | string | '' | This is the name of a field on El.Form's data property that the rest of this type references. |
ref | Referrential Tree | undefined | This is a link to the mutable data tree which can retrieve the value of name by calling this.ref.get(name) |
Name | Type | Description |
---|---|---|
validate | (Referrential Tree, string) => Promise | This method calls all the MiddlwareFunctions in serial using promises. |
- type: (value: any) => Promise or any
This type is used for defining middleware for El.Form. Do validation and input sanitization with these functions such as:
function isRequired(value) {
value = value.trim()
if (value && value != '') {
return value
}
throw new Error('Required')
}
- type: { p: Promise }
This type is used internally in places to facilitate returning promises by reference.
This is the base class for all El classes. Each El.View corresponds with a custom tag. Extend this class to make your own custom tags.
Name | Type | Default | Description |
---|---|---|---|
css | string | '' | This is a string representing the tag's css. It is injected once per class at the bottom of the tag when mounted. |
data | Referrential Tree | undefined | This property stores the state of the tag. |
html | string | '' | This is a string representing the tag's inner html. |
root | HTMLElement | undefined | This property stores a reference to the tag in your webpage that the mounted view is bound to. |
tag | string | '' | This is the custom tag name. |
Name | Type | Description |
---|---|---|
beforeInit | () => | The code here executes before the tag is initialized. |
init | () => | The code here executes when tag is initialized but before its mounted. Recommended - If you need to bind to the tag's lifecycle, do it here. |
scheduleUpdate | () => Promise | This method schedules an asynchronous update call. It batches update calls at the top-most view if there are nested views. It returns a promise for when the update executes |
update | () => | This method updates the tag. This is called implicitly after events triggered from webpage. See onkeydown in A 'Simple Form Example' for such a case. Manually call this method to update the tag. Recommended - It is recommended to manually call scheduleUpdate() instead to prevent synchronous update cascades. |
Each El.View is an event emitter. See riot.observable for further documentation, http://riotjs.com/api/observable/
Name | Type | Description |
---|---|---|
El.View.register | () => | This registers the current custom tag with the rendering engine. Call it after you defined a tag |
This class is used to represent forms as well as more complex IO driven micro-apps. This class supplies common form validation and form submit logic.
Name | Type | Default | Description |
---|---|---|---|
configs | Object | undefined | Supply a map of names to a MiddlewareFunction or array of MiddlewareFunctions. See MiddlewareFunction for more information. |
inputs | Object | null | Each element in configs is converted to an element in inputs. Modifying this directly is not recommended. |
Name | Type | Description |
---|---|---|
init | () => | Code here executes when tag is initialized but before its mounted. Calls initInputs() so manually call that - or call super() in ES6. Recommended - If you need to bind to the tag's lifecycle, do it here. |
initInputs | () => | Compile configs and assign the emitted struct to inputs. inputs like configs contain references to the named field in data. |
submit | (Event) => Promise | This method triggers validation for each field in data as defined in configs. This method should be called as an event handler/listener. It calls submit() if validation is successful, returns a promise for when validation succeeds/fails |
_submit | () => | Code here executes when the form is validated during submit() call |
This is the base class for building form inputs and IO controls.
Name | Type | Default | Description |
---|---|---|---|
bind | string | '' | This property determines which field in the parent form's data this binds to. |
lookup | string | '' | Same as bind, deprecated. |
errorMessage | string | '' | This property is set to the first error message that this.input.validate's returned promise catches. |
input | InputType | null | This property is taken from the parent form's inputs property based on what parent data's field bind specifies. |
valid | bool | false | This property is used to determine the validation state the input is in. It is set when this.input.validate is called, it is only ever set to true if this.input.validate's returned promise executes completely. |
Name | Type | Description |
---|---|---|
change | (Event) => | This method updates the input and then validates it. This method should be called by an event handler/listener. |
changed | () => | This method is called when this.input.validate's returned promise executes completely. |
clearError | () => | This method sets errorMessage to '' and is called before validation. |
error | (Error) => | This method sets errorMessage and is called when validation fails. |
getValue | (Event) => any | This method gets the value from the input. By default, this method returns the Event's target.value. |
validate | (PromiseReference?) => Promise | This method validates the input, it returns a Promise representing validation success and fail both by reference (needed internally) and by value. |
Name | Type | Description |
---|---|---|
El.scheduleUpdate | () => | Schedule update for all micro-apps on the page. |
El.js's life cycle functions are inherited from Riot.js.
El.js uses Referrential Trees to store its form data.
Implement the get, set, on, once, off methods from referrential around your own datastructure and drop it in as the data property.
A container is a custom tag that provides methods to use for its internal template and whose content can be overwritten entirely (only contains content in one or more tags). A control is a component which interacts with the user for the purposes of displaying information in an interesting way or getting input such as an input, select, or a GoogleMaps embed.
Instead of building widgets in a tightly coupled fashion, decompose the widget into a containers and controls to maximize reuseability. Structure the internal html in whatever way makes the most sense. Then, release your completed widget, container, and controls to your users so they can customize the widget for their various requirements.
By abstracting your ui elements like this, it is much easier for someone else to reuse and customize your code. See shop.js for an implementation.
It is best to use a single high level state store to simplify saving and restoring state for your webpage or entire website.
This can be acomplished by supplying all top level containers on the page the same data field. via the initial mount call
var data = {
state0: 0,
state1: 1,
}
El.mount('*', { data: data })
Unlike normal Riot rendering, El.js allows the implicit accessing of values on this.parent and this.parent...parent via prototypical inheritence of the rendering context. This is done to avoid repeatedly passing the same data down through nested containers because it is error prone and overly verbose. This also makes it easier to build containers and controls.
Explicitly passing the data variable:
<my-container-1>
<my-container-2 data='{ data }'>
<my-container-3 data='{ data }'>
value: { data.value1 }
</my-container-3>
<my-container-3 data='{ data }'>
value: { data.value2 }
</my-container-3>
</my-container-2>
<my-container-2 data='{ data }'>
<my-container-3 data='{ data }'>
value: { data.value3 }
</my-container-3>
<my-container-3 data='{ data }'>
value: { data.value4 }
</my-container-3>
</my-container-2>
</my-container-1>
// El.mount passes data to the top level container of each micro-app
El.mount('*', data: { value1: 1, value2: 2, value3: 3, value4: 4 } )
Is equivalent to implicitly referencing the data variable.
<my-container-1>
<my-container-2>
<my-container-3>
value: { data.value1 }
</my-container-3>
<my-container-3>
value: { data.value2 }
</my-container-3>
</my-container-2>
<my-container-2>
<my-container-3>
value: { data.value3 }
</my-container-3>
<my-container-3>
value: { data.value4 }
</my-container-3>
</my-container-2>
</my-container-1>
// El.mount passes data to the top level container of each micro-app
El.mount('*', data: { value1: 1, value2: 2, value3: 3, value4: 4 } )