0.2.0 • Published 5 months ago

@beforesemicolon/cube v0.2.0

Weekly downloads
-
License
BSD-3-Clause
Repository
-
Last release
5 months ago

cube

npm npm

Cube is a simple and powerful function-oriented framework to create future-proof UI components and web applications.

Quick Start

npm install @beforesemicolon/cube
import {template, register} from '@beforesemicolon/cube';

const MyButton = (props) => {
  template`
    <button type=${props.type}>
      <slot></slot> 
    </button>`;
}

register(MyButton, {
  // defining props by setting default values
  type: "button"
});
<my-button>Click me</my-button>

You can also import it directly in the browser

<!--insert in the head of the document-->

<!-- Grab the latest version -->
<script src="https://unpkg.com/@beforesemicolon/cube/dist/client.js"></script>

<!-- Or a specific version -->
<script src="https://unpkg.com/@beforesemicolon/cube@0.1.6/dist/client.js"></script>

Then you would use it as:

const {template, register} = window.BFS.Cube;

Documentation

Table of Content

Create a component

Cube components are simply functions that you register as web components although not every component needs to be a web component.

const MyButton = () => {}; // valid component

register(MyButton)

The name of the function must be two part camel-cased. This is because Cube will turn it into a kebab-cased valid html web component name. Therefore, the above button can be used like:

<my-button></my-button>

But it will not render anything because we haven't defined its template body. To do that we use the template function.

const MyButton = () => {
  template`<button>my button</button>`
};

register(MyButton)

Cube template is based on @bfs/html , and it is a powerful reactive templating system that does not everything you need for templating.

Check this amazing Todo App built just with this templating library.

Create Elements

Cube elements are simply html instances. Bellow are valid elements:

const Button = html`<button>my button</button>`;
const ButtonFn = (props) => html`<button type="${props.type}">${props.content}</button>`;

They don't need to be registered and can be used by any component or rendered directly in the DOM:

Button.render(document.body)
Button({type: "button", content: "click me"}).render(document.body)

To learn more about Cube template check @bfs/html docs which Cube is built on.

Props

Props are simply attributes that the component tag will receive or that the element will take. The value of these props can be anything.

A component only becomes aware of attributes when you define prop defaults when registering a component.

const MyButton = (props) => {
  template`
    <button attr.disabled="${props.disabled}">
      <slot></slot>
    </button>`;
};

register(MyButton, {
    disabled: false,
})

One thing to keep in mind is the data type, especially when you plan to set these attribute values when using the .html file. Bellow is an example of how to pass a JSON data to an element in a HTML file using single quotes to wrap the whole thing.

If this is how you plan to use the tag, JSON string is the most complex data representation you can use.

// index.html

<todo-item data='{"name": "sample", "status": "pending"}'></todo-item>

If the component tag will be used only inside other components, it does not matter how the data look like, but you should always aim for simple data representations.

const TodoApp = () => {
    const todos = [];
    
    template`
      <todo-item data="${todos[0]}"></todo-item>
      <todo-item data="${todos[1]}"></todo-item>
      <todo-item data="${todos[2]}"></todo-item>
    `
}

Recommended would be to split the data into their primitive values instead like native HTML.

// index.html

<todo-item name="sample" status="pending"></todo-item>

Component Options

Web components are rendered with shadow root in open mode. Cube allows you to change these options:

register(MyButton, {
    disabled: false,
}, {
  // the none value is valid to Cube only and 
  // it means to render component without shadow root
  mode: 'none',
  delegatesFocus: true
})

Lifecycles

Cube has few lifecycle functions:

  • onMount: Executes when component is added to the DOM
  • onUpdate: Executes when component props change either by attributes or property change.
  • onDestroy: Executes when component is removed to the DOM
  • onAdoption Executes when component is moved from one document to another. For example, from an iframe document to the page document.
const MyButton = () => {
  onMount(() => {
    console.log('mounted');
  })
  
  // called only after component is mounted
  // any prop event while component is unmounted is ignored
  onUpdate((propName, newValue, oldValue) => {
    console.log('prop updated', propName, newValue, oldValue);
  })
  
  onDestroy(() => {
    console.log('unmounted');
  })
  
  onAdoption(() => {
    console.log('mouted on a different document');
  })
};

State

There is no real concept of state in Cube. Any data used in the template is checked for change when there is an update.

By default, any data in the template is considered static data:

const CounterApp = () => {
	const count = 0; // static data
	
	template`<span>${count}</span>`;
}

To define dynamic data you should use functions which are called in the template whenever there is an update.

const CounterApp = () => {
    let count = 0;
    
    const countUp = () => {
        count += 1;
        temp.update(); // tell the DOM to update to catch the count change
    }
    
    // tell template count will change
    var temp = template`
      <span>${() => count}</span>
      <button onclick="${countUp}">count</button>
    `;
}

However, this is quite messy. Therefore, Cube gives you state that handles all that for you:

const CounterApp = () => {
    // get a getter and a setter functions for a value
    let [count, updateCount] = state(0);
    
    const countUp = () => {
      updateCount(count() + 1)
    }
        
    var temp = template`
      <span>${count}</span>
      <button onclick="${countUp}">count</button>
    `;
}

The state will automatically trigger DOM update on changes but, it should not be used for everything. You can still keep data as dynamic data and have a state that when changed will trigger DOM update causing everything else to be considered.

