Skip to content

Commit

Permalink
feat: initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
infodusha committed Aug 23, 2023
1 parent 1697253 commit 72180ad
Show file tree
Hide file tree
Showing 10 changed files with 293 additions and 0 deletions.
33 changes: 33 additions & 0 deletions .github/workflows/merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Merge
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- uses: google-github-actions/release-please-action@v3
id: release
with:
release-type: node
pull-request-title-pattern: 'chore: release ${version}'
- uses: actions/checkout@v3
if: ${{ steps.release.outputs.release_created }}
- uses: actions/setup-node@v3
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
if: ${{ steps.release.outputs.release_created }}
- run: npm install -g npm
if: ${{ steps.release.outputs.release_created }}
- run: npm ci
if: ${{ steps.release.outputs.release_created }}
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
if: ${{ steps.release.outputs.release_created }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "1.0.0"
}
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,46 @@
# define-html

Define custom element to import in html

# Usage

Add link to preload external html file and define-html script
```html
...
<link rel="preload" href="content.html" as="fetch" crossorigin />
<script src="https://unpkg.com/define-html" type="module"></script>
</head>
```
Where `content.html` is
```html
<template data-selector="app-content">
Lorem ipsum
</template>

<style>
:host {
display: block;
}
</style>
```
So later you can use include your template with
```html
<app-content></app-content>
```

# Features

* Read attribute values
* Make conditional elements
* Optionally enable shadow root
* Partial style encapsulation (when not a shadow root mode)

# TODO

* Watch for attribute changes (from js)
* Full style encapsulation (when not a shadow root mode)
* Full slot support

# License

Apache-2.0
9 changes: 9 additions & 0 deletions example/content.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template data-selector="app-content">
Lorem ipsum
</template>

<style>
:host {
display: block;
}
</style>
23 changes: 23 additions & 0 deletions example/header.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<template data-selector="app-header" data-shadow>
<header>
My name is: <b data-attr="name"></b>
<span data-if="admin">
I AM THE ADMIN
</span>
<span data-if="admin" data-if-not>
I am the user
</span>
<slot></slot>
</header>
</template>

<style>
:host {
display: inline-block;
}

header {
background-color: wheat;
padding: 1rem;
}
</style>
18 changes: 18 additions & 0 deletions example/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>define-html example</title>
<link rel="preload" href="header.html" as="fetch" crossorigin />
<link rel="preload" href="content.html" as="fetch" crossorigin />
<script src="../index.js" type="module"></script>
</head>
<body>
<header>
<h1>Hello there</h1>
</header>
<app-header name="Andrei" admin>extra data</app-header>
<app-header name="Vika"></app-header>
<app-content></app-content>
</body>
</html>
120 changes: 120 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
const parser = new DOMParser();
const ignoreDataAttribute = 'data-define-html-ignore';

document.addEventListener('DOMContentLoaded', () => {
fetchFromLinks(document).catch(console.error);
});

async function fetchFromLinks(element) {
const links = Array.from(element.querySelectorAll(`link[rel='preload'][as='fetch'][href$='.html']:not([${ignoreDataAttribute}])`));
await Promise.all(links.map(defineHtml));
}

async function defineHtml(link) {
const href = link.getAttribute('href');
const response = await fetch(href);
const text = await response.text();
const definedElement = parser.parseFromString(text, 'text/html');
createCustomElement(definedElement);
}

function createCustomElement(definedElement) {
const template = definedElement.querySelector('template');
const useShadow = template.hasAttribute('data-shadow');
const selector = template.getAttribute('data-selector');
const styles = definedElement.querySelectorAll('style');

function setEmulatedStyles() {
for (const style of styles) {
const cssRules = [];

for (const rule of style.sheet.cssRules) {
rule.selectorText = rule.selectorText.replace(/:host/g, selector);
rule.selectorText = rule.selectorText.replace(/:host\((.+)\)/g, `${selector}$1`);
const re = new RegExp(`^(?!${selector})(.+?)\\s*`,'g');
rule.selectorText = rule.selectorText.replace(re, `${selector} $1`);
cssRules.push(rule);
}

const element = style.cloneNode(true);
document.head.appendChild(element);

while (element.sheet.cssRules.length !== 0) {
element.sheet.deleteRule(0);
}
for (const rule of cssRules) {
element.sheet.insertRule(rule.cssText);
}
}
}

if(!useShadow) {
setEmulatedStyles();
}

class DefineHTMLElement extends HTMLElement {
constructor() {
super();
const content = this.#getContent();
this.#attach(content);
this.#setAttrs();
if (useShadow) {
this.#setShadowStyles();
}
}

#getContent() {
const content = template.content.cloneNode(true);
for (const element of content.querySelectorAll(`[data-if]`)) {
const hasIfNot = element.hasAttribute('data-if-not');
const name = element.getAttribute('data-if');
const hasAttr = this.hasAttribute(name);
if (hasIfNot ? hasAttr : !hasAttr) {
element.remove();
}
}
return content;
}

#attach(content) {
if (useShadow) {
const shadowRoot = this.attachShadow({ mode: 'closed' });
shadowRoot.appendChild(content);
} else {
const slotElements = content.querySelectorAll('slot');
if (slotElements.length === 0 && this.childNodes.length > 0) {
throw new Error(`No slot found for ${selector}`)
}
// TODO probably handle full slot support
for (const element of slotElements) {
if (this.childNodes.length > 0) {
element.before(...this.childNodes);
}
element.remove();
}
this.appendChild(content);
}
}

#setAttrs() {
const root = useShadow ? this.shadowRoot : this;
for (const { name, value } of this.attributes) {
for (const element of root.querySelectorAll(`[data-attr='${name}']`)) {
if (element.childNodes) {
// TODO handle case when there are already nodes inside
}
element.innerText = value;
}
}
}

#setShadowStyles() {
for (const style of styles) {
const element = style.cloneNode(true);
this.shadowRoot.appendChild(element);
}
}
}

customElements.define(selector, DefineHTMLElement);
}
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "define-html",
"version": "1.0.0",
"description": "Define custom element to import in html",
"main": "index.js",
"type": "module",
"scripts": {
"example": "npx http-server --cors"
},
"repository": {
"type": "git",
"url": "git+https://github.com/infodusha/define-html.git"
},
"keywords": [
"html",
"define",
"import",
"include"
],
"files": [
"index.js",
"package.json",
"LICENSE",
"README.md",
"CHANGELOG.md"
],
"author": "infodusha",
"license": "Apache-2.0"
}

0 comments on commit 72180ad

Please sign in to comment.