Bearer

The Bearer Documentation Hub

Welcome to the Bearer documentation hub. You'll find comprehensive guides and documentation to help you start working with Bearer as quickly as possible, as well as support if you get stuck. Let's jump right in!

Get Started    Changelog

Components

If you don't any experience building component this guide should help you understand some of the core concept used in Bearer Framework.

Stencil Heritage

Bearer heavily use Stencil behind the Scene to render components, as a result many of the concepts & documentations shown below is inherited. Thanks to the Stencil team for their amazing work 🙏

Architecture

Bearer components are created by adding a new file with a .tsx extension, such as my-first-component.tsx, and placing them in the /views or /views/components directory. The .tsx extension is required since Bearer components are built using JSX and TypeScript.

Bearer provides two kinds of components, see Scenario Anatomy.

Here is an example of what a Bearer component looks like:

import { Component, Intent, BearerFetch } from '@bearer/core'

@Component({
  tag: 'list-repositories'
})

export class ListRepositories {
  @Intent('ListRepositories') fetcher: BearerFetch
  
  render() {
    return <bearer-scrollable fetcher={this.fetcher} />
  }
}

Once compiled, this components can be used in HTML just like any other tag.

<list-repositories />

Nesting

Components can be composed easily by adding the HTML tag to the JSX code. Since components are just HTML tags, nothing needs to be imported to component within another component.

Here is an example:

import { Component, Prop } from '@bearer/core'

@Component({
  tag: 'my-embedded-component'
})

export class MyEmbeddedComponent {
  @Prop() color: string = 'blue';

  render() {
    return (
      <div>My favorite color is {this.color}</div>
    )
  }
}
import { Component, Prop } from '@bearer/core'

@Component({
  tag: 'my-parent-component'
})

export class MyParentComponent {
  render() {
    return (
      <my-embedded-component color="red"></my-embedded-component>
    )
  }
}

JSX

Bearer components are rendered using JSX, a popular, declarative template syntax. Each component has a render function that returns a tree of components that are rendered to the DOM at runtime.

Data binding

Components often need to render dynamic data. To do this in JSX, use { } around a variable:

render() {
  return (
    <div>Hello {this.name}</div>
  )
}

Conditionals

If we want to conditionally render different content, we can use JavaScript if/else statements: Here, if name is not defined, we can just render a different element.

render() {
  if (this.name) {
    return ( <div>Hello {this.name}</div> )
  } else {
    return ( <div>Hello, World</div> )
  }
}

Additionally, inline conditionals can be created using the JavaScript ternary operator:

render() {
  return (
    {this.name
      ? <p>Hello {this.name}</p>
      : <p>Hello World</p>
    }
  )
}

Loops

Loops can be created in JSX using either traditional loops when creating JSX trees, or using array operators such as map when inlined in existing JSX.

In the example below, we're going to assume the component has a local property called todos which is a list of todo objects. We'll use the map function on the array to loop over each item in the map, and to convert it to something else - in this case JSX.

render() {
  return (
    <ul>
      {this.todos.map((todo) =>
        <li>
          <div>{todo.taskName}</div>
          <div>{todo.isCompleted}</div>
        </li>
      )}
    </ul>
  )
}

Each step through the map function creates a new JSX sub tree and adds it to the array returned from map, which is then drawn in the JSX tree above it.

Life Cycle

Components have numerous lifecycle methods which can be used to know when the component "will" and "did" load, update, and unload. These methods can be added to a component to hook into operations at the right time.

Implement one of the following methods within a component class and it will get automatically call them in the right order:

import { Component } from '@bearer/core';

@Component({
  tag: 'my-component'
})

export class MyComponent {

  /**
   * The component is about to load and it has not
   * rendered yet.
   *
   * This is the best place to make any data updates
   * before the first render.
   *
   * componentWillLoad will only be called once.
   */
  componentWillLoad() {
    console.log('Component is about to be rendered');
  }

