mirror of
https://github.com/jaandrle/deka-dom-el
synced 2024-11-21 15:39:36 +01:00
💥 customElement (#11)
* 🎉 * Update customElement.js * 💥 `observedAttributes`
This commit is contained in:
parent
eb920f7bbd
commit
e88a495525
43
dist/dde-with-observables.js
vendored
43
dist/dde-with-observables.js
vendored
File diff suppressed because one or more lines are too long
29
dist/dde.js
vendored
29
dist/dde.js
vendored
File diff suppressed because one or more lines are too long
8
dist/esm-with-observables.js
vendored
8
dist/esm-with-observables.js
vendored
File diff suppressed because one or more lines are too long
2
dist/esm.js
vendored
2
dist/esm.js
vendored
File diff suppressed because one or more lines are too long
@ -121,7 +121,7 @@ document.head.append(
|
||||
const interval= 5 * 1000;
|
||||
setTimeout(clearInterval, 10*interval,
|
||||
setInterval(()=> count(count()+1), interval));
|
||||
</code></div><script>Flems(document.getElementById("code-example-1-ehcq40v0h5k"), JSON.parse("{\"files\":[{\"name\":\".js\",\"content\":\"import { O } from \\\"https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js\\\";\\nconst count= O(0);\\n\\nimport { el } from \\\"https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js\\\";\\ndocument.body.append(\\n\\tel(\\\"p\\\", O(()=> \\\"Currently: \\\"+count())),\\n\\tel(\\\"p\\\", { classList: { red: O(()=> count()%2) }, dataset: { count }, textContent: \\\"Attributes example\\\" })\\n);\\ndocument.head.append(\\n\\tel(\\\"style\\\", \\\".red { color: red; }\\\")\\n);\\n\\nconst interval= 5 * 1000;\\nsetTimeout(clearInterval, 10*interval,\\n\\tsetInterval(()=> count(count()+1), interval));\\n\"}],\"toolbar\":false}"));</script><p>To derived attribute based on value of observable variable just use the observable as a value of the attribute (<code>assign(element, { attribute: O('value') })</code>). <code>assign</code>/<code>el</code> provides ways to glue reactive attributes/classes more granularly into the DOM. Just use dedicated build-in attributes <code>dataset</code>, <code>ariaset</code> and <code>classList</code>.</p><p>For computation, you can use the derived observable (see above) like <code>assign(element, { textContent: O(()=> 'Hello '+WorldObservable()) })</code>.</p><p>To represent part of the template filled dynamically based on the observable value use <code>O.el(observable, DOMgenerator)</code>. This was already used in the todo example above or see:</p><!--<dde:mark type="component" name="example" host="this" ssr/>--><div id="code-example-2-8r8qappf8mo" class="example"><!--<dde:mark type="component" name="code" host="parentElement" ssr/>--><code class="language-js">import { O } from "https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js";
|
||||
</code></div><script>Flems(document.getElementById("code-example-1-ehcq40v0h5k"), JSON.parse("{\"files\":[{\"name\":\".js\",\"content\":\"import { O } from \\\"https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js\\\";\\nconst count= O(0);\\n\\nimport { el } from \\\"https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js\\\";\\ndocument.body.append(\\n\\tel(\\\"p\\\", O(()=> \\\"Currently: \\\"+count())),\\n\\tel(\\\"p\\\", { classList: { red: O(()=> count()%2) }, dataset: { count }, textContent: \\\"Attributes example\\\" })\\n);\\ndocument.head.append(\\n\\tel(\\\"style\\\", \\\".red { color: red; }\\\")\\n);\\n\\nconst interval= 5 * 1000;\\nsetTimeout(clearInterval, 10*interval,\\n\\tsetInterval(()=> count(count()+1), interval));\\n\"}],\"toolbar\":false}"));</script><p>To derived attribute based on value of observable variable just use the observable as a value of the attribute (<code>assign(element, { attribute: O('value') })</code>). <code>assign</code>/<code>el</code> provides ways to glue reactive attributes/classes more granularly into the DOM. Just use dedicated build-in attributes <code>dataset</code>, <code>ariaset</code> and <code>classList</code>.</p><p>For computation, you can use the “derived observable” (see above) like <code>assign(element, { textContent: O(()=> 'Hello '+WorldObservable()) })</code>. This is read-only observable its value is computed based on given function and updated when any observable used in the function changes.</p><p>To represent part of the template filled dynamically based on the observable value use <code>O.el(observable, DOMgenerator)</code>. This was already used in the todo example above or see:</p><!--<dde:mark type="component" name="example" host="this" ssr/>--><div id="code-example-2-8r8qappf8mo" class="example"><!--<dde:mark type="component" name="code" host="parentElement" ssr/>--><code class="language-js">import { O } from "https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js";
|
||||
const count= O(0, {
|
||||
add(){ this.value= this.value + Math.round(Math.random()*10); }
|
||||
});
|
||||
@ -147,4 +147,4 @@ setTimeout(clearInterval, 10*interval, setInterval(function(){
|
||||
O.action(count, "add");
|
||||
O.action(numbers, "push", count());
|
||||
}, interval));
|
||||
</code></div><script>Flems(document.getElementById("code-example-2-8r8qappf8mo"), JSON.parse("{\"files\":[{\"name\":\".js\",\"content\":\"import { O } from \\\"https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js\\\";\\nconst count= O(0, {\\n\\tadd(){ this.value= this.value + Math.round(Math.random()*10); }\\n});\\nconst numbers= O([ count() ], {\\n\\tpush(next){ this.value.push(next); }\\n});\\n\\nimport { el } from \\\"https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js\\\";\\ndocument.body.append(\\n\\tO.el(count, count=> count%2\\n\\t\\t? el(\\\"p\\\", \\\"Last number is odd.\\\")\\n\\t\\t: el()\\n\\t),\\n\\tel(\\\"p\\\", \\\"Lucky numbers:\\\"),\\n\\tel(\\\"ul\\\").append(\\n\\t\\tO.el(numbers, numbers=> numbers.toReversed()\\n\\t\\t\\t.map(n=> el(\\\"li\\\", n)))\\n\\t)\\n);\\n\\nconst interval= 5*1000;\\nsetTimeout(clearInterval, 10*interval, setInterval(function(){\\n\\tO.action(count, \\\"add\\\");\\n\\tO.action(numbers, \\\"push\\\", count());\\n}, interval));\\n\"}],\"toolbar\":false}"));</script><div class="notice"><!--<dde:mark type="component" name="mnemonic" host="parentElement" ssr/>--><h3 id="h-mnemonic"><!--<dde:mark type="component" name="h3" host="parentElement" ssr/>--><a href="#h-mnemonic" tabindex="-1">#</a> Mnemonic</h3><ul><li><code>O(<value>)</code> — observable: reactive value</li><li><code>O(()=> <computation>)</code> — observable: reactive value dependent on calculation using other observables</li><li><code>O.on(<observable>, <listener>[, <options>])</code> — listen to the observable value changes</li><li><code>O.clear(...<observables>)</code> — off and clear observables</li><li><code>O(<value>, <actions>)</code> — observable: pattern to create complex reactive objects/arrays</li><li><code>O.action(<observable>, <action-name>, ...<action-arguments>)</code> — invoke an action for given observable</li><li><code>O.el(<observable>, <function-returning-dom>)</code> — render partial dom structure (template) based on the current observable value</li></ul></div><div class="prevNext"><!--<dde:mark type="component" name="prevNext" host="parentElement" ssr/>--><a rel="prev" href="p03-events" title="Using not only events in UI declaratively."><!--<dde:mark type="component" name="pageLink" host="parentElement" ssr/>-->Events and Addons (previous)</a><a rel="next" href="p05-scopes" title="Organizing UI into components"><!--<dde:mark type="component" name="pageLink" host="parentElement" ssr/>-->(next) Scopes and components</a></div></main></body></html>
|
||||
</code></div><script>Flems(document.getElementById("code-example-2-8r8qappf8mo"), JSON.parse("{\"files\":[{\"name\":\".js\",\"content\":\"import { O } from \\\"https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js\\\";\\nconst count= O(0, {\\n\\tadd(){ this.value= this.value + Math.round(Math.random()*10); }\\n});\\nconst numbers= O([ count() ], {\\n\\tpush(next){ this.value.push(next); }\\n});\\n\\nimport { el } from \\\"https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js\\\";\\ndocument.body.append(\\n\\tO.el(count, count=> count%2\\n\\t\\t? el(\\\"p\\\", \\\"Last number is odd.\\\")\\n\\t\\t: el()\\n\\t),\\n\\tel(\\\"p\\\", \\\"Lucky numbers:\\\"),\\n\\tel(\\\"ul\\\").append(\\n\\t\\tO.el(numbers, numbers=> numbers.toReversed()\\n\\t\\t\\t.map(n=> el(\\\"li\\\", n)))\\n\\t)\\n);\\n\\nconst interval= 5*1000;\\nsetTimeout(clearInterval, 10*interval, setInterval(function(){\\n\\tO.action(count, \\\"add\\\");\\n\\tO.action(numbers, \\\"push\\\", count());\\n}, interval));\\n\"}],\"toolbar\":false}"));</script><div class="notice"><!--<dde:mark type="component" name="mnemonic" host="parentElement" ssr/>--><h3 id="h-mnemonic"><!--<dde:mark type="component" name="h3" host="parentElement" ssr/>--><a href="#h-mnemonic" tabindex="-1">#</a> Mnemonic</h3><ul><li><code>O(<value>)</code> — observable: reactive value</li><li><code>O(()=> <computation>)</code> — read-only observable: reactive value dependent on calculation using other observables</li><li><code>O.on(<observable>, <listener>[, <options>])</code> — listen to the observable value changes</li><li><code>O.clear(...<observables>)</code> — off and clear observables</li><li><code>O(<value>, <actions>)</code> — observable: pattern to create complex reactive objects/arrays</li><li><code>O.action(<observable>, <action-name>, ...<action-arguments>)</code> — invoke an action for given observable</li><li><code>O.el(<observable>, <function-returning-dom>)</code> — render partial dom structure (template) based on the current observable value</li></ul></div><div class="prevNext"><!--<dde:mark type="component" name="prevNext" host="parentElement" ssr/>--><a rel="prev" href="p03-events" title="Using not only events in UI declaratively."><!--<dde:mark type="component" name="pageLink" host="parentElement" ssr/>-->Events and Addons (previous)</a><a rel="next" href="p05-scopes" title="Organizing UI into components"><!--<dde:mark type="component" name="pageLink" host="parentElement" ssr/>-->(next) Scopes and components</a></div></main></body></html>
|
@ -7,7 +7,7 @@ export function mnemonic(){
|
||||
el("code", "O(<value>)"), " — observable: reactive value",
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "O(()=> <computation>)"), " — observable: reactive value dependent on calculation using other observables",
|
||||
el("code", "O(()=> <computation>)"), " — read-only observable: reactive value dependent on calculation using other observables",
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "O.on(<observable>, <listener>[, <options>])"), " — listen to the observable value changes",
|
||||
|
@ -88,7 +88,9 @@ export function page({ pkg, info }){
|
||||
el("code", "ariaset"), " and ", el("code", "classList"), "."
|
||||
),
|
||||
el("p").append(
|
||||
"For computation, you can use the derived observable (see above) like ", el("code", "assign(element, { textContent: O(()=> 'Hello '+WorldObservable()) })"), "."
|
||||
"For computation, you can use the “derived observable” (see above) like ", el("code", "assign(element, { textContent: O(()=> 'Hello '+WorldObservable()) })"), ".",
|
||||
" ",
|
||||
"This is read-only observable its value is computed based on given function and updated when any observable used in the function changes."
|
||||
),
|
||||
el("p").append(
|
||||
"To represent part of the template filled dynamically based on the observable value use ", el("code", "O.el(observable, DOMgenerator)"), ".",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { style, el, O } from '../exports.js';
|
||||
import { style, el, O, isObservable } from '../exports.js';
|
||||
const className= style.host(thirdParty).css`
|
||||
:host {
|
||||
color: green;
|
||||
@ -22,12 +22,13 @@ export function thirdParty(){
|
||||
// Array.from((new URL(location)).searchParams.entries())
|
||||
// .forEach(([ key, value ])=> O.action(store, "set", key, value));
|
||||
// O.on(store, data=> history.replaceState("", "", "?"+(new URLSearchParams(JSON.parse(JSON.stringify(data)))).toString()));
|
||||
useAdapter(store, store_adapter, {
|
||||
useStore(store_adapter, {
|
||||
onread(data){
|
||||
Array.from(data.entries())
|
||||
.forEach(([ key, value ])=> O.action(store, "set", key, value));
|
||||
return store;
|
||||
}
|
||||
});
|
||||
})();
|
||||
return el("input", {
|
||||
className,
|
||||
value: store().value(),
|
||||
@ -36,9 +37,14 @@ export function thirdParty(){
|
||||
});
|
||||
}
|
||||
|
||||
function useAdapter(observable, adapter, { onread, onbeforewrite }= {}){
|
||||
if(!onread) onread= observable;
|
||||
function useStore(adapter_in, { onread, onbeforewrite }= {}){
|
||||
const adapter= typeof adapter_in === "function" ? { read: adapter_in } : adapter_in;
|
||||
if(!onread) onread= O;
|
||||
if(!onbeforewrite) onbeforewrite= data=> JSON.parse(JSON.stringify(data));
|
||||
onread(adapter.read()); //TODO OK as synchronous
|
||||
return function useStoreInner(data_read){
|
||||
const observable= onread(adapter.read(data_read)); //TODO OK as synchronous
|
||||
if(adapter.write && isObservable(observable))
|
||||
O.on(observable, data=> adapter.write(onbeforewrite(data)));
|
||||
return observable;
|
||||
};
|
||||
}
|
||||
|
@ -13,24 +13,29 @@ export class CustomHTMLTestElement extends HTMLElement{
|
||||
connectedCallback(){
|
||||
if(!this.hasAttribute("pre-name")) this.setAttribute("pre-name", "default");
|
||||
this.attachShadow({ mode: "open" }).append(
|
||||
customElementRender(this, this.render)
|
||||
customElementRender(this, this.render, this.attributes)
|
||||
);
|
||||
}
|
||||
|
||||
render({ test }){
|
||||
attributes(element){
|
||||
const observed= O.observedAttributes(element);
|
||||
return Object.assign({ test: element.test }, observed);
|
||||
}
|
||||
render({ name, preName, test }){
|
||||
console.log(scope.state);
|
||||
scope.host(
|
||||
on.connected(()=> console.log(CustomHTMLTestElement)),
|
||||
on.attributeChanged(e=> console.log(e)),
|
||||
on.disconnected(()=> console.log(CustomHTMLTestElement))
|
||||
);
|
||||
const name= O.attribute("name");
|
||||
const preName= O.attribute("pre-name");
|
||||
|
||||
console.log({ name, test, preName});
|
||||
const text= text=> el().append(
|
||||
el("#text", text),
|
||||
" | "
|
||||
);
|
||||
return el("p").append(
|
||||
el("#text", name),
|
||||
el("#text", preName),
|
||||
text(test),
|
||||
text(name),
|
||||
text(preName),
|
||||
el("button", { type: "button", textContent: "pre-name", onclick: ()=> preName("Ahoj") })
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "deka-dom-el",
|
||||
"version": "0.7.5",
|
||||
"version": "0.7.6",
|
||||
"description": "A low-code library that simplifies the creation of native DOM elements/components using small wrappers and tweaks.",
|
||||
"author": "Jan Andrle <andrle.jan@centrum.cz>",
|
||||
"license": "MIT",
|
||||
@ -46,6 +46,7 @@
|
||||
"browser": true,
|
||||
"undef": "true",
|
||||
"latedef": "true",
|
||||
"-W014": true,
|
||||
"maxparams": 5,
|
||||
"maxdepth": 3,
|
||||
"maxcomplexity": 14,
|
||||
@ -58,7 +59,7 @@
|
||||
"size-limit": [
|
||||
{
|
||||
"path": "./index.js",
|
||||
"limit": "9.75 kB",
|
||||
"limit": "9.85 kB",
|
||||
"gzip": false,
|
||||
"brotli": false
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { scope } from "./dom.js";
|
||||
export function customElementRender(custom_element, render, props= custom_element){
|
||||
export function customElementRender(custom_element, render, props= observedAttributes){
|
||||
scope.push({
|
||||
scope: custom_element,
|
||||
host: (...c)=> c.length ? c.forEach(c=> c(custom_element)) : custom_element,
|
||||
custom_element
|
||||
});
|
||||
if(typeof props==="function") props= props.call(custom_element, custom_element);
|
||||
const out= render.call(custom_element, props);
|
||||
scope.pop();
|
||||
return out;
|
||||
@ -31,3 +32,8 @@ export { lifecycleToEvents as customElementWithDDE };
|
||||
function wrapMethod(obj, method, apply){
|
||||
obj[method]= new Proxy(obj[method] || (()=> {}), { apply });
|
||||
}
|
||||
|
||||
import { observedAttributes as oA } from "./helpers.js";
|
||||
export function observedAttributes(instance){
|
||||
return oA(instance, (i, n)=> i.getAttribute(n));
|
||||
}
|
||||
|
@ -84,6 +84,7 @@ on.attributeChanged= function(listener, options){
|
||||
});
|
||||
const c= onAbort(options.signal, ()=> observer.disconnect());
|
||||
if(c) observer.observe(element, { attributes: true });
|
||||
//TODO: clean up when element disconnected
|
||||
return element;
|
||||
};
|
||||
};
|
||||
|
@ -15,3 +15,12 @@ export function onAbort(signal, listener){
|
||||
signal.removeEventListener("abort", listener);
|
||||
};
|
||||
}
|
||||
export function observedAttributes(instance, observedAttribute){
|
||||
const { observedAttributes= [] }= instance.constructor;
|
||||
return observedAttributes
|
||||
.reduce(function(out, name){
|
||||
Reflect.set(out, kebabToCamel(name), observedAttribute(instance, name));
|
||||
return out;
|
||||
}, {});
|
||||
}
|
||||
function kebabToCamel(name){ return name.replace(/-./g, x=> x[1].toUpperCase()); }
|
||||
|
@ -9,7 +9,7 @@ const stack_watch= [];
|
||||
/**
|
||||
* ### `WeakMap<function, Set<ddeObservable<any, any>>>`
|
||||
* The `Set` is in the form of `[ source, ...depended observables (DSs) ]`.
|
||||
* When the DS is cleaned (`S.clear`) it is removed from DSs,
|
||||
* When the DS is cleaned (`O.clear`) it is removed from DSs,
|
||||
* if remains only one (`source`) it is cleared too.
|
||||
* ### `WeakMap<object, function>`
|
||||
* This is used for revesed deps, the `function` is also key for `deps`.
|
||||
@ -18,16 +18,16 @@ const stack_watch= [];
|
||||
const deps= new WeakMap();
|
||||
export function observable(value, actions){
|
||||
if(typeof value!=="function")
|
||||
return create(value, actions);
|
||||
return create(false, value, actions);
|
||||
if(isObservable(value)) return value;
|
||||
|
||||
const out= create();
|
||||
const out= create(true);
|
||||
const contextReWatch= function(){
|
||||
const [ origin, ...deps_old ]= deps.get(contextReWatch);
|
||||
deps.set(contextReWatch, new Set([ origin ]));
|
||||
|
||||
stack_watch.push(contextReWatch);
|
||||
out(value());
|
||||
write(out, value());
|
||||
stack_watch.pop();
|
||||
|
||||
if(!deps_old.length) return;
|
||||
@ -113,41 +113,38 @@ observable.el= function(o, map){
|
||||
return out;
|
||||
};
|
||||
import { on } from "./events.js";
|
||||
const key_attributes= "__dde_attributes";
|
||||
observable.attribute= function(name, initial= null){
|
||||
//TODO host=element & reuse existing
|
||||
const out= observable(initial);
|
||||
let element;
|
||||
scope.host(el=> {
|
||||
element= el;
|
||||
if(elementAttribute(element, "has", name)) out(elementAttribute(element, "get", name));
|
||||
else if(initial!==null) elementAttribute(element, "set", name, initial);
|
||||
|
||||
if(el[key_attributes]){
|
||||
el[key_attributes][name]= out;
|
||||
return;
|
||||
import { observedAttributes } from "./helpers.js";
|
||||
function observedAttribute(instance, name){
|
||||
const out= (...args)=> !args.length
|
||||
? instance.getAttribute(name)
|
||||
: instance.setAttribute(name, ...args);
|
||||
out.attribute= name;
|
||||
return out;
|
||||
}
|
||||
element[key_attributes]= { [name]: out };
|
||||
const key_attributes= "__dde_attributes";
|
||||
observable.observedAttributes= function(element){
|
||||
const attrs= observedAttributes(element, observedAttribute);
|
||||
const store= element[key_attributes]= {};
|
||||
const actions= {
|
||||
_set(value){ this.value= value; },
|
||||
};
|
||||
Object.keys(attrs).forEach(name=> {
|
||||
const attr= attrs[name]= toObservable(attrs[name], attrs[name](), actions);
|
||||
store[attr.attribute]= attr;
|
||||
});
|
||||
on.attributeChanged(function attributeChangeToObservable({ detail }){
|
||||
/*! This maps attributes to observables (`S.attribute`).
|
||||
/*! This maps attributes to observables (`O.observedAttributes`).
|
||||
* Investigate `__dde_attributes` key of the element.*/
|
||||
const [ name, value ]= detail;
|
||||
const curr= element[key_attributes][name];
|
||||
if(curr) return curr(value);
|
||||
if(curr) return observable.action(curr, "_set", value);
|
||||
})(element);
|
||||
on.disconnected(function(){
|
||||
/*! This removes all observables mapped to attributes (`S.attribute`).
|
||||
/*! This removes all observables mapped to attributes (`O.observedAttributes`).
|
||||
* Investigate `__dde_attributes` key of the element.*/
|
||||
observable.clear(...Object.values(element[key_attributes]));
|
||||
})(element);
|
||||
});
|
||||
return new Proxy(out, {
|
||||
apply(target, _, args){
|
||||
if(!args.length) return target();
|
||||
const value= args[0];
|
||||
return elementAttribute(element, "set", name, value);
|
||||
}
|
||||
});
|
||||
return attrs;
|
||||
};
|
||||
|
||||
import { typeOf } from './helpers.js';
|
||||
@ -169,7 +166,7 @@ function removeObservablesFromElements(o, listener, ...notes){
|
||||
element[key_reactive]= [];
|
||||
on.disconnected(()=>
|
||||
/*!
|
||||
* Clears all Observables listeners added in the current scope/host (`S.el`, `assign`, …?).
|
||||
* Clears all Observables listeners added in the current scope/host (`O.el`, `assign`, …?).
|
||||
* You can investigate the `__dde_reactive` key of the element.
|
||||
* */
|
||||
element[key_reactive].forEach(([ [ o, listener ] ])=>
|
||||
@ -180,9 +177,10 @@ function removeObservablesFromElements(o, listener, ...notes){
|
||||
});
|
||||
}
|
||||
|
||||
function create(value, actions){
|
||||
const o= (...value)=>
|
||||
value.length ? write(o, ...value) : read(o);
|
||||
function create(is_readonly, value, actions){
|
||||
const o= is_readonly
|
||||
? ()=> read(o)
|
||||
: (...value)=> value.length ? write(o, ...value) : read(o);
|
||||
return toObservable(o, value, actions);
|
||||
}
|
||||
const protoSigal= Object.assign(Object.create(null), {
|
||||
|
Loading…
Reference in New Issue
Block a user