Kliensoldali webprogramozás
Horváth Győző
Egyetemi docens
1117 Budapest, Pázmány Péter sétány 1/c., 2.408-as szoba
Tel: (1) 372-2500/8469
horvath.gyozo@inf.elte.hu
<!-- index.html -->
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js"></script>
<form>
<input type="password" name="password1" value="secret1">
<input type="password" name="password2" value="secret2">
</form>
<style>
/* ... */
</style>
<form>
<div class="show-password">
<input type="password" name="password1" value="secret1">
<button type="button"></button>
</div>
<div class="show-password">
<input type="password" name="password2" value="secret2">
<button type="button"></button>
</div>
</form>
<script>
// ...
</script>
<form>
<!-- SHOW-PASSWORD BEGINS -->
<style> /* ... */ </style>
<div class="show-password">
<input type="password" name="password1" value="secret1">
<button type="button"></button>
</div>
<script> /* ... */ </script>
<!-- SHOW-PASSWORD ENDS -->
<!-- SHOW-PASSWORD BEGINS -->
<style> /* ... */ </style>
<div class="show-password">
<input type="password" name="password2" value="secret2">
<button type="button"></button>
</div>
<script> /* ... */ </script>
<!-- SHOW-PASSWORD ENDS -->
</form>
<form>
<show-password name="password1" value="secret1"></show-password>
<show-password name="password2" value="secret2"></show-password>
</form>
<template>
<template>
<!-- ... -->
</template>
<div id="movies"></div>
<template id="movie-details">
<details>
<summary>Film</summary>
<img src="">
</details>
</template>
(async function () {
const search = 'Hobbit'
const response = await fetch(`http://www.omdbapi.com/?s=${search}&apikey=2dd0dbee`)
const json = await response.json()
const results = json.Search
const movies = document.querySelector('#movies')
const template = document.querySelector('#movie-details')
results.forEach(movie => {
const instance = template.content.cloneNode(true)
instance.querySelector('summary').innerHTML = movie.Title
instance.querySelector('img').src = movie.Poster
movies.appendChild(instance)
});
}());
<template>
<template id="movie-details">
<details>
<summary>Film</summary>
<img src="">
</details>
</template>
const title = 'Hobbit'
const url = 'http://imguri'
const template = document.querySelector('#movie-details')
const instance = template.content.cloneNode(true)
instance.querySelector('summary').innerHTML = title
instance.querySelector('img').src = url
movies.appendChild(instance)
const title = 'Hobbit'
const url = 'http://imguri'
const s = `
<details>
<summary>${title}</summary>
<img src="${url}">
</details>
`
movies.innerHTML += s
const template = document.createElement('template')
template.innerHTML = `
<details>
<summary>Film</summary>
<img src="">
</details>
`
const template = document.querySelector('#movie-details')
const instance = template.content.cloneNode(true)
// ...
Bármi lehet
<template>
<style>
/* */
</style>
<div>
<!-- -->
</div>
<script>
//
</script>
</template>
~ show-password (???)
<show-password name="password1" value="secret1"></show-password>
extends HTMLElement
class ShowPassword extends HTMLElement {
connectedCallback() {
this.innerHTML = `Hello component!`;
}
}
customElements.define('show-password', ShowPassword);
<show-password></show-password>
Kötőjelet kell tartalmaznia a névnek!
<show-password>
<p>Content</p>
</show-password>
class ShowPassword extends HTMLElement {
connectedCallback() {
console.log(this.innerHTML) // '<p>Content</p>'
this.appendChild(document.createElement('hr'))
this.innerHTML = `Hello component!`
}
}
customElements.define('show-password', ShowPassword);
class ShowPassword extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
// ...
}
connectedCallback() {
// Called every time the element is inserted into the DOM.
// Useful for running setup code, such as fetching resources or rendering.
// Generally, you should try to delay work until this time.
}
disconnectedCallback() {
// Called every time the element is removed from the DOM.
// Useful for running clean up code.
}
attributeChangedCallback(attrName, oldVal, newVal) {
// Called when an observed attribute has been added, removed, updated, or replaced.
}
adoptedCallback() {
// The custom element has been moved into a new document
}
}
setAttribute(name, value)
getAttribute(name)
removeAttribute(name)
hasAttribute(name)
<show-password name="password1" value="secret1"></show-password>
Komponens JavaScript API-ja
const sp = document.querySelector('show-password[name=password1]')
sp.name
sp.name = 'password0'
sp.show()
class ShowPassword extends HTMLElement {
constructor() {
super()
this._name = ''
}
get name() {
return this._name
}
set name(val) {
this._name = val
}
show() {
// ...
}
}
Pl. emiatt:
show-password[disabled] { opacity: 0.4; }
class ShowPassword extends HTMLElement {
// ...
get name() {
if (this.hasAttribute('name')) {
return this.getAttribute('name')
}
}
set name(val) {
if (val) {
this.setAttribute('name', val);
} else {
this.removeAttribute('name');
}
}
}
Statikus observedAttributes
tömb
class ShowPassword extends HTMLElement {
static get observedAttributes() {
return ['name', 'value'];
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(name, oldValue, newValue)
}
}
const sp = document.querySelector('show-password[name=password1]')
sp.setAttribute('name', 'password0')
class ShowPassword extends HTMLElement {
static get observedAttributes() {
return ['name', 'value'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'name') {
this.name = newValue
}
else if (name === 'value') {
this.value = newValue
}
}
✒>get name() {
if (this.hasAttribute('name')) {
return this.getAttribute('name')
}
}
set name(val) {
if (val) {
this.setAttribute('name', val);
} else {
this.removeAttribute('name');
}
}<✒
}
Mivel az oldal DOM-jába renderelődik ki, ezért a globális stílusok alkalmazódnak.
<style>
p {
color: red;
}
show-password {
border: 3px solid orange;
}
</style>
<show-password>
<p>Content</p>
</show-password>
class ExtendedShowPassword extends ShowPassword {
constructor() {
super()
// ....
}
show() {
// super.show()
}
newMethod() {
// ...
}
}
customElements.define('extended-show-password', ExtendedShowPassword);
class LoggerButton extends HTMLButtonElement {
constructor() {
super();
}
connectedCallback() {
this.addEventListener('click', e => console.log('click'));
}
}
customElements.define('logger-button', LoggerButton, {extends: 'button'});
<button is="logger-button">Click me to log!</button>
<button is="logger-button" disabled>Click me to log!</button>
Támogatottsága egyelőre kicsi
<video>
tagattachShadow({ mode: 'open' })
<style>
p { color: red; }
</style>
<p>Other paragraph</p>
<div>
<p>Original content in div</p>
</div>
const elem = document.querySelector('div')
const shadowRoot = elem.attachShadow({ mode: 'open' })
shadowRoot.innerHTML = `
<style>
p { color: blue; }
</style>
<p>Shadow content</p>
`
<slot>
elem, light, shadow, flattened DOM
<style>p { color: red; }</style>
<div>
<!-- Light DOM -->
<span slot="content">Span from outside</span>
<p>Original content</p>
</div>
const elem = document.querySelector('div')
const shadowRoot = elem.attachShadow({ mode: 'open' })
shadowRoot.innerHTML = `
<!-- Shadow DOM -->
<style>p { color: blue; }</style>
<p>
Shadow content and
<slot name="content">Default content</slot>
</p>
<slot>Default value</slot>
`
:host
, :host(sel)
,
:host-context(sel)
: root stílusa::slotted
: beemelt elemek belső stílusának--my-variable: red;
color: var(--my-variable, blue);
<show-password>
<form action="" method="get">
<input type="password" name="password1" value="secret1">
<button>Submit</button>
</form>
<form action="" method="get">
<input type="password" name="password1" value="secret1" data-show-password>
<button>Submit</button>
</form>
<style>/* ... */</style>
const input = document.querySelector('[data-show-password]')
const parent = input.parentNode
const div = document.createElement('div')
div.classList.add('show-password')
const button = document.createElement('button')
button.type = 'button'
parent.replaceChild(div, input)
div.appendChild(input)
div.appendChild(button)
let visible = false;
button.addEventListener('click', function (e) {
visible = !visible
if (visible) {
div.setAttribute('data-visible', '')
input.type = 'text'
} else {
div.removeAttribute('data-visible')
input.type = 'password'
}
})
<style>
div.show-password {
white-space: nowrap;
display: inline-flex;
}
div.show-password input {
line-height: 2;
border: 1px solid gray;
border-radius: 0.25em;
flex: 1;
}
div.show-password button {
border: 1px solid gray;
border-radius: 0.25em;
background-color: hsl(200, 50%, 90%);
}
div.show-password button::after {
content: '⚪';
}
div.show-password[data-visible] button::after {
content: '⚫';
}
div.show-password input:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: 0;
}
div.show-password button:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
</style>
<form action="" method="get">
<input type="password" name="password1" value="secret1" data-show-password>
<input type="password" name="password2" value="secret2" data-show-password>
<button>Submit</button>
</form>
<style></style>
document.querySelectorAll('[data-show-password]').forEach(
el => new ShowPassword(el))
class ShowPassword {
constructor(input) {
if (!input.matches('input[type=password]')) {
return
}
const parent = input.parentNode
const div = document.createElement('div')
div.classList.add('show-password')
const button = document.createElement('button')
button.type = 'button'
parent.replaceChild(div, input)
div.appendChild(input)
div.appendChild(button)
let visible = false;
button.addEventListener('click', function (e) {
visible = !visible
if (visible) {
div.setAttribute('data-visible', '')
input.type = 'text'
} else {
div.removeAttribute('data-visible')
input.type = 'password'
}
})
}
}
Shadow DOM nélkül
<form action="" method="get">
<show-password name="password1" value="secret"></show-password>
<input type="password" name="password2" value="secret" data-show-password>
</form>
<style>
/* ... */
</style>
<template id="show-password">
<div class="show-password">
<input type="password">
<button type="button"></button>
</div>
</template>
class ShowPassword extends HTMLElement {
constructor() {
super()
}
connectedCallback() {
const name = this.getAttribute('name')
const value = this.getAttribute('value')
const template = document.querySelector('#show-password')
const content = template.content.cloneNode(true)
this.appendChild(content)
const div = this.querySelector('div')
const button = this.querySelector('button')
const input = this.querySelector('input')
this.input = input
input.name = name
input.value = value
let visible = false;
button.addEventListener('click', function (e) {
visible = !visible
if (visible) {
div.setAttribute('data-visible', '')
input.type = 'text'
} else {
div.removeAttribute('data-visible')
input.type = 'password'
}
})
}
get name() {
if (this.hasAttribute('name')) {
return this.getAttribute('name')
}
}
set name(val) {
if (val) {
this.setAttribute('name', val);
} else {
this.removeAttribute('name');
}
}
get value() {
return this.input.value
}
set value(val) {
this.input.value = val
}
}
customElements.define('show-password', ShowPassword)
Shadow DOM
Probléma: form nem küldi el az input mezőt
class ShowPassword extends HTMLElement {
constructor() {
super()
}
connectedCallback() {}
const name = this.getAttribute('name')
const value = this.getAttribute('value')
✒>const template = document.querySelector('#show-password')
const content = template.content.cloneNode(true)
const shadowRoot = this.attachShadow({ mode: 'open' })
shadowRoot.appendChild(content)<✒
✒>const parent = this.parentNode
const hidden = document.createElement('input')
hidden.type = 'hidden'
hidden.name = name
parent.insertBefore(hidden, this)<✒
const div = ✒>shadowRoot<✒.querySelector('div')
const button = shadowRoot.querySelector('button')
const input = shadowRoot.querySelector('input')
this.input = input
input.name = name
input.value = value
let visible = false;
button.addEventListener('click', function (e) {
visible = !visible
if (visible) {
div.setAttribute('data-visible', '')
input.type = 'text'
} else {
div.removeAttribute('data-visible')
input.type = 'password'
}
})
✒>input.addEventListener('input', function (e) {
hidden.value = this.value
})<✒
}
get name() {
if (this.hasAttribute('name')) {
return this.getAttribute('name')
}
}
set name(val) {
if (val) {
this.setAttribute('name', val);
} else {
this.removeAttribute('name');
}
}
get value() {
return this.input.value
}
set value(val) {
this.input.value = val
}
}
customElements.define('show-password', ShowPassword)
<template id="show-password">
<style>
div {
white-space: nowrap;
display: inline-flex;
}
input {
line-height: 2;
border: 1px solid gray;
border-radius: 0.25em;
flex: 1;
}
button {
border: 1px solid gray;
border-radius: 0.25em;
background-color: hsl(200, 50%, 90%);
}
button::after {
content: '⚪';
}
div[data-visible] button::after {
content: '⚫';
}
input:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: 0;
}
button:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
</style>
<div>
<input type="password">
<button type="button"></button>
</div>
</template>
<form action="" method="get">
<show-password>
<input type="password" name="password1" value="secret1">
</show-password>
</form>
<template id="show-password">
<style>
div {
white-space: nowrap;
display: inline-flex;
}
✒>::slotted(input) {
line-height: 2;
border: 1px solid gray;
border-radius: 0.25em;
flex: 1;
}<✒
button {
border: 1px solid gray;
border-radius: 0.25em;
background-color: hsl(200, 50%, 90%);
}
button::after {
content: '⚪';
}
div[data-visible] button::after {
content: '⚫';
}
input:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: 0;
}
button:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
</style>
<div>
✒><slot></slot><✒
<button type="button"></button>
</div>
</template>
class ShowPassword extends HTMLElement {
constructor() {
super()
}
connectedCallback() {
const template = document.querySelector('#show-password')
const content = template.content.cloneNode(true)
const shadowRoot = this.attachShadow({ mode: 'open' })
shadowRoot.appendChild(content)
const div = shadowRoot.querySelector('div')
const button = shadowRoot.querySelector('button')
const input = this.querySelector('input')
let visible = false;
button.addEventListener('click', function (e) {
visible = !visible
if (visible) {
div.setAttribute('data-visible', '')
input.type = 'text'
} else {
div.removeAttribute('data-visible')
input.type = 'password'
}
})
}
}
customElements.define('show-password', ShowPassword)
<form action="" method="get">
<input type="password" name="password1" value="secret" is="show-password">
<input type="password" name="password2" value="secret" data-show-password>
</form>
<style>/* Again global styles without template */</style>
class ShowPassword extends HTMLInputElement {
constructor() {
super()
}
connectedCallback() {
window.onload = () => {
// Nem működik Shadow DOM-mal
const input = this
const button = document.createElement('button')
button.type = 'button'
const div = document.createElement('div')
div.classList.add('show-password')
const parent = this.parentElement
parent.insertBefore(div, this)
div.appendChild(this)
div.appendChild(button)
let visible = false;
button.addEventListener('click', function (e) {
visible = !visible
if (visible) {
div.setAttribute('data-visible', '')
input.type = 'text'
} else {
div.removeAttribute('data-visible')
input.type = 'password'
}
})
}
}
}
customElements.define('show-password', ShowPassword, { extends: 'input' })
<show-password>
<input type="password" name="password1" value="secret">
</show-password>
<input type="password" name="password1" value="secret" is="show-password">