  /**
   * The component has loaded and has already rendered.
   *
   * Updating data in this method will cause the
   * component to re-render.
   *
   * componentDidLoad will only be called once.
   */
  componentDidLoad() {
    console.log('Component has been rendered');
  }

  /**
   * The component is about to update and re-render.
   *
   * Called multiple times throughout the life of
   * the component as it updates.
   *
   * componentWillUpdate is not called on the first render.
   */
  componentWillUpdate() {
    console.log('Component will update and re-render');
  }

  /**
   * The component has just re-rendered.
   *
   * Called multiple times throughout the life of
   * the component as it updates.
   *
   * componentWillUpdate is not called on the
   * first render.
   */
  componentDidUpdate() {
    console.log('Component did update');
  }

  /**
   * The component did unload and the element
   * will be destroyed.
   */
  componentDidUnload() {
    console.log('Component removed from the DOM');
  }
}

Rendering State

It's always recommended to make any rendered state updates within componentWillLoad() or componentWillUpdate(), since these are the methods which get called before the render() method. Alternatively, updating rendered state with the componentDidLoad() or componentDidUpdate() methods will cause another re-render, which isn't ideal for performance.

If state must be updated in componentDidUpdate(), it has the potential of getting components stuck in an infinite loop. If updating state within componentDidUpdate() is unavoidable, then the method should also come with a way to detect if the props or state is "dirty" or not (is the data actually different or is it the same as before). By doing a dirty check, componentDidUpdate() is able to avoid rendering the same data, and which in turn calls componentDidUpdate() again.

Lifecycle Hierarchy

A useful feature of lifecycle methods is that they take their child component's lifecycle into consideration too. For example, if the parent component, cmp-a, has a child component, cmp-b, then cmp-a isn't considered "loaded" until cmp-b has finished loading. Another way to put it is that the deepest components finish loading first, then the componentDidLoad() calls bubble up.

It's also important to note that even though components are lazy-loaded, and have asynchronous rendering, the lifecycle methods are still called in the correct order. So while the top-level component could have already been loaded, all of its lifecycle methods are still called in the correct order, which means it'll wait for a child components to finish loading. The same goes for the exact opposite, where the child components may already be ready while the parent isn't.

In the example below we have a simple hierarchy of components. The numbered list shows the order of which the lifecycle methods will fire.

  <cmp-a>
    <cmp-b>
      <cmp-c></cmp-c>
    </cmp-b>
  </cmp-a>

1 cmp-a - componentWillLoad() 2 cmp-b - componentWillLoad()
3 cmp-c - componentWillLoad() 4 cmp-c - componentDidLoad()
5 cmp-b - componentDidLoad() 6 cmp-a - componentDidLoad()

Even if some components may or may not be already loaded, the entire component hierarchy waits on its child components to finish loading and rendering.

Events

Components can emit data and events using the Event Emitter decorator.

To dispatch Custom DOM events for other components to handle, use the @Event() decorator.

import { Event, EventEmitter } from '@bearer/core';

...
export class TodoList {

  @Event() todoCompleted: EventEmitter;

  todoCompletedHandler(todo: Todo) {
    this.todoCompleted.emit(todo);
  }
}

The code above will dispatch a custom DOM event called todoCompleted.

Listening for Events

The Listen() decorator is for handling events dispatched from @Events.

In the example below, assume that a child component, TodoList, emits a todoCompleted event using the EventEmitter.

import { Listen } from '@bearer/core';

...
export class TodoApp {

  @Listen('todoCompleted')
  todoCompletedHandler(event: CustomEvent) {
    console.log('Received the custom todoCompleted event: ', event.detail);
  }
}

Handlers can also be registered for an event on a specific element. This is useful for listening to application-wide events. In the example below, we're going to listen for the scroll event.

import { Listen } from '@bearer/core';

...
export class TodoList {

  @Listen('body:scroll')
  handleScroll(ev) {
    console.log('the body was scrolled', ev);
  }
}

Using events in JSX

You can also bind listeners to events directly in JSX. This works very similar to normal DOM events such as onClick.

