|
1 | | -# Component Framework |
| 1 | +JS Component Framework |
| 2 | +====================== |
2 | 3 |
|
3 | | -A framework for attaching an ES6 class to a DOM element or collection of DOM elements, making it easier to organize the DOM interactions on your website. |
| 4 | +A zero-dependency framework for configuring a JavaScript component and attaching it to a DOM element or collection of DOM elements, simplifying organization of DOM interactions on your website. |
4 | 5 |
|
5 | | -## How it works |
| 6 | +Components can be ES6 classes or simple functions, and their child nodes are collected automatically based on the component configuration and passed to the component, which reduces or removes the need to write DOM queries for each component. |
6 | 7 |
|
7 | | -A high-level overview on how it works. You ... |
| 8 | +<picture> |
| 9 | + <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Mqxx/GitHub-Markdown/main/blockquotes/badge/light-theme/warning.svg"> |
| 10 | + <img alt="Warning" src="https://raw.githubusercontent.com/Mqxx/GitHub-Markdown/main/blockquotes/badge/dark-theme/warning.svg"> |
| 11 | +</picture><br> |
8 | 12 |
|
9 | | -* provide a configuration and an ES6 class (which extends the base Component class). |
10 | | -* create a ComponentManager and call its `instanceComponents` method, passing it one or more component configurations created in the previous step. |
| 13 | +Version 3.0.0 contains breaking changes. See [v2 documentation](src/v2/) for backward compatibility. |
11 | 14 |
|
12 | | -The library will ... |
| 15 | +<picture> |
| 16 | + <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Mqxx/GitHub-Markdown/main/blockquotes/badge/light-theme/info.svg"> |
| 17 | + <img alt="Info" src="https://raw.githubusercontent.com/Mqxx/GitHub-Markdown/main/blockquotes/badge/dark-theme/info.svg"> |
| 18 | +</picture><br> |
13 | 19 |
|
14 | | -* loop through every element match it finds for `data-component={componentName}` in your configuration and start an instance of the component class. |
15 | | -* add each instance of the component to a global manifest on the `window` object, using a property provided when you instanced the manager. |
16 | | - |
17 | | -This results in distinct (and encapsulated) functionality for each DOM element. |
| 20 | +**Coming from a previous version?** Find full upgrade documentation in [the Wiki](https://github.com/alleyinteractive/js-component-framework/wiki/Updating-to-v3). |
18 | 21 |
|
19 | 22 | ## Getting Started |
20 | 23 |
|
21 | | -Install the js-component-framework and all the plugins: |
| 24 | +Install js-component-framework from [NPM](https://www.npmjs.com/package/js-component-framework). |
| 25 | + |
22 | 26 | ```bash |
23 | 27 | npm install js-component-framework |
24 | 28 | ``` |
25 | | -Below is a basic set up for using the component framework without the included Aria plugins: |
| 29 | + |
| 30 | +## Creating a Component |
| 31 | + |
| 32 | +Component elements are denoted by a `data-component` attribute, the value of which is used to match the component to its element(s). |
| 33 | + |
| 34 | +``` |
| 35 | +<header data-component="site-header">...</header> |
| 36 | +``` |
| 37 | + |
| 38 | +### The Configuration Object |
| 39 | + |
| 40 | +**name**: _(Required)_ - The component name. This must match the component root element's `data-component` attribute value. |
| 41 | + |
| 42 | +**component**: _(Required)_ - A component can be created as an ES6 class or a function. This property accepts the exported class or function to be initialized for the component. |
| 43 | + |
| 44 | +**querySelector**: _(Optional)_ - An object mapping of `name: selector` pairs matching a single child element of the component. Each selector is passed to `querySelector()` and the result is passed to as component's `children` property. For example, if you provide `{ title: '.site-title' }`, the element will be accessible in your component as `children.title`. |
| 45 | + |
| 46 | +**querySelectorAll**: _(Optional)_ - Same as `querySelector`, but each selector is passed to `querySelectorAll()` and returned as an array of elements for each selector. |
| 47 | + |
| 48 | +**options**: _(Optional)_ - An arbitrary value, typically an object, used by the component. This could be a configuration for another JS library, values used for calculating styles, etc. This is passed to the wrapped function as the `options` property. |
| 49 | + |
| 50 | +**load**: _(Optional)_ - Accepts `false`, array, or a callback. _Default is a `domContentLoaded` callback_. |
| 51 | + |
| 52 | +* `false` will disable loading and instruct `componentProvider` to return the provider function |
| 53 | +* An array, in the format of `[HTMLElement, event]`, will listen on `HTMLElement` for `event` (e.g., `[window, 'load']`) |
| 54 | +* A callback that accepts a function and contains the logic to call the function |
| 55 | + |
| 56 | +### Component Properties |
| 57 | + |
| 58 | +Components receive an object of component properties as their only argument. These are based on the config and are included automatically by the framework. |
| 59 | + |
| 60 | +**element**: the `data-component` element to which the component is attached |
| 61 | + |
| 62 | +**children**: the component’s child elements as described in `config.querySelector` and `config.querySelectorAll` |
| 63 | + |
| 64 | +**options**: the `config.options` value, if any |
| 65 | + |
| 66 | +#### Functional Components |
| 67 | + |
| 68 | +Functional components receive the object of component properties as their only parameter. |
26 | 69 |
|
27 | 70 | ```javascript |
28 | | -// Import the ES modules to be consumed by a bundler such as webpack append the '/es' to the end of the import statement. |
29 | | -import { Component } from 'js-component-framework/es'; |
30 | | - |
31 | | -/** |
32 | | - * Custom component which extends the base component class. |
33 | | - */ |
34 | | -class MyComponent extends Component { |
35 | | - |
36 | | - /** |
37 | | - * Start the component |
38 | | - */ |
39 | | - constructor(config) { |
40 | | - super(config); |
41 | | - } |
42 | | -} |
| 71 | +function myComponent({ element, children, options }) { ... } |
43 | 72 | ``` |
44 | 73 |
|
45 | | -If you also want to use the entire framework using the CommonJS bundled scripts with Aria plugins use the default import: |
46 | | -```js |
47 | | -import { Component, plugins } from 'js-component-framework'; |
| 74 | +#### Classical Components |
| 75 | + |
| 76 | +For classical components, the object of component properties is passed as the constructor's only parameter. |
| 77 | + |
| 78 | +```javascript |
| 79 | +class MyComponent { |
| 80 | + constructor({ element, children, options }) { ... } |
| 81 | +} |
48 | 82 | ``` |
49 | 83 |
|
50 | | -## Best practices for creating components |
| 84 | +## Loading Components |
51 | 85 |
|
52 | | -### Create one directory per component |
| 86 | +Due to the nature of the component framework, components must be prepared for loading prior to initialization. By default, the function that performs this preparation will also load the component. |
53 | 87 |
|
54 | | -It will contain the configuration file and the class. |
| 88 | +### componentProvider |
55 | 89 |
|
56 | | -### The configuration file |
| 90 | +`componentProvider` creates a provider function that contains all the properties and DOM querying logic necessary for all instances of the component to be initialized. |
57 | 91 |
|
58 | | -Use the convention `index.js`, as this will be the default export for the component. The configuration requires several properties: |
| 92 | +The component will automatically be initialized according to the `config.load` value. If `config.load` is `false`, the provider function is returned. |
59 | 93 |
|
60 | | -* `name` - *required* - **THIS _MUST_ BE UNIQUE**. An arbitrary name for the component. This is used to find component elements via data attribute `data-component="componentName"`. `name` is also used to store instances of the component in the global manifest. |
61 | | -* `class` - *required* - The imported ES6 class for the component. |
62 | | -* `querySelector` - *optional* - An object containing child selectors you'll need for the component logic. Each of these selectors should correspond to an element of which there is only one within the component (and will be queried using the `querySelector` JS method). The base Component class will automatically query these selectors and add them as properties to the class (using the provided object keys). For example, if you provide `title: '.site-title'`, this will be accessible in your component class as `this.children.title`. |
63 | | -* `querySelectorAll` - *optional* - Same as `querySelector`, but will provide an array of each element it finds matching the selector. |
64 | | -* `options` - *optional* - An object containing arbitrary additional options for the component. This could be a configuration for another JS library, values used for calculating styles, etc. |
| 94 | +**Parameters** |
65 | 95 |
|
66 | | -[See the Header example](./examples/Header/index.js) for context. |
| 96 | +**config** _(Required)_ A component config object. |
67 | 97 |
|
68 | | -### The component class |
| 98 | +```javascript |
| 99 | +import { componentProvider } from 'js-component-framework'; |
| 100 | + |
| 101 | +function productDetails({ element, children, options }) { ... } |
69 | 102 |
|
70 | | -To create a component, do the following at the top of the file: |
| 103 | +const productDetailsConfig = { |
| 104 | + name: 'product-details', |
| 105 | + component: productDetails, |
| 106 | + // load: false | array | function |
| 107 | + querySelectorAll: { |
| 108 | + toggles: '.product-details__toggle', |
| 109 | + }, |
| 110 | +}; |
71 | 111 |
|
72 | | -```js |
73 | | -import { Component } from 'js-component-framework'; |
| 112 | +componentProvider(productDetailsConfig); |
74 | 113 | ``` |
75 | 114 |
|
76 | | -If you are compiling your code with a bundler like webpack, only import what you need for a smaller footprint: |
77 | | -```js |
78 | | -import { Component } from 'js-component-framework/es'; |
| 115 | +When using a bundler like webpack, import the ES module for a smaller footprint: |
| 116 | + |
| 117 | +```javascript |
| 118 | +import { componentProvider } from 'js-component-framework/es'; |
79 | 119 | ``` |
80 | 120 |
|
81 | | -When writing a component class, are some rules you need to follow and some guidelines: |
| 121 | +### componentLoader |
82 | 122 |
|
83 | | -* You _must_ provide a constructor. At minimum, the constructor should look like the following: |
84 | | - ```js |
85 | | - constructor(config) { |
86 | | - super(config); |
87 | | - } |
88 | | - ``` |
89 | | - This constructor will be called when ComponentManager instances your component, your configuration passed in, then passed to the parent Component class using the `super()` function. |
90 | | -* Don't perform much logic within the class constructor. Instead, separate logic into distinct methods, each for a specific purpose. Use the constructor to call these methods and initialize any runtime component logic, event listeners, calculations, etc. |
91 | | -* Any method you might call from another component or use as a callback for an event listener should be bound to the component class using `this.myMethod = this.myMethod.bind(this)`. Methods can be called from other components using the component manager via `manager.callComponentMethod`. See the ComponentManager class for documentation. |
| 123 | +`componentLoader` is used by `componentProvider` to load components. It is exported individually for cases where one doesn't want `componentProvider` to load the provider function automatically. |
92 | 124 |
|
93 | | -[See the Header example](./examples/Header/Header.js) for context. |
| 125 | +**Parameters** |
94 | 126 |
|
95 | | -## How to instantiate components |
| 127 | +**providerFunction**: _(Required)_ A function returned from `componentProvider`. |
96 | 128 |
|
97 | | -Import the component config in one of your entry point files, and pass that config to the ComponentManager's instanceComponents method. |
| 129 | +**load**: _(Optional)_ A valid `config.load` value. _Default is a `domContentLoaded` callback_. |
98 | 130 |
|
99 | | -Generally speaking, you'll want to instantiate your component in the footer or on `DOMContentLoaded`. |
| 131 | +```javascript |
| 132 | +// my-component/my-component.js |
100 | 133 |
|
101 | | -```js |
| 134 | +const wrappedComponent = componentProvider(config); // config.load === false |
| 135 | +export default wrappedComponent; |
| 136 | +``` |
102 | 137 |
|
103 | | -import { ComponentManager } from `js-component-framework/es`; |
104 | | -import headerConfig from `./Components/Header`; |
| 138 | +```javascript |
| 139 | +// my-component/index.js |
105 | 140 |
|
106 | | -// Instantiate manager |
107 | | -// "namespace" can be any string to namespace this instance of the manager |
108 | | -const manager = new ComponentManager('namespace'); |
| 141 | +import { componentLoader } from 'js-component-framework'; |
| 142 | +import wrappedComponent from './my-component'; |
109 | 143 |
|
110 | | -// Create component instances |
111 | | -document.addEventListener('DOMContentLoaded', () => { |
112 | | - manager.initComponents([ |
113 | | - headerConfig |
114 | | - ]); |
115 | | -}); |
| 144 | +// This is a contrived example; in a real-world component this same outcome can |
| 145 | +// be accomplished simply by setting the config load value to `[window, 'load']` |
| 146 | +componentLoader(wrappedComponent, [window, 'load']); |
116 | 147 | ``` |
117 | | - |
118 | | -Be sure you've added the appropriate data attribute containing your configured `name` to the element(s) on which you want this component to be attached. For example, add `data-component="siteHeader"` to instance [the Header component](./examples/Header/Header.js), assuming you configured the Header component `name` to be `siteHeader`. |
|
0 commit comments