Skip to content

Commit 26f9f14

Browse files
authored
Merge pull request #37 from alleyinteractive/feature/component-provider
v3 Feature: Component provider
2 parents e2caea4 + fa14c1f commit 26f9f14

30 files changed

+12472
-5239
lines changed

.babelrc.json

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
{
2+
"presets": [
3+
"@babel/preset-env",
4+
],
25
"env": {
36
"es": {
47
"plugins": ["@babel/plugin-transform-runtime"],
@@ -13,11 +16,6 @@
1316
},
1417
"cjs": {
1518
"plugins": ["@babel/plugin-transform-runtime"],
16-
"presets": [
17-
[
18-
"@babel/preset-env"
19-
]
20-
]
2119
}
2220
}
2321
}

.eslintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"env": {
33
"browser": true,
44
"es2021": true,
5-
"node": true
5+
"node": true,
6+
"jest": true
67
},
78
"extends": ["airbnb"],
89
"parser": "@babel/eslint-parser",

.github/workflows/node-tests.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: Node Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
node-tests:
13+
uses: alleyinteractive/.github/.github/workflows/node-tests.yml@main
14+
with:
15+
run-build: false

.npmignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
/src/
2-
/examples/

CHANGELOG.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Change Log
2+
This project adheres to [Semantic Versioning](http://semver.org/).
3+
4+
## 3.0.0
5+
6+
**Added**
7+
8+
* Adds `componentProvider` for initializing and loading components
9+
* Adds `componentLoader` for optionally loading the function returned from `componentProvider`
10+
11+
**Changed**
12+
13+
* Simplifies component creation by allowing components to be a class or a function
14+
* No longer writes to `window[manifest]`
15+
* Replaces `ComponentManager` with `initComponents` for initializing legacy components using the `/v2` export
16+
* Moves `Component` to the `/v2` export
17+
18+
**Fixed**
19+
20+
* Moves `@babel/runtime` to devDependencies
21+
22+
**Removed**
23+
24+
* Removes the rate limiter
25+
* Removes Aria plugins; consider using [aria-components](https://www.npmjs.com/package/aria-components) instead.
26+
* Removes the `/core` export due to code restructuring
27+
28+
## 2.1.0
29+
30+
Adds an ES Module export to reduce bundle sizes
31+
32+
## 2.0.0
33+
34+
**Changed**
35+
36+
* Reduce the size of the framework by adding a core/ directory which does not include the Aria plugins
37+
* Updates babel configs and plugins
38+
* Updates ESLint config and include Airbnb standards
39+
40+
**Removed**
41+
42+
* Polyfills and support for IE
43+
44+
## 1.2.6
45+
46+
Automated security updates
47+
48+
## 1.2.5
49+
50+
**Changed**
51+
52+
* Moved rate limiter to standalone function.
53+
54+
**Removed**
55+
56+
* Bottleneck library
57+
58+
## 1.2.4
59+
60+
Automated security updates
61+
62+
## 1.2.2
63+
64+
**Fixed**
65+
66+
* Bottleneck updates caused missing polyfills and non-transpiled code
67+
68+
**Changed**
69+
70+
* Updates Bottleneck package to latest
71+
72+
**Added**
73+
74+
* Babel polyfills and plugins relative to bottleneck package update
75+
* Browserlist settings to package.json
76+
77+
## 1.2.1
78+
79+
**Fixed**
80+
81+
Bugs related to the hasTransition option for AriaPlugin
82+
83+
## 1.2.0
84+
85+
Updates to Aria classes.
86+
87+
**Fixed**
88+
89+
* Prevents arrow keys from activating out-of-bounds tabs
90+
91+
**Added**
92+
93+
* hasTransition option to delay focus, etc. until after transition
94+
* Target and controller now have a reference to the parent popup
95+
* loadOpen config option
96+
97+
**Changed**
98+
99+
* Normalizes Aria event names
100+
* Refactors AriaTablist setup to verify markup
101+
102+
**Removed**
103+
104+
* Removes breakpoint option and related code
105+
* Event listeners on ariaDestroy
106+
* First item focus for popups
107+
108+
## 1.0.3
109+
110+
**Added**
111+
112+
Aria plugins.
113+
114+
## 1.0.1
115+
116+
Documentation updates.
117+
118+
## 1.0.0
119+
120+
Initial release.

README.md

Lines changed: 106 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,147 @@
1-
# Component Framework
1+
JS Component Framework
2+
======================
23

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.
45

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.
67

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>
812

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.
1114

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>
1319

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).
1821

1922
## Getting Started
2023

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+
2226
```bash
2327
npm install js-component-framework
2428
```
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.
2669

2770
```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 }) { ... }
4372
```
4473

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+
}
4882
```
4983

50-
## Best practices for creating components
84+
## Loading Components
5185

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.
5387

54-
It will contain the configuration file and the class.
88+
### componentProvider
5589

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.
5791

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.
5993

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**
6595

66-
[See the Header example](./examples/Header/index.js) for context.
96+
**config** _(Required)_ A component config object.
6797

68-
### The component class
98+
```javascript
99+
import { componentProvider } from 'js-component-framework';
100+
101+
function productDetails({ element, children, options }) { ... }
69102

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+
};
71111

72-
```js
73-
import { Component } from 'js-component-framework';
112+
componentProvider(productDetailsConfig);
74113
```
75114

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';
79119
```
80120

81-
When writing a component class, are some rules you need to follow and some guidelines:
121+
### componentLoader
82122

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.
92124

93-
[See the Header example](./examples/Header/Header.js) for context.
125+
**Parameters**
94126

95-
## How to instantiate components
127+
**providerFunction**: _(Required)_ A function returned from `componentProvider`.
96128

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_.
98130

99-
Generally speaking, you'll want to instantiate your component in the footer or on `DOMContentLoaded`.
131+
```javascript
132+
// my-component/my-component.js
100133

101-
```js
134+
const wrappedComponent = componentProvider(config); // config.load === false
135+
export default wrappedComponent;
136+
```
102137

103-
import { ComponentManager } from `js-component-framework/es`;
104-
import headerConfig from `./Components/Header`;
138+
```javascript
139+
// my-component/index.js
105140

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';
109143

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']);
116147
```
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

Comments
 (0)