项目作者: inad9300

项目描述 :
Type-safe HTML and custom components at your fingertips.
高级语言: TypeScript
项目地址: git://github.com/inad9300/Soil.git
创建时间: 2017-07-26T19:17:11Z
项目社区:https://github.com/inad9300/Soil

开源协议:GNU Affero General Public License v3.0

下载


Soil

A foundational library to help building long-lasting web applications. Soil
allows you to declaratively create type-safe HTML and SVG elements. One way to
think of it might be as “HTML-in-JS”.

Build Status
codecov
FOSSA Status

Motivation

The pace at which the web ecosystem evolves is unthinkably fast. At the same
time, trendy web frameworks often offer poor interoperability with standard
technologies, and the technical burden they introduce tends to be significant
and clearer than the benefits. Projects based on them are left with the choice
of deprecation and long-term unmaintainability, or the expense of unreasonable
amounts of resources to match the community’s speed.

Soil aims at putting together a minimal set of basic elements that embrace
today’s web standards and help you in developing high-quality, enduring
applications, while being competitive with popular frameworks in areas such as
reliability, testability, reusability, development experience and performance.

Basics

Soil encourages an architecture around components, conceptually similar to
the Web Components
proposal. Components are responsible for rendering parts of UI and controlling
the user interaction with them, and are framework-agnostic.

They create and manipulate HTML elements dynamically, with the help of
type-safe functions with a one-to-one correspondence with standard HTML
elements, which provides a look-and-feel similar to regular HTML.

  1. import {h, element} from '@soil/lib'
  2. export const counter = element(() => {
  3. const count = h('span')
  4. const tmpl = h('div', {}, [
  5. h('button', {onclick: () => ctrl.value--}, ['-']),
  6. count,
  7. h('button', {onclick: () => ctrl.value++}, ['+'])
  8. ])
  9. const ctrl = {
  10. get value() {
  11. return parseInt(count.textContent!, 10)
  12. },
  13. set value(v: number) {
  14. count.textContent = '' + v
  15. }
  16. }
  17. return [tmpl, ctrl]
  18. })

Custom components can then be used in a way similar to native ones.

  1. import {counter} from './counter'
  2. const c = counter({value: 1})
  3. document.body.appendChild(c)
  4. c.value++

While purely @dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0">presentational components
have no dependencies, container components may have them, including other
components. For this, pure dependency injection
is recommended, which could be achieved through default parameters,
or through explicit factory functions, as illustrated bellow.

  1. import {counterService, CounterService} from './counterService'
  2. export const counterFactory = (counterService: CounterService) => (props: {}) => {
  3. // ...
  4. }
  5. export const counter = counterFactory(counterService)
  6. export type Counter = typeof counter

Communicating adjacent components is usually easy. What about distant
components? There are plenty of alternatives
out there. One possibility is to use the native CustomEvent,
which integrates nicely with web components based on native DOM elements.

To get a bit more familiar with the ideas presented above, you may head to the
individual sub-projects to read their documentation, check out the examples,
or dive directly into the source code!

Installation

The package is available at npm’s registry, so it can be installed via npm or
Yarn:

  1. npm i -S @soil/lib
  2. # AKA npm install --save @soil/lib
  1. yarn add @soil/lib

Documentation

h (function and namespace)

Creating HTML using strings is not type-safe. Creating them from code is too
verbose. The h function serves as a shortcut function to create any HTML
element. As a namespace, it contains type aliases to refer the types returned
by this function.

  1. import {h} from '@soil/lib'
  2. const button: h.Button = h('button', {onclick: () => alert('Clicked')}, ['Click me'])
  3. const paragraph: h.P = h('p', {}, [
  4. 'Text with ',
  5. h('a', {href: '...'}, ['link'])
  6. ])
  7. const input: h.Input = h('input', {placeholder: 'Input...'})

They are provided under a namespace to avoid polluting the scope with plenty of
functions and types (a, A, b, B, …); to prevent problems with reserved
words such as var and switch, which would be required for elements such as
<var> and
<switch>;
and to avoid long import statements. As a nice side effect the auto-completion
experience is better too.

s (function and namespace)

Analogous to h for SVG elements.

  1. import {s} from '@soil/lib'
  2. s('svg', {width: {baseVal: {value: 100}}, height: {baseVal: {value: 100}}}, [
  3. s('circle', {
  4. cx: {baseVal: {value: 50}},
  5. cy: {baseVal: {value: 50}},
  6. r: {baseVal: {value: 40}},
  7. style: {
  8. stroke: 'green',
  9. strokeWidth: '4',
  10. fill: 'yellow'
  11. }
  12. })
  13. ])

Unfortunately, creating type-safe SVG programmatically results in verbose code,
and the difference between attributes and properties is bigger than in the HTML
case. The code above produces the same circle than the following HTML:

  1. <svg width="100" height="100">
  2. <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" ></circle>
  3. </svg>

On the other hand, we have access to the whole SVG API, richer than its
attribute-based counterpart, and there will be no differences between creating
elements and modifying them, e.g. you would otherwise need <circle stroke="green" ></circle>
for creation but circle.style.stroke = 'red' for modification.

element() (function)

Components are the main building block of modern web applications. They are
functions responsible for creating instances of custom elements, which typically
are regular HTML or SVG elements extended with custom functions, getters and
setters. The element() function facilitates and streamlines the creation of
such components, in a way that makes them resemble native elements.

element() accepts a function which serves as the definition of the custom
component. In turn, that function is responsible for returning a two-size tuple
containing the internal DOM structure of the element (its “template”) and a
number of functions which will determine the ways one is allowed to interact
with it (its “controller”), and optionally accepts a series of children.

  1. import {h, element} from '@soil/lib'
  2. const fancyLink = element(() => {
  3. const tmpl = h('a', {href: 'https://example.org/'}, ['Fancy Link'])
  4. const ctrl = {
  5. set secret(s: number) {
  6. tmpl.dataset.secret = '' + s
  7. },
  8. fly: () => console.log('Taking off...')
  9. }
  10. return [tmpl, ctrl]
  11. })
  12. const aFancyLink = fancyLink({secret: 1, className: 'a-fancy-link'})
  13. aFancyLink.secret = 2
  14. aFancyLink.fly()

Note how the controller implement the interface of the properties supported by
the component. Following from this, there is no need to perform an initial
assignment of the properties to the internal DOM elements: this will happen
automatically, enforcing consistent behaviour between initialization and later
usage, as we are accustomed to with native interfaces.

Dependencies

When components need dependencies, they can be defined as a high-order function.
To facilitate both regular development and testing, both the factory function
and the default instance can be exported.

  1. import {element} from '@soil/lib'
  2. import {serviceX, ServiceX} from './ServiceX'
  3. export const elemXFactory = (serviceX: ServiceX) => element(() => {
  4. // ...
  5. })
  6. export const elemX = elemXFactory(serviceX)

Contributions

Feel free and encouraged to open new discussions on any
non-technical topic that may help maturing Soil. For technical contributions,
pull requests are also welcomed.

License

The Soil project is licensed under the GNU Affero General Public License.