Lets use our TodoList component from above:

import { Event, EventEmitter } from '@bearer/core';

...
export class TodoList {

  @Event() todoCompleted: EventEmitter;

  todoCompletedHandler(todo: Todo) {
    this.todoCompleted.emit(todo);
  }
}

Decorators

Bearer makes it easy to build rich, interactive components using Decorators.

Component & RootComponent

Each Bearer Component must be decorated with either a @Component() or @RootComponent() decorator.

Every Scenario must provide at least one RootComponent in order to be Integrated.
The @RootComponent() decorator looks like this:

import { RootComponent } from '@bearer/core';

@RootComponent({
  role: 'action',
  group: 'feature',
})

Every Root Component end-up being served as a WebComponent, the HTML tag associated is generated using: <ScenarioName-RootComponentGroup-RootComponentName />.

The @Component() decorator:

import { Component } from '@bearer/core';

@Component({
  tag: 'create-reminder',
})

Output and Input

The Output decorator is used to store information that can be retrieved later. It is automatically connected with the SaveState Intent that will be responsible for storing in Bearer's storage.

import { RootComponent, Output } from '@bearer/core'
import '@bearer/ui'

@RootComponent({
  role: 'action',
  group: 'feature'
})
export class FeatureAction {
  @Output() foo: any = []

  ...
  
  myMethod = ({ data, complete }): void => {
    this.foo = [...this.foo, data.foo]
    complete()
  }
  
  ...

  render() {
    return (
      <bearer-navigator
        direction="right"
        complete={this.myMethod}
      >
				...
      </bearer-navigator>
    )
  }
}
import { SaveState, TOAUTH2AuthContext, TSaveStateCallback } from '@bearer/intents'

export default class SaveFooIntent {
  static intentType: any = SaveState

  static action(
    _context: TOAUTH2AuthContext,
    _params: any,
    body: any,
    state: any,
    callback: TSaveStateCallback
  ): void {
    callback({
      state: {
        ...state,
        foo: body.foo
      },
      data: body.foo
    })
  }
}

The Input decorator is used to retrieve stored information. It is connected to the RetrieveState Intent that is going to retrieve the data from Bearer's storage automatically.

import { RootComponent, Input } from '@bearer/core'
import '@bearer/ui'

@RootComponent({
  role: 'display',
  group: 'feature'
})
export class FeatureDisplay {
  @Input() foo: any = []

  render() {
    return (
      <ul>
        {this.foo.map(item => (
          <li>...</li>
        ))}
      </ul>
    )
  }
}
import { RetrieveState, TOAUTH2AuthContext, TRetrieveStateCallback } from '@bearer/intents'

import Client from './client'

export default class RetrieveFooIntent {
  static intentType: any = RetrieveState

  static action(context: TOAUTH2AuthContext, _params: any, state: any, callback: TRetrieveStateCallback) {
  	...
    callback({ data: state.foo })
  }
}

The Input and the Output decorator works well together.
When 2 Root Components are using an Input and an Output are on the same page, they will communicate with each other automatically.

State

The @State() decorator can be used to manage internal data for a component. This means that a user cannot modify this data from outside the component, but the component can modify it however it sees fit. Any changes to a @State() property will cause the components render function to be called again.

import { State } from '@bearer/core';

...
export class TodoList {
  @State() completedTodos: Todo[];

  completeTodo(todo: Todo) {
    // This will cause our render function to be called again
    this.completedTodos = [...this.completedTodos, todo];
  }

  render() {
    //
  }
}

Intent

Intent decorator ease the way to retrieve data from the intents created. It handles all the complexity behind data fetching for you.

Here is a simple usage:

import { Component, Intent, BearerFetch } from '@bearer/core';

@Component({
  tag: 'list-pull-request',
})

class ListPullRequest {
  @Intent('listPullRequest') fetcher: BearerFetch
  
  render(){
    return (
      <bearer-scrollable fetcher={this.fetcher} />
    )
  }
}

