{"id":21365457,"url":"https://github.com/frameable/el","last_synced_at":"2025-04-07T17:08:28.706Z","repository":{"id":45389849,"uuid":"442885772","full_name":"frameable/el","owner":"frameable","description":"Minimal JavaScript application framework / WebComponents base class","archived":false,"fork":false,"pushed_at":"2023-11-28T15:55:01.000Z","size":53,"stargazers_count":272,"open_issues_count":1,"forks_count":10,"subscribers_count":8,"default_branch":"main","last_synced_at":"2025-03-31T16:15:05.751Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/frameable.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-12-29T20:42:38.000Z","updated_at":"2025-03-30T00:48:51.000Z","dependencies_parsed_at":"2023-11-28T17:12:16.250Z","dependency_job_id":null,"html_url":"https://github.com/frameable/el","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frameable%2Fel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frameable%2Fel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frameable%2Fel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frameable%2Fel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/frameable","download_url":"https://codeload.github.com/frameable/el/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247694876,"owners_count":20980733,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-22T07:11:17.393Z","updated_at":"2025-04-07T17:08:28.673Z","avatar_url":"https://github.com/frameable.png","language":"JavaScript","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"readme":"# El\n\nMinimal JavaScript application framework inspired by React, Vue, and lit-element. See a working todo list [example](https://frameable.github.io/el/example.html) and [source](https://github.com/frameable/el/blob/main/example.html)\n\n\n### Introduction\n\nEl is based on [Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components), and provides a friendly interface to these features:\n\n- Built-in observable store\n- Reactive templates with one-way binding\n- Fast differential DOM updates\n- Scoped styles via shadow DOM\n- CSS preprocessing for implicit nesting and ampersands\n- Watch expressions\n- Component lifecycle methods\n- Just ~150 lines of source code (~2kb gzipped)\n- Minimal surface area with easy learning curve\n- No dependencies on any other libraries\n- No need for build tools like Webpack or Rollup\n\n```html\n\u003cmy-counter\u003e\u003c/my-counter\u003e\n\n\u003cscript\u003e\n  class MyCounter extends El {\n    created() {\n      this.state = this.$observable({ count: 0 });\n    }\n    increment() {\n      this.state.count += 1;\n    }\n    render(html) {\n      return html`\n        \u003cspan\u003eCount: ${this.state.count}\u003c/span\u003e\n        \u003cbutton onclick=${this.increment}\u003eIncrement\u003c/button\u003e\n      `\n    }\n  }\n  customElements.define('my-counter', MyCounter);\n\u003c/script\u003e\n```\n\n## Installation\n\nEl consists of a single source JavaScript file, [`el.js`](https://raw.githubusercontent.com/frameable/el/main/el.js). You can drop it into your project directly, and import:\n\n```html\n\u003cscript type=\"module\"\u003e\n  import { El } from './el.js'\n  /* ... */\n\u003c/script\u003e\n```\n\nOr else install from the npm registry:\n\n```\nnpm install @frameable/el\n```\n\n\n## Components\n\nEl serves as a base class for custom elements / Web Components.  Inherit from `El` and then register with `customElements.define`:\n\n```html\n\u003cmy-element\u003e\u003c/my-element\u003e\n\n\u003cscript\u003e\n  class MyElement extends El {\n    /* ... */\n  }\n  customElements.define(MyElement, 'my-element');\n\u003c/script\u003e\n```\n\nIf you are new to custom elements, some tips per the spec:\n\n- Element tag names must be lowercase with at least one hyphen\n- `customElements.define` takes the given class and registers with the tag name\n- In the markup, custom elements cannot be self-closing\n\n#### Lifecycle methods\n\nIf lifecycle methods are defined on the component, they will fire at the appropriate time:\n\n- `created()` - componenent has been created but not yet mounted\n- `mounted()` - component has been attached to the DOM\n- `unmounted()` - component has been removed from the DOM\n\n## Observable\n\nUse `El.observable` to create an observable store which will allow components to update when the store changes.  El keeps track of which components depend on which parts of the store, and only performs the necessary updates.\n\n```javascript\nconst store = El.observable({ items: [] });\n```\n\nA component may wish to have its own observable state:\n\n```javascript\nclass TodoItem extends El {\n  created() {\n    this.state = this.$observable({\n      status: 'new'\n    });\n  }\n}\n```\n\nA component can also subscribe to changes with `$watch`.\n\n```javascript\nclass TodoItems extends El {\n  mounted() {\n    this.$watch(store.items.length, () =\u003e console.log(\"length changed!\"));\n  }\n}\n```\n\n\u003e Observable stores are implemented as recursive [proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy).  When a component is rendered, as properties are accessed from observable stores, El keeps track of which components are dependent on which properties.  When those properties later change, the components that depend on them are rendered again.\n\n## Templates\n\nTemplates are rendered through the `render` function, which accepts a `html` tag function.  Element attributes like class names and event handlers can be assigned expressions directly, or interpolated.\n\n```javascript\nclass TodoItem extends El {\n  render(html) {\n    return html`\n      \u003cdiv class=\"title ${this.done \u0026\u0026 'title--done'}\"\u003e\n        ${this.title}\n      \u003c/div\u003e\n      \u003cbutton onclick=${this.edit}\u003eEdit\u003c/button\u003e\n    `\n  }\n}\n```\n\n\u003e Component rendering templates are implemented using [tag functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates). When the `html` tag function comes across a value to be interpolated, if the value is a complex value like an array or object being passed as a property, or if the value is a function being assigned as an event handler, the tag function stashes the value and interpolates into the template a key that El uses later to refer back to the original complex value.\n\n#### Looping\n\nIterate through items with `map`, and make sure to add a unique `key` attribute:\n\n```javascript\nclass TodoItems extends El {\n  render(html) {\n    return html`\n      \u003cdiv class=\"todo-items\"\u003e\n        ${this.items.map, item =\u003e html`\n          \u003ctodo-item item=${item} key=${item.id}\u003e\u003c/todo-item\u003e\n        `}\n      \u003c/div\u003e\n    `\n  }\n}\n```\n\n#### Conditional logic\n\nWithin a render function, you can use short-circuit (`\u0026\u0026`) or ternary syntax (`condition ? then : else`).\n\n```javascript\nclass TodoItem extends El {\n  render(html) {\n    return html`\n      \u003cdiv class=\"title ${this.done \u0026\u0026 'title--done'}\"\u003e\n        ${this.title}\n      \u003c/div\u003e\n      ${this.editable\n        ? html`\u003cbutton onclick=${this.edit}\u003eEdit\u003c/button\u003e`\n        : html`\u003cspan\u003eArchived\u003c/span\u003e\n      `}\n    `\n  }\n}\n```\n\n\u003e Once a template is rendered to html, it then needs to find its way into the DOM.  El renders first to a [DocumentFragment](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment), then traverses the fragment and its corresponding component in the actual DOM, and selectively alters the real DOM only where the two structures diverge.\n\n#### Refs\n\nRefer to elements within a component by the name specified by their `ref` attribute.\n\n```javascript\nclass TodoItemDescription extends El {\n  save() {\n    store.setDescription(this.itemId, this.$refs.descriptionInput.value);\n  }\n  render(html) {\n    return html`\n      \u003cinput ref=\"descriptionInput\"\u003e\n      \u003cbutton onclick=${this.save}\u003eSave\u003c/button\u003e`\n    `\n  }\n}\n```\n\n\u003e Refs are implemented as a dynamic getter via Proxy.  When the property is read, the proxy handler runs a `querySelector` query on the component root, so these properties will yield \"live\" results but may not be ideal in a tight loop where performance is critical.\n\n\n#### Computed properties\n\nProperties accessed via getters will be computed just once per render cycle.  In the following example, the `dependents` property invokes the getter just once, even though it is referred to multiple times in template.\n\n```javascript\nclass TodoItem extends El {\n\n  get dependents() {\n    // expensive execution is cached per render\n    return store.items.filter(item =\u003e item.dependencies.includes(this.item.id));\n  }\n\n  render(html) {\n    return html`\n      \u003ch1\u003e${this.title}\u003c/h1\u003e\n      \u003ch4\u003e${this.dependents.length} dependent tasks\u003c/h4\u003e\n      \u003cul\u003e\n        ${this.dependents.map(d =\u003e html`\n          \u003cli\u003e${d.title}\u003c/li\u003e\n        `)}\n      \u003c/ul\u003e\n    `\n  }\n}\n```\n\n#### Escaping\n\nInterpolated values are HTML-escaped by default. If you have sanitized markup that you would like to include as-is, use `html.raw`:\n\n```javascript\nclass TodoItemDescription extends El {\n  render(html) {\n    return html`\n      \u003cdiv class=\"description\"\u003e\n        ${html.raw(this.sanitizedDescriptionHTML)}\n      \u003c/div\u003e\n    `\n  }\n}\n```\n\n\n## Style\n\nSpecify CSS via the `styles` method and `css` tag function. Styles are scoped so that they only apply to elements in this component.  Neither ancestors nor descendants of this component will be affected by these styles. The built-in preprocessor adds support for implicit nesting and ampersand selectors.\n\n```javascript\nclass TodoItem extends El {\n  styles(css) {\n    return css`\n      .item {\n        margin: 16px;\n        padding: 16px;\n\n        \u0026:hover {\n          background: whitesmoke;\n        }\n\n        .title {\n          font-weight: 500;\n        }\n      }\n    `\n  }\n  render(html) {\n    return html`\n      \u003cdiv class=\"item\"\u003e\n        \u003cdiv class=\"title ${this.done \u0026\u0026 'title--done'}\"\u003e\n          ${this.title}\n        \u003c/div\u003e\n      \u003c/item\u003e\n    `\n  }\n}\n```\n\n\u003e The shadow DOM provides scoped CSS so that styles defined within a component don't leak either up to parents or down to children.  By default, global styles will also not be applied within components, which is great when you're building abstract components to be used across projects, but a hinderance when you want different components within a single application to have consistent fonts, colors, spacing, etc.  El clones global styles and applies those styles to each component via `link` tag with a data URI, so components will be affected by application-wide stylesheets.\n\n\u003e El runs a stack-based line-by-line [zcss](https://github.com/dchester/zcss.js) source filter on CSS in order to implement nesting CSS and the ampersand selector, popularized by SCSS and other tools, now a W3C working draft [CSS Nesting](https://www.w3.org/TR/css-nesting-1/).\n\n\n## Resources\n\nEl Starter app template \\\nhttps://github.com/frameable/el-starter\n\nMDN on Web Components \\\nhttps://developer.mozilla.org/en-US/docs/Web/Web_Components\n\nEditor syntax highlighting \\\nhttps://github.com/jonsmithers/vim-html-template-literals (Vim) \\\nhttps://github.com/0x00000001A/es6-string-html (VS Code)\n\nzcss preprocessor \\\nhttps://github.com/dchester/zcss.js\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fframeable%2Fel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fframeable%2Fel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fframeable%2Fel/lists"}