├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── src
└── element-f.js
└── webpack.config.js
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release NPM Package
2 | on:
3 | release:
4 | types: [published]
5 |
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v2
11 | - uses: actions/setup-node@v2
12 | with:
13 | node-version: '14'
14 | - run: npm install
15 | - run: npm run build
16 | - uses: JS-DevTools/npm-publish@v1
17 | with:
18 | token: ${{ secrets.NPM_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Tal Weinfeld
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # element-f
2 | Define your custom elements with elegance 👒
3 |
4 | ### Installation
5 |
6 | ```
7 | npm i element-f
8 | ```
9 |
10 | ### Basics
11 | In order to define a custom-element, you only need one definition function:
12 |
13 | ```javascript
14 | import elementF from "element-f";
15 |
16 | const MyElement = elementF(function(){
17 |
18 | // Your logic goes here
19 | const shadow = this.attachShadow({mode: 'open'});
20 |
21 | });
22 | ```
23 |
24 | To tap into lifecycle events, this function can observe the "life" event emitter, passed to it as an argument:
25 | ```javascript
26 | const MyElement = elementF(function(life)=> {
27 |
28 | const shadow = this.attachShadow({mode: 'open'});
29 |
30 | // Listen once to when this component connects to a document
31 | life.once('connect', ()=> shadow.innerHTML = `I'm Alive!`);
32 |
33 | });
34 | ```
35 |
36 | The "life" event emitter supports three methods:
37 | * **`once(name, fn)`
`on(name, fn)`** - Registers **`fn`** for events of name **`name`**. **`once()`** will invoke **fn** once.
38 | * **`name`** - The name of the event to listen to
39 | * **`fn(payload)`** - The function to be called when an event occurs
40 | * **`payload`** - An object containing information regarding the event
41 | * **`off(name, fn)`** - Removes an event handler previously registered using **on** or **once**.
42 |
43 | The following events are thrown:
44 | * **`connect`** - Fired upon `connectedCallback`. Delivers no payload.
45 | * **`disconnect`** - Fired upon `disconnectedCallback`. Delivers no payload.
46 | * **`attribute:[Attribute Name]`** - Fired when an observed attribute changes. Delivers **previousValue** and **newValue** as payload.
47 |
48 | To observe attributes, just add their list to `elementF` call:
49 | ```javascript
50 | const MyElement = elementF(function(life) {
51 |
52 | life.on('attribute:foo', ({ previousValue, newValue })=> {
53 | // Do something when attribute "foo" changes value
54 | });
55 |
56 | life.on('attribute:bar', ({ previousValue, newValue })=> {
57 | // Do something when attribute "bar" changes value
58 | });
59 |
60 | }, ["foo", "bar"]);
61 | ```
62 |
63 | #### Usage Examples
64 | Whereas defining custom elements using standard class notation looks like this:
65 |
66 | ```javascript
67 | class MyButton extends HTMLElement {
68 |
69 | constructor(){
70 | super();
71 | console.log(`I'm alive!`);
72 | }
73 |
74 | static get observedAttributes(){
75 | return ['disabled'];
76 | }
77 |
78 | attributeChangedCallback(name, oldValue, newValue) {
79 | if(name === "disabled") this.classList.toggle('disabled', newValue);
80 | }
81 |
82 | connectCallback() {
83 | this.innerHTML = "I'm an x-foo-with-markup!";
84 | }
85 | }
86 | ```
87 |
88 | With **element-f** the same custom element definition would look like this:
89 |
90 | ```javascript
91 | const MyButton = elementF(function(life){
92 |
93 | life.on('connect', ()=> this.innerHTML = "I'm an x-foo-with-markup!");
94 |
95 | life.on('attribute:disabled', ({ newValue, oldValue })=> this.classList.toggle('disabled', newValue));
96 |
97 | console.log(`I'm alive!`);
98 |
99 | }, ['disabled']);
100 | ```
101 |
102 | Compact, functional and elegant 😇
103 |
104 | ### What does Element-F solve?
105 |
106 | **Element-F** is a stylistic framework, not a fundamental solution to any specific architectural or functional problem. If you're happy with OOP-styled constructs, you probably wouldn't draw much enjoyment from using it :)
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "element-f",
3 | "version": "2.0.0",
4 | "description": "Define your custom elements with elegance",
5 | "main": "./dist/umd.min.js",
6 | "module": "./src/element-f.js",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "build": "webpack"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/tweinfeld/element-f.git"
14 | },
15 | "files": [
16 | "README.md",
17 | "LICENSE",
18 | "dist/umd.min.js",
19 | "src/element-f.js"
20 | ],
21 | "keywords": [
22 | "Custom Elements",
23 | "Functional",
24 | "Shim",
25 | "Wrapper"
26 | ],
27 | "author": "Tal Weinfeld",
28 | "license": "MIT",
29 | "bugs": {
30 | "url": "https://github.com/tweinfeld/element-f/issues"
31 | },
32 | "homepage": "https://github.com/tweinfeld/element-f#readme",
33 | "devDependencies": {
34 | "webpack": "^5.10.1",
35 | "webpack-cli": "^4.2.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/element-f.js:
--------------------------------------------------------------------------------
1 | export default function (generator, attributes = []) {
2 | const
3 | BUS = Symbol('bus'),
4 | Element = class extends HTMLElement {
5 | constructor() {
6 | super();
7 | const emitter = (function(){
8 | const
9 | eventMap = new Map(),
10 | onceMap = new WeakMap();
11 |
12 | this[BUS] = function ({type, ...payload}) {
13 | [...(eventMap.get(type) || new Set()).values()].forEach((handler) => handler(payload));
14 | };
15 |
16 | return {
17 | on(type, handler) {
18 | eventMap.set(type, (eventMap.get(type) || new Set()).add(handler));
19 | },
20 | off(type, handler) {
21 | (eventMap.get(type) || new Set()).delete(onceMap.get(handler) ?? handler);
22 | },
23 | once(type, handler) {
24 | let wrappedHandler = (...props)=> {
25 | handler(...props);
26 | this.off(type, wrappedHandler);
27 | };
28 | onceMap.set(handler, wrappedHandler);
29 | this.on(type, wrappedHandler);
30 | }
31 | };
32 | }).call(this);
33 |
34 | generator.call(this, emitter);
35 | }
36 |
37 | attributeChangedCallback(attributeName, previousValue, newValue) {
38 | this[BUS]({type: ["attribute", attributeName].join(':'), newValue, previousValue});
39 | }
40 |
41 | connectedCallback() {
42 | this[BUS]({type: "connect"});
43 | }
44 |
45 | disconnectedCallback() {
46 | this[BUS]({type: "disconnect"});
47 | }
48 | };
49 |
50 | Object.defineProperty(Element, 'observedAttributes', {
51 | configurable: false,
52 | enumerable: false,
53 | get: ()=> attributes
54 | });
55 |
56 | return Element;
57 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | "mode": "production",
5 | "entry": "./src/element-f.js",
6 | "output": {
7 | path: path.join(__dirname, "./dist"),
8 | filename: "umd.min.js",
9 | libraryTarget: "umd",
10 | libraryExport: "default",
11 | library: "ElementF"
12 | }
13 | }
--------------------------------------------------------------------------------