We added a fetcher property to our component. Then we passed it to the bearer-scrollable component. When bearer-scrollable is mounted, it uses our fetcher to fetch data from the listPullRequest intent implemented.

Passing extra parameter to the fetcher.

In the example below we pass extra information to the fetcher, going all its way to the the intent:

import { Component, Intent, State, BearerFetch } from '@bearer/core';

@Component({
  tag: 'advanced-fetcher',
})
class AdvancedFetcher {
  @Intent('listPullRequest') fetcher: BearerFetch
  @Prop() repositoryName: string
  
  customizedFetcher: BearerFetch = (params = {}): Promise<any> => {
    return this.fetcher({ ...params, repositoryName: this.repositoryName })
  }
  
  render(){
    return (
      <bearer-scrollable fetcher={this.customizedFetcher} />
    )
  }
}

Saving Scenario State with Intent Decorator

SaveState

The SaveState is automatically created for you when you use the @Output Decorator.
See Component Communication

Each scenario must have at least one Intent with SaveState type either you want to store data (save pull requests references) or you want to perform an action without expecting to store any data (create a slack reminder).

import { RootComponent, Intent, IntentType State, BearerFetch } from '@bearer/core';

@RootComponent({
  role: 'action',
  group: 'feature',
})

class FeactureAction {
  @Intent('saveState', IntentType.SaveState) fetcher: BearerFetch

  completeScenario = ({ data, complete }) => {
    this.fetcher({ body: data }).then(() => {
      // update local BearerState if needed
      complete()
    })
  }
  
  render(){
    return (
      <bearer-navigator complete={this.completeScenario}>
        //many screens
      </bearer-navigator>      
    )
  }
}

When a user complete the feature action flow then the data (coming from the navigator completion) is sent to the completeScenario method.

Retrieving Scenario State with Intent Decorator

RetrieveState

The RetrieveState is automatically created for you when you use the @Input Decorator.
See Component Communication

In some cases, we want to retrieve the current state of the scenario to display it within the display component. Here's how to achieve this:

import { RootComponent, Intent, IntentType, BearerState, BearerFetch, State } from '@bearer/core';

@RootComponent({
  role: 'display',
  group: 'feature',
})

class FeactureDisplay {
  @Intent('RetrieveState', IntentType.RetrieveState) fetcher: BearerFetch
  @State() loading: boolean = true
  @BearerState() attachedPullRequests: Array<any> = []
  
  componentDidLoad() {
    this.loading = true
    this.fetcher()
      .then(({ data }) => {
        if (data) {
          this.attachedPullRequests = data
        }
        this.loading = false
      })
      .catch(error => {
        console.error('Error while fetching', error)
        this.loading = false
      })
  }
  
  render() {
    if (this.loading) {
      return <bearer-loading />
    }
    return (
      <ul>
        {this.attachedPullRequests.map(pr => <li>{pr.title}</li>)}
      </ul>
    )
  }
}

Prop

Props are custom attribute/properties exposed publicly on the element that developers can provide values for. Children components should not know about or reference parent components, so Props should be used to pass data down from the parent to the child. Components need to explicitly declare the Props they expect to receive using the @Prop() decorator. Props can be a number, string, boolean, or even an Object or Array. By default, when a member decorated with a @Prop() decorator is set, the component will efficiently re-render.

import { Prop } from '@bearer/core';
...
export class TodoList {
  @Prop() color: string;
  @Prop() favoriteNumber: number;
  @Prop() isSelected: boolean;
  @Prop() myHttpService: MyHttpService;
}

Within the TodoList class, the Props are accessed via the this operator.

logColor() {
  console.log(this.color)
}

Externally, Props are set on the element (In HTML, you must set attributes using dash-case):

<todo-list color="blue" favorite-number="24" is-selected="true"></todo-list>

in JSX you set an attribute using camelCase:

<todo-list color="blue" favoriteNumber="24" isSelected="true"></todo-list>

Prop value mutability

It's important to know, that a Prop is by default immutable from inside the component logic. Once a value is set by a user, the component cannot update it internally.

