Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
trusktr committed Feb 20, 2020
0 parents commit d3fb264
Show file tree
Hide file tree
Showing 13 changed files with 854 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .builderrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
archetypes:
- builder-js-package
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# EditorConfig is awesome: http://EditorConfig.org

# top-most EditorConfig file
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules/
package-lock.json
dist/
.vscode/

*.log
**/.rush/
6 changes: 6 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# this file is copied into the dist/ after build, and the lib is published
# relative to the dist/ folder.
/**/*.test.js
/**/*.test.js.map
/**/*.test.d.ts
/tests/
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
10 changes: 10 additions & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
tabWidth: 4,
useTabs: true,
semi: false,
singleQuote: true,
trailingComma: 'all',
bracketSpacing: false,
printWidth: 120,
arrowParens: 'avoid',
}
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2018 Joseph Orbegoso Pea ([email protected])

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
302 changes: 302 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
# @lume/element

Create Custom Elements with reactivity and automatic re-rendering.

#### `npm install @lume/element --save`

## Intro

[Custom](https://developers.google.com/web/fundamentals/web-components/customelements)
[Elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements)
(also known as [Web
Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components) are
a feature of browsers that allow us to define new HTML elements that the
browser understands in the same way as built-in elements like `<div>` or
`<button>`.

If that flew over your head then you might first want to try a [beginner HTML
tutorial](https://htmldog.com/guides/html/beginner/). You will also need to
some basic knowledge of
[JavaScript](https://www.google.com/search?q=JavaScript%20for%20absolute%20beginners).

`@lume/element` provides a set of features that make it easier to manipulate
elements and to define new custom elements and easily compose them together
into an application.

With `@lume/element` we can create custom elements that have the following
features:

- Properties are reactive variables that make it easy to react to changes in these properties.
- Each custom element has an HTML template (in the form of [HTML markup inside JavaScript, or
JSX](https://facebook.github.io/jsx)) that automatically "re-renders" when any
reactive variable used in the template changes.
- When a template "re-renders", the whole template doesn't render, only the
part of the template where a variable changed is re-rendered. The term
"re-render" is quoted because tempate don't really re-render, but instead
reactive variables are mapped to discrete parts of the live
[DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model)
generated from a template.
- Changes to HTML attributes on a custom element can be easily mapped to the
custom element's properties. Because properties are reactive, this will cause
the custom element's template to update.

Additionally `@lume/element` can be used to create and manipulate trees of
DOM elements without necessarily creating new custom elements.

## Install and Setup

This section is TODO, and will explain how to set up an environment that
supports JSX syntax, which is needed for things to work.

## Basic Usage

### Manipulating and composing trees of elements

The following is an example that shows how to create a tree of HTML elements
whose attributes or text content automatically update when the value of a
reactive variable changes.

```jsx
import {variable} from '@lume/element'

// Make a reactive variable with an initial value of 0.
const count = variable(0)

// Increment the value of the variable every second.
setInterval(() => count(count() + 1), 1000)

// Create a <div> element with a child <h1> element. The data-count attribute
// and the text content of the <h1> element will automatically update whenever
// the count variable changes. You will see the text content update live in your
// browser.
const el = (
<div>
<h1 data-count={count()}>The count is: {count()}</h1>
</div>
)

// The result stored in the `el` variable is a regular DOM element! For example,
// you can call regular DOM APIs like setAttribute on it.
el.setAttribute('foo', 'bar')

// Append the element to the body of the page, and now you'll see a
// continually-updating message on your screen.
document.body.appendChild(el)
```

Continuing with the same `count` variable from the previous example, here's a
simple way to compose DOM trees using "functional components".

```jsx
// A functional component is a function that returns the
// The Label functional component uses the empty <></> tag to contain more than
// one root-level child; it will not render any actual element output for the
// empty tags.
const Label = props => (
<>
<div>{props.greeting}</div>
{props.children}
</>
)

// This Greeting functional component nests the content of the Label component
// in its template, and the <div> inside the <Label> gets distributed to the
// part of the Label component where we see `{props.children}`.
const Greeting = () => (
<section>
<Label greeting={'hello (' + count() + ')'}>
<div>John</div>
</Label>
</section>
)

// You only need to call this once, and you get an element reference. You do NOT
// need to call it over and over to re-render like you do in some other
// libraries. That's what makes all of this simple and clean. The reactivity
// inside the component templates takes care of updating content of the created
// DOM tree.
const elem = Greeting()

// It's just DOM! Use regular DOM APIs to append the element to the body.
document.body.prepend(elem)
```

### Create custom elements

Composing DOM trees with functions is one way that you can build an application.

Another way to create re-usable components is to create Custom Elements. The
advantage of custom elements is that they follow web standards, and therefore
they can be used in any web application and manipulated by any DOM
manipulation libraries like [jQuery](https://jquery.com/),
[React](https://reactjs.org), [Vue](https://vuejs.org), or
[Angular](https://angular.io), and many more.

In contrast to custom elements, functional components made with
`@lume/element` only work within the context of other functional components
or custom elements made with `@lume/element`. For portability across
applicatins and frameworks, it is preferable to create custom elements.

The following is a class-based web component (custom element) with a reactive
property `firstName` that also accepts values from an attribute named
`first-name` (the property name is converted to dash-case for the attribute
name).

```jsx
import {
Element, // A base class for LUME custom elements
attribute, // A property decorator to map attributes to properties
reactive, // A property decorator to make a property reactive
} from '@lume/element'

@customElement('greeting-card') // defines the element tag name
class GreetingCard extends Element {
// Make the firstName property a reactive variable, and also map any value
// from an attribute named 'first-name' back to this property (the attribute
// name is the dash-case version of the property name).
@reactive @attribute firstName = 'Roger'

// connectedCallback is a method that fires any time this custom element is
// connected into a web site's live DOM tree.
connectedCallback() {
// Once the element is connected, let's update the `.firstName` prop after a
// couple of seconds, and we'll see the change on screen change.
setTimeout(() => (this.firstName = 'Zaya'), 2000)

// And show that it works with by setting HTML attributes too, two seconds later.
setTimeout(() => this.setAttribute('first-name', 'Raquel'), 4000)
}

// Define the structure of the DOM tree that we want rendered on screen by
// providing a template() method. This template() method should simply
// return an DOM element, and that DOM element will be, by default, appended
// into the ShadowRoot of our custom element.
//
// To take advantage of reactivity in our template, simply use the same
// technique here as we did in the section above titled "Manipulating and
// composing trees of elements", by using reactive variables or properties
// in the places where they should be "rendered".
template() {
// any time the `.firstName` reactive property's value changes, the DOM
// will be automatically updated, thanks how the JSX works (it comiles
// to reactive computations).
const result = (
<div>
<span>
Hello <i>{this.firstName}</i>
</span>
</div>
)

return result
}
}
```

Now we can use it in the HTML of a web site, or in the template of another
component:

```html
<greeting-card first-name="Raynor"></greeting-card>
```

Just like in the section [Manipulating and composing trees of
elements](#manipulating-and-composing-trees-of-elements), inside a custom
element's `template()` method we can assign bits and pieces of DOM to
variables, and we can also use other custom elements and functional
components. For example the following is an alternative way to write the
previous `template()` method.

```jsx
@customElement('greeting-card')
class GreetingCard extends Element {
// ... same as before ...

template() {
const greeting = (
<span>
Hello <i>{this.firstName}</i>
</span>
)

const result = <div>{greeting}</div>

return result
}
}
```

### TypeScript

In TypeScript, all JSX expressions return the type `JSX.Element`. But with
`@lume/element`, JSX expressions return actual DOM nodes, and we want the
variable types to reflect that fact. For this we have a set of convenience
helpers to cast JSX types to DOM element types.

Modifying the very first example from above for TypeScript, it would look
like the following.

```tsx
import {variable, div} from '@lume/element'

const count = variable(0)

setInterval(() => count(count() + 1), 1000)

const el = div(
<div>
<h1 data-count={count()}>The count is: {count()}</h1>
</div>,
)

el.setAttribute('foo', 'bar')

document.body.appendChild(el)
```

The main difference is that the `div()` helper function explicitly returns
the type `HTMLDivElement` so that the `el` will be typed as `HTMLDivElement`
instead of `JSX.Element`. Under the hood, the `div()` function doesn't do
anything, it simply returns whatever you pass into it (an identity function
at runtime), and serves only as a convenient type cast helper.

We should remember to use the correct type helper depending on what the root
element of the JSX expression is. For for example, if the root was a `<menu>`
element then we'd use need to use the `menu()` helper like follows.

```tsx
import {variable, menu} from '@lume/element'

// ...

// The type of `el` will be `HTMLMenuElement`.
const el = menu(
<menu>
<h1 data-count={count()}>The count is: {count()}</h1>
</menu>,
)
```

If we use the wrong helper, then it will effectively cast the expression to
the wrong type. For example, in the next snippte the `el` variable will be of
type `HTMLDivElement` despite the fact that at runtime we will be have an
`HTMLMenuElement`.

```tsx
import {variable, div} from '@lume/element'

// ...

// This is wrong, don't do this!
const el = div(<menu>...</menu>)
```

Without the type helpers, we would need to write more verbose code like the following to have the proper types.

```tsx
import {variable} from '@lume/element'

// ...

const el = ((<menu>...</menu>) as any) as HTMLMenuElement
```
Loading

0 comments on commit d3fb264

Please sign in to comment.