somatic
Create composable (x)html components with pure javascript functions
Description
-
Composable components can be generated by calling createComponent() with a template function argument and an options object
-
Template functions passed to createComponent return a markup string that can make use of other component in the form of cutom tags. ES6 template strings are especially useful for defining these template functions
-
Lower-case HTML tags are recognized automatically; other tags are resolved using options passed to the createComponent() function
-
Component attributes which are not primitive can be specified in encoded form (JSON stringified then base64 encoded); template functions passed to createComponent() can access these attributes, already decoded, in the argument
API
/**
* Main API function; returns pure js composable html
* component wrapper function based on a markup renderer function.
* @param baseRenderer: specifies the function that returns the raw markup for the component.
* @param options: options used to pre-configure wrapper function
*/
createComponent<T>(baseRenderer: Renderer<T>, options: Partial<ComponentOptions<T>>): Component<T>;
/**
* create module with default options applied when options not supplied or fails
*/
setOptions(defaultOptions: Partial<Options>): Module
/**
* resolve the component <name> argument to a component function,
* using the supplied resolution <options> argument
*/
resolveComponent(name: string, options?: Options): AnyRenderer;
/**
* Converts ast to markup consisting solely of primitive components (html elements)
*/
evalAst(ast: AbstractSyntaxTree, options?: Options): string;
/**
* generate JSON AST from xml markup
*/
generateAST(markup: string): AbstractSyntaxTree;
/**
* Helper method to stringify object argument passed to template function
*/
stringifyProps<T>(props: T, encode?: boolean): string;
/**
* Helper method to stringify data-{x} arguments passed to template function
*/
stringifyDataProps(props: AnyObject): string;
/**
* Helper method to stringify CSS object or passthrough CSS string
*/
stringifyCSS(style: CssStyle | string): string;
/**
* Parse CSS string into JSON-style object
*/
parseCSS(style: string): CssStyle;
/**
* Generate HTMl markup
*/
generateHTML(tag: string, props?: ObjectDictionary<string>, children?: any): string;
/**
* Formats html
*/
formatHTML(htmlOrAst: string | AbstractSyntaxTree, options?: AnyObject): string;
Types
interface AbstractSyntaxTree {
name: string,
type: string,
value: string,
children: any[],
attributes: AnyObject
}
interface Options {
readonly failAsDiv: boolean, // render tags that cannot be resolved as html divs?
readonly resolution: Partial<{
readonly dict: ObjectDictionary<AnyComponent> // resolution dictionary (first priority)
readonly func: (string) => AnyComponent // resolution function (second priority)
}>
readonly formatMarkup: boolean // format the output as xml/html?
readonly debugMode: boolean
}
interface ComponentOptions<T> extends Options {
readonly name: string, // component name, for documentation and debugging purposes
readonly defaultProps: Partial<T>,
readonly isContainer: boolean // whether component have can children elements
}
type Renderer<T> = (props?: T, children?: any) => string;
type AnyRenderer = Renderer<any>;
type Component<T> = Renderer<T> & {
readonly componentOptions: ComponentOptions<T>, // for documentation and inspection purposes
setOptions: (opts: ComponentOptions<T>) => Component<T> // override initial options
}
type AnyComponent = Component<any>;
interface ObjectDictionary<T> { [key: string]: T}
type AnyObject = ObjectDictionary<any>;
Example
Note that we are able to compose components from both base html elements and other custom higher-order components. Any component references in the markup are resolved using the resolution member of the options passed to the createComponent function.
let components = () => _components;
const somatic: Module = (require("./index") as Module).setOptions({
resolution: {
func: (name) => _components[name]
},
debugMode: false
})
const _ = somatic.stringifyProps;
let _components = {
Banner: somatic.createComponent(function (props: { logo, title }, children) {
return `
<div style="display: flex; justify-content: flex-start">
<img src="${props.logo}"/>
<span>${props.title}</span>
</div>`;
}, { name: "Banner", resolution: { func: (ref) => components()[ref] } }),
StackPanelHorizontal: somatic.createComponent(function (props, children) {
return `
<div style="display: flex; justify-content: flex-start">
${children.map(child => `<div>${child}</div>`).join("")}
</div>`;
}, { name: "StackPanelHorizontal", resolution: { func: (ref) => components()[ref] } }),
StackPanelVertical: somatic.createComponent(function (props, children) {
var stringifiedProps = _({
style: {
display: "flex",
flexDirection: "column",
justifyContent: "flex-start"
}
}, false);
return `
<div ${stringifiedProps}>
${children.map(child => `<div>${child}</div>`).join("")
}
</div>`;
}, { name: "StackPanelVertical", resolution: { func: (ref) => components()[ref] } }),
Footer: somatic.createComponent(function (props, children) {
return `
<StackPanelHorizontal>
<StackPanelVertical>
<div>col1,row1</div>
<div>col1,row2</div>
</StackPanelVertical>
<StackPanelVertical>
<div>col2,row1</div>
<div>col2,row2</div>
</StackPanelVertical>
</StackPanelHorizontal>`;
}, { name: "Footer", resolution: { func: (ref) => components()[ref] } }),
Page: somatic.createComponent(function (props, children) {
return `
<StackPanelVertical>Eureka
<Banner ${_({ title: "YCombinator", logo: "https://news.ycombinator.com/y18.gif" })} />
${children.join("")}
<Footer/>
</StackPanelVertical>`;
}, { name: "Page", resolution: { func: (ref) => components()[ref] } })
};
try {
let markup = components().Page();
console.log("Page markup: " + markup);
}
catch (e) {
console.log(e.toString());
}
Output is
Page markup: <div style='display: flex; flex-direction: column; justify-content: flex-start'>
<div>
Eureka
</div>
<div>
<div style='display: flex; justify-content: flex-start'>
<img src='https://news.ycombinator.com/y18.gif'></img>
<span>
YCombinator
</span>
</div>
</div>
<div>
<div style='display: flex; justify-content: flex-start'>
<div>
<div style='display: flex; flex-direction: column; justify-content: flex-start'>
<div>
<div>
col1,row1
</div>
</div>
<div>
<div>
col1,row2
</div>
</div>
</div>
</div>
<div>
<div style='display: flex; flex-direction: column; justify-content: flex-start'>
<div>
<div>
col2,row1
</div>
</div>
<div>
<div>
col2,row2
</div>
</div>
</div>
</div>
</div>
</div>
</div>
Install
npm install somatic --save