├── .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 | } --------------------------------------------------------------------------------