Events

Cube is event driven and that's why you should not pass function as props. Function as props are just callbacks and with Cube, any component should be able to listen to another regardless of hierarchy via events.

To listen to an event, you set an attribute on the element tag itself starting with on followed by the name of the event all lowercase to match the native HTML pattern. The name of the event can be any custom event that particular component dispatches or any native HTML element events.

const TextInput = () => {
    const dispatch = eventDispatch();
    
    const onChange = (event) => {
      dispatch('changed', event.target.value)
    }
    
    template`
      <input value=${props.value} type=${props.type} onchange="${onChange}"/>
    `;
}

The eventDispatch helper will return a function you can call with the name of the event and data you want to pass with this event. All this will trigger a Customer with correct data details.

Host

When you create a web component you get a HTML tag you can use. In case you want to access it for any reason, Cube exposes a host helper that will provide you with that instance.

const MyButton = () => {
  const btn = host(); // returns HTMLElement instance of my-button tag
};

Conditional Render

Again, because everything is function, conditional rendering is as simple as a function with a ternary:

const TodoStatus = (props) => {
    template`
      ${props.status() === "pending" 
        ? html`<span class="pending">Pending</span>` 
        : html`<span class="done">Done</span>`
      }
    `
}

However, you need to remember that such function is called whenever there is a change and will produce new instances of the html to be rendered. This might not be what you want as it forces new DOM creation.

To optimal way to do it is to have variable with those elements and swap them on render updates.

const TodoStatus = (props) => {
    const pendingIndicator = html`<span class="pending">Pending</span>`;
    const comleteIndicator = html`<span class="done">Done</span>`;
    
    template`
      ${props.status() === "pending" 
        ? pendingIndicator
        : comleteIndicator
      }
    `
}

But Cube exposes a when helper which does all these by default. Learn more about it

const TodoStatus = (props) => {
    template`
      ${when(props.status() === "pending",
        html`<span class="pending">Pending</span>`,
        html`<span class="done">Done</span>`
      )}
    `
}

Conditional Attributes

Cube handles conditional attributes by prefixing any attributes with attr and providing a condition. Learn more

const MyButton = () => {
    const ctaButton = () => props.variant() === 'cta';

    template`
      <button
        // only adds disabled attribute when disabled value is TRUE
        attr.disabled="${props.disabled}" 
        
        // adds class of 'cta' when variant is 'cta'
        attr.class.cta="${ctaButton}" 
        
        // adds orange background when cta button variant
        attr.style="background-color: orange, ${ctaButton}"
        >
        <slot></slot>
      </button>
    `
}

Repeating/listing content

Cube will already optimally handle any array you put in the template.

const TodoApp = () => {
    const items = ["item-1", "item-2", "item-3"];
    
    template`${items}`; // renders item-1item-2item-3
}

You may also have a list of html instances.

const TodoApp = () => {
    const items = [
        html`<span>item-1</span>`,
        html`<span>item-2</span>`,
        html`<span>item-3</span>`,
    ];
    
    template`${items}`;
}

But when it comes to composing lists and dynamic data Cube exposes the repeat helper that handles everything for you. Learn more

const TodoApp = () => {
    template`${repeat(todos, item => html`<span>${item.name}</span>`)}`;
}

Styling

Cube allows you to attach style to your components when you register them. Style is simply a function that returns a CSS string or object. The style is only render once but once Cube understands you use the props in your style, it gets updated with every prop change.

const MyButtonStyle = (props) => {
  return {
    ":host": {
        button: {
          all: "unset",
          background: "#222",
          color: "#fff",
          padding: "8px 20px",
          borderRadius: 3,
          opacity: props.disabled() ? "0.1" : "1"
        }
    }
  }
}

Then you just need to tell Cube which style to use:

const MyButton = (props) => {
  ...
};

register(MyButton, {
    disabled: false,
}, MyButtonStyle) // provide the style

You may also return a CSS string but it does not support nested style:

const MyButtonStyle = (props) => {
  return `
    // the style tag is not needed but it helps IDEs syntax highlight CSS
    <style> 
      :host button {
        all: unset;
        background: #222;
        color: #fff;
        border-radius: 3px;
        opacity: ${props.disabled() ? "0.1" : "1"}
      }
    </style>
  `
}

You may also provide a stylesheet link which is perfect if you have external stylesheets:

const MyButtonStyle = () => {
  return `<link rel="stylesheet" href="./my-button.css">`;
}
0.2.0

5 months ago

0.1.21-beta

6 months ago

0.1.20-beta

7 months ago

0.1.19-beta

7 months ago

0.1.18-beta

7 months ago

0.1.17-beta

7 months ago

0.1.16-beta

7 months ago

0.1.15-beta

7 months ago

0.1.14-beta

7 months ago

0.1.13-beta

7 months ago

0.1.12-beta

7 months ago

0.1.11-beta

7 months ago

0.1.10-beta

7 months ago

0.1.10

8 months ago

0.1.9

9 months ago

0.1.8

9 months ago

0.1.7

9 months ago

0.1.6

9 months ago

0.1.5

9 months ago

0.1.4

9 months ago

0.1.3

10 months ago

0.1.2

10 months ago

0.1.1

10 months ago

0.1.0

10 months ago