This is a specification for non-existent UI Framework called Glassjs. Just an attempt to define a good API that customer would need to interact with. We (and/or somebody else) may implement this spec.
- Zero or near to zero boilerplate code
- Want to build a framework which is dynamic. Dynamic as in, if you have a big app, you should be able to release just a component without having to redeploy whole app.
- Very minimal framework related references in User code base. If there has to be, it will be agnostic to this framework name so if somebody else implements with better one, you should be able to run same code there.
- Huge emphasis on conventions over configuration (Can we achive zero configuration?)
- Should include all features that a modern UI app would need
- Loose coupling between discrete components and if it has to, communicate using events
- First class Server side generation support
- Pure framework without any any custom UI components (like no Tree widget or others) but we may build such resharable based on material design
- Moduralized CSS
- Supports only Evergreen browsers (chrome, firefox, edge and safari)
- Conforming to web-components. I feel that to achieve (almost) zero configuration and boilerplate, it has to deviate from that.
UI development technologies and how folks view UI development has evolved a lot since last few years, thanks to many improvements to language and also many UI frameworks. So this framework is no exception. This would not have become except for many frameworks that have come before.
This specification and syntax is inspired by
- Markojs
- Aurelia
- Aura (Salesforce)
- Vuew
- React
- And many others
Credit all the contributors of those libraries for doing an incredible job.
Component
is ANY reusable bits of an UI Application.
Each component is a file with extn .cmp
and stored under its own folder with same name as component file name without .cmp
extension, within the namespace folder.
A Component is identified by a name which also dictates where it is stored in the file system. Each component is of a type
and type
defines what kind of functionality that component exhibits.
Here are the types:
view
(default)app
event
interface
lib
- ...
Depending on the type, component support various types of blocks of code. Each block is associated with semantic meaning and syntactic language.
Here are the various blocks of code.
markup
(required)script
style
doc
- ...
Each block of code is stored with pair of start and end tags with their name. For ex.,
<markup>
... markup
</markup>
<script>
... script code
</script>
<style>
... style code
<style>
Component name must follow localCamelCase
format with following rules.
- Must being with letter
- and followed by alphanumeric chars
- Must be unique in a namespae
Namespace is a string that virtually groups set of components. Components are ALWAYS be part of one (only one) namespace. Components with same namespace may have different privileges.
Component is always referred in the format namespace:componentName
.
Each Javascript package can consists of multiple namespaces and each namespace consists of multiple components.
File structure of typical project using this framework is as shown below.
$ tree
.
├── LICENSE
├── README.md
├── package.json
└── src
└── ui
├── main
│ ├── main
│ │ └── main.cmp
├── ns1
│ ├── comp1
│ │ └── comp1.cmp
│ └── comp2
│ └── comp2.cmp
└── ns2
├── comp1
│ └── comp1.cmp
└── comp2
└── comp2.cmp
src/ui
is base path (which could be anything)- Under
src/ui
each folder indicates it is a namespace. In this examplens1
andns2
are namespaces - Under each namespace there will one folder for each component named after component name itself
- Within the component folder, there will be a file with extension
.cmp
which is main component code. - There could be multiple other files within each component folder, which are referred within the each component code.
App component is main entry point into UI application, similar to main methods in programming languages. App component is the first page that loads from the server and initializes the initial document. All other interactions within this document context.
No other components can include app component as child include another app component.
App component is compiled differently that depending on the compiler, it may be compiled into bundle with all of its non-conditional components are compiled into to reduce the page load times. Where as other components, usually compiled individually.
App components are also gets new additional life cycle methods compared to view components.
Here is most simple UI application one can define.
src/ui/main/main.cmp
<markup type="app">
Hello World
</markup>
Since type
is default attribute, you can specify same code using short form as below.
<markup=app>
Hello World
</markup>
Component with type="view"
are called view components. View Component is used to build any visible part of the application (dom in browser and string render in server).
Here is a most basic view component you can define.
<markup>
Hello World
</markup>
When rendered this should show a text node in the UI with Hello World
.
Description of the component can be specified using desc
attribute in markup
tag as follows.
<markup desc="Just displays static 'Hello World'"/>
Hello World
</markup>
If you have larger block of code, you can include desc
tag within the component
<markup/>
<desc>
This is description block which explains what this component does.
</desc>
Hello World
</markup>
If both are specified, then expanded text takes precedence. There must be only one desc
tag per component (there could be one for attr but that is at attribute level. See below). In these cases, compiler will warn the ambuguity.
Component can include all of allowed html tags. Some tags are not allowed due to security or deprecated reasons. List of tags allowed is at the end of this document.
<markup>
<div>
<header>Today's Headlines</header>
<div class="body">
<ul>
<li>Company acquired</li>
<li>500m accounts breached</li>
</ul>
</div>
</div>
</markup>
Html comments can be specified as usual and they will be rendered as specified.
<markup>
<!-- This is Header -->
<div>Header</div>
<!-- This is Body -->
<div>Body</div>
</markup>
Sometimes framework may use an attribute which is also a html attribute. In that case, you can prefix with html-
which will be passed down to html markup as is.
<markup>
<div html-for="firstName"/>
</markup>
Data attributes can be specified as usual using data-
prefix.
<markup>
<div data-firstName="John" data-lastName="Doe"/>
</markup>
Markup can contain multiple root elements
<markup>
<span class="first">John</span>
<span class="last">Doe</span>
</markup>
Each component can define zero or more attributes which becomes the API for external parties to consume this component.
Each attribute is defined within the markup
block as below.
<markup>
<attr name="name" type="string" desc="The name to be displayed" default="World"/>
</markup>
where
name
is name of the component. Name should follow javascript variable convention at least 2 chars. Some names are reserved (state
,s
,c
)type
is type of the attributes. Valid values arestring
,number
,boolean
,object
desc
is description of the attributedefault
default value of the attribute. It is set to this, if one is not specified by the consumer of this component.
Since only name
is required for an attribute and type defaults to string
, you can write above component as
<markup>
<attr name="name"/>
</markup>
If you have loger description for this attribute, it can be specified using attr
tag in the attribute.
<markup>
<attr name="name">
<desc>
This is longer version of the description
</desc>
</attr>
</markup>
Each view component inherits a default set of attributes which are as follows. Since they are inherited by default, those cannot be redefined again. If defined, warning will be logged and ignored.
<attr name="class" type="string" desc="String or object representing list of css classes" default=""/>
<attr name="version" type="string" desc="Indicates the version of the component that indicates what behavior this component should exhibit, if it supports multiple" default="1.0"/>
<attr name="body" type="Component[]" desc="The body of this component, that is everything between start and end tags"/>
Each defined attributes can be used in the markup using ${attribute-name}
pattern as below.
<markup>
<attr name="name"/>
Hello ${name}
</markup>
Within ${
and }
you can use javascript variables and expressions. Not all javascript is supported but simple expressions to simplify rendering the values.
<markup>
<attr name="firstName"/>
<attr name="lastName"/>
Hello ${firstName + ' ' + lastName}
</markup>
If you have a text that you are sure safe, you can set text as is to innerHTMl
using $!{
and }
<markup>
<attr name="productDocs"/>
Product: $!{productDocs}
</markup>
Styles can be specified inside style
tag. Supported language is scss.
<style>
.header {
font-size: 1.2em;
}
.body {
font-size: 1em;
}
</style>
<markup>
<div class="header">This is the header</div>
<div class="body">This is body</div>
</markup>
By default all css classes are scoped and applicable only with that component and containment. If you would like a component styles are to be unscoped, add unscoped
to style tag.
<style unscoped>
.header {
font-size: 1.2em;
}
.body {
font-size: 1em;
}
</style>
<markup>
<div class="header">This is the header</div>
<div class="body">This is body</div>
</markup>
If you would like to conditionally render a component use if
attribute. Note when expression is falsy, the component is removed from the DOM.
<markup>
<attr name="showBody" type="boolean"/>
<div if="showBody">This is body</div>
</markup>
You can add zero or more elseif
conditioned sublings and zero or one else
tag as below.
<markup>
<attr name="showLevel" type="number"/>
<div if="showLevel === 1">This is body Level1</div>
<div elseif="showLevel === 2">This is body Level2</div>
<div else>This is body Level3-n</div>
</markup>
If you have multiple tags to be rendered for each of conditional, you can use expanded tags version.
<markup>
<attr name="showLevel" type="number"/>
<if="showLevel === 1">
<div>This is body Level1</div>
<div>This is body Level1</div>
</if>
<elseif="showLevel === 2">
<div >This is body Level2</div>
<div >This is body Level2</div>
</elseif>
<else>
<div>This is body Level3-n</div>
<div>This is body Level3-n</div>
</else>
</markup>
If you want to keep the component in the dom but just want to hide it, you can use display attribute. This basically add or removes display: none
style property.
<markup>
<attr name="showLevel" type="number"/>
<div display="showLevel == 1">This is body Level1</div>
<div display="showLevel == 2">This is body Level2</div>
<div display="showLevel > 2">This is body Level3-n</div>
</markup>
If you have an Iterable
value, you can iterate over values and use it to render values.
<markup>
<attr name="colors" type="string[]" default="['red', 'blue']">
<ul>
<li for="color of colors">${color}</li>
</ul>
</markup>
If you have multiple tags to process for each iteration, then you can use expanded form.
<markup>
<attr name="colors" type="string[]" default="['red', 'blue']">
<ul>
<for="color of colors">
<li>${color}</li>
</for>
</ul>
</markup>
If value specified for iteration is not an Iterable
then array will be created with specified single value.
<markup>
<attr name="color" type="string" default="red">
<ul>
<for="color of colors">
<li>${color}</li>
</for>
</ul>
</markup>
In addition to creating requested iteration variable (in this case color
), it also creates special variable called $value
so you can omit your variable and use it.
<markup>
<attr name="colors" type="string[]" default="['red', 'blue']">
<ul>
<for="colors">
<li>${$value}</li>
</for>
</ul>
</markup>
Sometimes you may want to access index of iteration or check if you are at last, first or odd or evan. To access those state, you can use special variable $loop
.
<markup>
<attr name="colors" type="string[]" default="['red', 'blue']">
<ul>
<for="colors">
<li>Index: ${$loop.length}</li>
<li>Index: ${$loop.index}</li>
<li>First: ${$loop.first}</li>
<li>Last: ${$loop.last}</li>
<li>Odd: ${$loop.odd}</li>
<li>Even: ${$loop.even}</li>
<!-- $loop.value will be same as $value -->
<li>Loop Value: ${$loop.value}</li>
<li>Value: ${$value}</li>
</for>
</ul>
</markup>
Component can be referred in another component by specifying the namespace and component name.
ui:displayName
<markup>
<attr = name/>
Hello ${name}
</markup>
ui:names
<markup>
<ui:displayName name="John"/>
<ui:displayName name="Emily"/>
<ui:displayName name="Kashyap"/>
</markup>
Slots help you project content from child component into parent component. Parent component defines the slots and their names. Child compoent can project content into those slots.
display.cmp
<markup>
<slot name="header"/>
this is body
<slot name="footer"/>
</markup>
main.cmp
<markup>
<display>
<div slot="header">
this is header
</div>
<div slot="footer">
this is footer
</div>
</display>
</markup>
Results in following dom.
<div>
this is header
</div>
this is body
<div>
this is footer
</div>
When a component defines a slot, it can provide a default content. That default content will be used if there is no content is projected.
display.cmp
<markup>
<slot name="header">
this is default header
</slot>
this is body
<slot name="footer"/>
</markup>
main.cmp
<markup>
<display>
<div slot="footer">
this is footer
</div>
</display>
</markup>
Results in following dom.
<div>
this is default header
</div>
this is body
<div>
this is footer
</div>
Routing can be configured using route
tag.
<markup type="apo">
<route path="/about">
<ui:about/>
</route>
<route path="/history">
<ui:history/>
</route>
</markup>
Since path
is default attribute, it can be written using short form.
<markup=app>
<route="/about">
<ui:about/>
</route>
<route="/history">
<ui:history/>
</route>
</markup>
Path can define set of path variables which are available via $route.params
object, which can be passed to downstream components.
<markup type="app">
<route path="/users/:userId">
<ui:userDisplay userId="$route.params.userId"/>
</route>
</markup>
Rotues can be nested within other routes. Child routes are relatively place within the parent components's route context.
<markup type="app">
<route path="/users/:userId">
<ui:userDisplay userId="$route.params.userId"/>
</route>
</markup>
ui:userDisplay
<markup>
<attr=userId/>
User Id is ${userId}
<route path="/profile">
<ui:userDetails userId="${userId}"/>
</route>
<route path="/edit">
<ui:userDetails userId="${userId}"/>
</route>
</markup>
Continueing above example, let's say other component wants to link to navigate to those routes, it can use link
tag. Note that link tag always link based on relative paths, which is relative to parent route's context. This is to ensure that component can route even when it is used in different parent contexts.
<markup>
<attr=userId/>
User Id is ${userId}
<link to="/profile">Goto Profile</link>
<link to="/edit">Goto Edit</link>
<route path="/profile">
<ui:userDetails userId="${userId}"/>
</route>
<route path="/edit">
<ui:userDetails userId="${userId}"/>
</route>
</markup>
Since to
is default attribute link declaration can be shortned to
<link="/profile">Goto Profile</link>
Lets say that you are developing a user display component which can be used by two different application. However each application is displaying this component under different paths. So when user component wants to link between profile and edit, it cannot hardcode path from beginning as that might change. Instead component can make use of rpath
attribute which automatically prefixes the parent's route path which would be set to different values depending where userDisplay
component is used.
ui:userDisplay
<markup>
<attr=userId/>
User Id is ${userId}
<route path="/profile">
<ui:userDetails userId="${userId}"/>
</route>
<route path="/edit">
<ui:userDetails userId="${userId}"/>
</route>
</markup>
ui:app1
<markup>
<route path="/app1/users/:userId">
<ui:userDisplay userId="$route.params.userId"/>
</route>
</markup>
ui:app2
<markup>
<route path="/app2/users/:userId">
<ui:userDisplay userId="$route.params.userId"/>
</route>
</markup>
Each component can have one script with one or more ES6 classes. One of such classes can be named Controller
, which acts as controller for this component. Controller can receive life cycle callbacks, interact with server, update view, fire events etc.,
Controller can define instance variables (called state) and methods. Variables can be accessed in component using c.
(c stands for controller) reference.
<script>
class Controller {
constructor(cmp, ctx) {
this.name = 'John Doe';
}
}
</script>
<markup>
Hello ${c.name}
</markup>
The reason all controller variable access needs to be prefixed with c
is to avoid any name collissions if component defines an attribute with same name.
<script>
class Controller {
constructor(cmp, ctx) {
this.name = 'John Doe';
}
}
</script>
<markup>
<attr name="name"/>
Hello ${name} and ${c.name}
</markup>
Each Controller
can have a contructor which receives two parameters cmp
and ctx
.
First parameter cmp
is an object that represents the component instance. This object would give access information about the current instance of the component. For ex., you can call cmp.metadata
which gives access to component Metadata, which allows you to query component name, its defined attributes, access to component instance attribute values etc., as we will explore more about it later.
Second parameter ctx
is an context object from the framework. Context object would provide all needed properties and methods needed to interact with framework. This would be the only connectivity between component and framework runtime.
You can define methods which can be bound to one or more events. Events follow React
event convention of onEventName
format. For ex., onClick
or onMouseOver
etc.,
In this example, we are incrementing a state variable on clicking a button which is rendered in the view.
<script>
class Controller {
constructor(cmp, ctx) {
this.count = 0;
}
increment() {
this.count++;
}
}
</script>
<markup>
Current Count is ${c.count}
<button onClick={increment()}>Click Me!</button>
</markup>
Credit: Example is from Markojs documentation
You can pass one or more parameters to controller function.
<script>
class Controller {
constructor(cmp, ctx) {
this.count = 0;
}
increment(incrBy) {
this.count += incrBy;
}
}
</script>
<markup>
Current Count is ${c.count}
<button onClick="increment(1)">Click Me!</button>
<button onClick="increment(10)">Click Me to Speed up!</button>
</markup>