However, it's possible to explicitly allow a Prop to be mutated from inside the component, by declaring it as mutable, as in the example below:

import { Prop } from '@bearer/core';
...
export class NameElement {
  @Prop({ mutable: true }) name: string = 'Bearer';

  componentDidLoad() {
    this.name = 'Bearer Beta';
  }
}

Prop default values and validation

Setting a default value on a Prop:

import { Prop } from '@bearer/core';
...
export class NameElement {
  @Prop() name: string = 'Bearer';
}

Watch

When a user updates a property, Watch will fire what ever method it's attached to and pass that method the new value of the prop along with the old value. Watch is useful for validating props or handling side effects.

import { Prop, Watch } from '@bearer/core';

export class LoadingIndicator {
  @Prop() activated: boolean;

  @Watch('activated')
  watchHandler(newValue: boolean, oldValue: boolean) {
    console.log('The new value of activated is: ', newValue);
  }
}

Forms

Basic forms

Here is an example of a component with a basic form:

@Component({
  tag: 'my-name',
  styleUrl: 'my-name.scss'
})
export class MyName {
  @State() value: string;

  handleSubmit(e) {
    e.preventDefault()
    console.log(this.value);
    // Post data to our Intent
  }

  handleChange(event) {
    this.value = event.target.value;
  }

  render() {
    return (
      <form onSubmit={(e) => this.handleSubmit(e)}>
        <label>
          Name:
          <input type="text" value={this.value} onInput={(event) => this.handleChange(event)} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

Let's go over what is happening here. First we bind the value of the input to a state variable, in this case this.value. We then set our state variable to the new value of the input with the handleChange method we have bound to onInput. onInput will fire every keystroke that the user types into the input.

Advanced forms

Here is an example of a component with a more advanced form:

@Component({
  tag: 'my-name',
  styleUrl: 'my-name.scss'
})
export class MyName {

  @State() value: string;
  @State() selectValue: string;
  @State() secondSelectValue: string;
  @State() avOptions: any[];

  handleSubmit() {
    console.log(this.value);
  }

  handleChange(event) {
    this.value = event.target.value;

    if (event.target.validity.typeMismatch) {
      console.log('this element is not valid')
    }
  }

  handleSelect(event) {
    console.log(event.target.value);
    this.selectValue = event.target.value;
  }

  handleSecondSelect(event) {
    console.log(event.target.value);
    this.secondSelectValue = event.target.value;
  }

  render() {
    return (
      <form onSubmit={(e) => this.handleSubmit(e)}>
        <label>
          Email:
          <input type="email" value={this.value} onInput={(e) => this.handleChange(e)} />
        </label>

        <select onInput={(event) => this.handleSelect(event)}>
          <option value="volvo" selected={this.selectValue === 'volvo'}>Volvo</option>
          <option value="saab" selected={this.selectValue === 'saab'}>Saab</option>
          <option value="mercedes" selected={this.selectValue === 'mercedes'}>Mercedes</option>
          <option value="audi" selected={this.selectValue === 'audi'}>Audi</option>
        </select>

        <select onInput={(event) => this.handleSecondSelect(event)}>
          {this.avOptions.map(recipient => (
            <option value={recipient.id} selected={this.selectedReceiverIds.indexOf(recipient.id) !== -1}>{recipient.name}</option>
          ))}
        </select>

        <input type="submit" value="Submit" />
      </form>
    );
  }
}

This form is a little more advanced in that it has two select inputs along with an email input. We also do validity checking of our email input in the handleChange method. We handle the select element in a very similar manner to how we handle text inputs.

For the validity checking, we are using the constraint validation api that is built right into the browser to check if the user is actually entering an email or not.

Styling

By default components use Shadow Dom, meaning DOM and Style of each component is encapsulated from the outside world.

The @Component and @RootComponent decorators provide a styleUrl property used to define the stylesheet associated with a component (relative path):

@Component({
  tag: 'create-reminder',
  styleUrl: 'CreateReminder.css'
})