Components in ChocolateChip-UI
Although there are many different layouts and widgets that you can use to build your hybrid mobile app with in ChocolateChip-UI, components are the most important feature. When you know how to use components, you can create your own widgets with layouts, state, bound user actions and scoped themes. And, since components are built using ES6 classes, you can extend the Component class to add features you need.
When creating components there are a few things to be aware of. Every component will need a root element which serves as the contain in which it renders. Then again, not every component needs to render. At minimum as component can server as the point for attaching user actions to a document element, or scoped styles. However, usually you are going to create a component to render data to the document. To do this, you need to define two things: the root element and the render function. The component's element property value must be a valid CSS selector. In general it is best to use an element ID, unless the element is unique in the document. If you use a tag or class and their are multiple instances of them, the component will render in each of them.
Initializing a Component:
The render function expects a template which it will use to render data. In the example below, we define our template using the tagged template literal "html". This is provided by ChocolateChip-UI to make it easier for you to use template literals with components. It does several things, it automatically handles string contention with Array.map and sanitizes any data consumed by the template. The last is very important as it protects you app from malicious script inject.
When we pass the variable "number" to the component's render function, the result will be one list item with the content of "One". We could also just pass a JavaScript number as a value, and that would print as well. Now, if we wanted to print out a list of numbers, we could pass the render function an array of numbers:
Besides these primitive types, we can also pass a component complex types, such as objects or arrays of objects. When we want to render a component with an object, we need to indicate what properties of the object the template should use, and we do that with "." notation. In the example below, we've create a person object with a first and last name. In the template we can access those properties off of the object:
This component will now render the first and last name of the person. But what if we have an array of persons? No problem. Just like our previous example, ChocolateChip-UI examines the data you pass to the render function. If it's a simple type or object it renders it, if it's and array, it loops over the array and renders each index against the template. If your template is expecting primitive types and the array has that, it will render them without a problem. If your template is expecting and object and the array contains many similar objects, the component will loop over them and print them out. No need for special syntax like ng-for, v-for, each{} or Array.map. Learning the particular ways in which frameworks handle arrays of data is one of the most annoying things for me. So, ChocolateChip-UI frees you from learning a new way to deal with arrays. You don't even have to think about them. Just design your component's template to work with the data, and then, whether its singular object or an array of objects, it will work. No muss, no fuss.
Array Index Value
When you are dealing with an array, you may sometimes want to be able to take advantage of the index value. This might be for outputting item numbering, creation of ids or outputting image sources. You can accomplish this as well by giving your render function a second parameter after the data parameter. This will be passed to the component render function as the loop index. Since loop indexes start with 0, if you want to make them human-readable you'll need to add 1 to the value before printing it. Below is an example of a component that outputs item number with the loop index. To capture the index value, we use the parameter idx:
Stateful Components
So far we have been dealing with stateless components. If you are not going to render your component more than once, then using a stateless component is fine. Or you may only need to render the component once in a while with updated data. However, if you don't know when the data will change, or it might change frequently, it's time to think about using stateful components. You do this by creating a State object and assigning it to the component. The State object is quite complex and it has a lot of powerful methods. As such, we are not going to delve into that here. You can read more about State in the documentation. Creating a State object is extremely easy. Just use the class to assign a new instance to a variable. You literally pass the State object the data you want to encapsulate:
The above code gives us a State object named personState. We can use this as the state of our component:
With the component's state controlled by personState, whenever we change personState, the component will update automatically. This is often referred to as "data binding". ChocolateChip-UI only supports one-way data flows. This makes your apps easier to reason about. ChocolateChip-UI also offers form-specific functionality, such as validation, serialization, etc. If you feel like you cannot live without two-way data binding, then you can pull it off by using ChocolateChip-UI's mediators to handle that. You can learn more about mediators in the documentation. However, components have a property called actions that let you define user interactions on a component. If you're component had an input, you could define an action that would render a component with the value of the input as the user types. This is trivial to set up. As a matter of fact you can examine the examples that ChocolateChip-UI provides to see how this works. How to get them was explained in the article previous to this.
User Actions
Besides the root element, render function and state, a component can also have actions. This property takes an array of events registered on the elements of the component. This might be the root element, or a child element. Where to register the event depends on the needs. Actions have three parts: an event, an element and a callback. You could use normal JavaScript events, such as click events or touch events. However, ChocolateChip-UI provides several special gestures that work identically on desktop and mobile. This makes it easy for you to develop and test on your computer and be sure how they'll work on mobile devices. The gestures are:
- tap
- dbltap
- longtag
- swipeleft
- swiperight
- swipeup
- swipe down
A tap has a delay of 150 milliseconds. This provides enough time so that if the use touches a tappable item and begins to scroll, the tap will be cancel and the scroll will continue. A long tap has a delay of 750 milliseconds. Swipes must be at least 50 pixels long to register.
Defining a User Action
As we mentioned, you need to indicate an element on which the event will be registered. If this is the component root, for element you can provide "self" as the selector. Otherwise, provide a selector for the components child elements. In the case of our previous list component example, we want to register a user action on the list item. When the user taps, we want to alert the content of the list item. This is how we would set that up:
Because the component action property takes and array, you can define many different user actions on a component.
Scoped Styles
You can create a virtual stylesheet scoped to your component. You do this using an object notation assigned to the component's styles property. Creating a scoped stylesheet makes your component more portable. You can reuse the component in another project and the styles will automatically be included. No need to track down an extraneous CSS file.
Because styles is a JavaScript object, it must follow JavaScript object conventions or you will get an error at runtime. This means that simple selectors are fine. But selectors with special operators, such as :, ::, >, *, etc. will need to be quoted. Also simple properties are find, but if they are hyphenated, you will have to camel case them or quote them. And finally, the property values must always be quoted, unless you are providing a pixel value, in which case you can use just the numerical value. It will get converted to pixel values automatically.
So, here's how we might style our previous list component:
With this knowledge about components, you should be able to cover all your data rendering needs in your ChocolateChip-UI app. You can find out more about components by reading the documentation.
LifeCycle Methods
Components also have lifecycle methods. This allows you to execute code during different stages of a component. The lifecycle methods are fired during three different stages of a component: mounting, rendering and unmounting. In our above examples we did not mount our elements because there was no need to. Mounting in only necessary when you define your component as a separate file and import it as an ES6 module. Because the import happens before the DOM is ready, the component will be unable to find its root element, register its user actions or create its scoped stylesheet. When you import a component into your project's app.js file, you'll need to mount it before you can render it:
Unmounting a component removes its user actions and empties its root element of any rendered content. You would do this first if you wanted to delete the root element. Unmounting a component before deleting its root element will help prevent memory leaks.
Mounting, rendering and unmounting lets you use the following six methods:
Mounting:
- componentWillMount - Use this when importing and mounting a component with mount(). It will execute before the component is mounted.
- componentDidMount - use this when importing and mounting a component with mount(). It will execute after the component is mounted.
Unmounting:
- componentWillUnmount - use this when running unmount() on a component. It will execute before the component is unmounted.
- componentDidUnmount - use this when running unmounted() on a component. It will execute after the component is unmounted.
Rendering:
- componentWillUpdate - use this when running render() on a component. It will execute before the component is rendered.
- componentDidUpdate - use this when running render() on a component. It will execute after the component is rendered.
Scenarios for Lifecycle Use
You might use componentWillMount to fetch data from a local or remote repository. You might use componentDidUpdate to start the rendering of another component, particularly if it is a child of the first. Here is an example:
Inspiration
The inspiration for ChocolateChip-UI, particularly the component architecture, was from Elm, Vue and React. At the same time, ChocolateChip-UI's components are very different from these. Components in ChocolateChip-UI embrace the concept of separation of concerns. Id does this in the following way:
- A component's render function to only be about rendering data. It should not contain logic, events or styles.
- Logic and be implemented in a block scope before returning the render template.
- Events are all corralled into the component's actions.
- Styles are never inline. Instead you create a scoped stylesheet using the components styles property.
This means when you look at a component render function template, you only see how it renders data to the document. When you look at the component actions, you see how to wire up user interactions. When you look at the component styles, you see how the component-specific styles are implemented.