I'm curious if we can define our own block type instead of using one from DRAFTBLOCKTYPE.
Currently I'm playing with draft-wysiwyg which uses plugin named draft-image-plugin. The problem is that I've to pass the block-image as the type of the block instead of atomic to make the plugin working.
Actually, I had tried to use the solution from this where I override the plugin's type to atomic. But it affects other blocks with atomic type on the application where I can't create my own blockRendererFn since the blockRenderer is 'swallowed' by that plugin's blockRenderer.
To set the block type to atomic, I can easily achieved it by:
AtomicBlockUtils.insertAtomicBlock(
editorState,
entityKey,
' '
)
How to set the block type to any custom defined type such as block-image or block-table? Is that even possible?
Yes, that's possible, and you have a few different options. Here are some I know of:
If you have control over the component that renders blocks of type atomic, it would probably be easiest to add your new type as an entity to those blocks.
If that's not an option, it get's a bit more cumbersome. AtomicBlockUtils is actually just a module made to help people create media (atomic) blocks easier (even though more utility functions will probably be added in the future). If you want the exact same behavior, but with a different type, you could copy that module and just exchange 'atomic' with something else (e.g. 'block-image' or a variable to make it more generic/resuable).
The technique they use is basically to create a selection of an empty block, and then use the Modifier.setBlockType() function to give it a new block type:
const asAtomicBlock = DraftModifier.setBlockType(
afterSplit, // ContentState
insertionTarget, // SelectionState
'atomic' // your custom type
);
In this example, the author has created his own version, called addNewBlock() (it doesn't work exactly like the one in AtomicBlockUtils though):
/*
Adds a new block (currently replaces an empty block) at the current cursor position
of the given `newType`.
*/
const addNewBlock = (editorState, newType = Block.UNSTYLED, initialData = {}) => {
const selectionState = editorState.getSelection();
if (!selectionState.isCollapsed()) {
return editorState;
}
const contentState = editorState.getCurrentContent();
const key = selectionState.getStartKey();
const blockMap = contentState.getBlockMap();
const currentBlock = getCurrentBlock(editorState);
if (!currentBlock) {
return editorState;
}
if (currentBlock.getLength() === 0) {
if (currentBlock.getType() === newType) {
return editorState;
}
const newBlock = currentBlock.merge({
type: newType,
data: getDefaultBlockData(newType, initialData),
});
const newContentState = contentState.merge({
blockMap: blockMap.set(key, newBlock),
selectionAfter: selectionState,
});
return EditorState.push(editorState, newContentState, 'change-block-type');
}
return editorState;
};
So if you want to e.g. create a block of type 'block-image', with a src attribute, you can use this function like so:
const newEditorState = addNewBlock(this.state.editorState, 'block-image', { src: 'https://...' })
this.setState({ editorState: newEditorState })
Update:
If you add a new type, you also need to add it to your editor's blockRenderMap:
import { Map } from 'immutable'
<Editor
// editor props
blockRenderMap={Map({
['unstyled']: {
element: 'div'
},
['block-image']: {
element: 'div' // or whatever element you want as a wrapper
},
// all your other block types
})}
/>
Related
I'm learning Lit and I want to make some common functionality that can be added either when extending the element or when using html`` to make a TemplateResult.
Example
For example, I have a custom input element which I want to be able to optionally apply a value converter on change.
I've made a simple mixin for this:
/**
* Mixin to to enable value formatting
* On focus, changes shadow dom input value to the elements value
* On blur, changes shadow dom input value to formatted value
*/
export function ValueFormatterMixin(superClass) {
return class extends superClass {
static properties = {
valueFormatter: { type: Function },
};
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.addEventListener('focus', this.unmaskValue);
this.addEventListener('blur', this.maskValue);
}
maskValue = (evt) => {
if (typeof this.valueFormatter === 'function') {
this.inputElement.value = this.valueFormatter(this.inputElement.value);
}
};
unmaskValue = (evt) => {
this.inputElement.value = this.value;
};
};
}
This lets me apply this when extending my base custom element:
// Focus: show raw entered number
// Blur: show formatted currency value
class CurrencyElement extends ValueFormatterMixin(BasicInput) {
valueFormatter(value) {
return new Intl.NumberFormat('en-US', { style: 'currency' }).format(value);
}
}
The Problem
I've realized I would also like to optionally add a valueFormatter when creating through element through HTML and not creating a custom element for the input field:
return html`
<basic-input ${addValueFormatter((value) => `${value * 100}%`)}>
`;
I do not however want to add the ValueFormatterMixin to the basic-input class. I would like to keep that class as simple as possible, then lazily add on features. Most of the time I will not value formatter on my inputs so I don't want to include that functionally by default.
I do not believe its possible to retroactively add a mixin from a directive though. Is there a way to retroactively upgrade an element to include more functionality without defining a new component?
My understanding is that data is passed to a custom html element via its attributes and sent out by dispatching a CustomEvent.
JavaScript objects can obviously be sent out in the event's detail field, but what if the element needs a lot of data passed into it. Is there a way to provide it with an object in JavaScript.
What if the element for instance contains a variable number of parts that needs to be initialized or changed dynamically (e.g. a table with a variable number of rows)? I can imagine setting and modifying an attribute consisting of a JSON string that is parsed inside the component, but it does not feel like an elegant way to proceed:
<my-element tableRowProperties="[{p1:'v1', p2:'v2'}, {p1:'v1',p2:'v2'}, {p1:'v1',p2:'v2'}]"></my-element>
Or can you make the element listen to events from the outside that contains a payload of data?
Passing Data In
If you really want/need to pass large amounts of data into your component then you can do it four different ways:
1) Use a property. This is the simplest since you just pass in the Object by giving the value to the element like this: el.data = myObj;
2) Use an attribute. Personally I hate this way of doing it this way, but some frameworks require data to be passed in through attributes. This is similar to how you show in your question. <my-el data="[{a:1},{a:2}....]"></my-el>. Be careful to follow the rules related to escaping attribute values. If you use this method you will need to use JSON.parse on your attribute and that may fail. It can also get very ugly in the HTML to have the massive amount of data showing in a attribute.
3 Pass it in through child elements. Think of the <select> element with the <option> child elements. You can use any element type as children and they don't even need to be real elements. In your connectedCallback function your code just grabs all of the children and convert the elements, their attributes or their content into the data your component needs.
4 Use Fetch. Provide a URL for your element to go get its own data. Think of <img src="imageUrl.png"/>. If your already has the data for your component then this might seem like a poor option. But the browser provides a cool feature of embedding data that is similar to option 2, above, but is handled automatically by the browser.
Here is an example of using embedded data in an image:
img {
height: 32px;
width: 32px;
}
<img src="data:image/svg+xml;charset=utf8,%3C?xml version='1.0' encoding='utf-8'?%3E%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 314.7 314.7'%3E%3Cstyle type='text/css'%3E .st0{fill:transparent;stroke:%23231F20;stroke-width:12;} .st1{fill:%23231F20;stroke:%23231F20;stroke-width:10;stroke-linejoin:round;stroke-miterlimit:10;} %3C/style%3E%3Cg%3E%3Ccircle class='st0' cx='157.3' cy='157.3' r='150.4'/%3E%3Cpolygon class='st1' points='108,76.1 248.7,157.3 108,238.6'/%3E%3C/g%3E%3C/svg%3E">
And here is an example of using embedded data in a web component:
function readSrc(el, url) {
var fetchHeaders = new Headers({
Accept: 'application/json'
});
var fetchOptions = {
cache: 'default',
headers: fetchHeaders,
method: 'GET',
mode: 'cors'
};
return fetch(url, fetchOptions).then(
(resp) => {
if (resp.ok) {
return resp.json();
}
else {
return {
error: true,
status: resp.status
}
}
}
).catch(
(err) => {
console.error(err);
}
);
}
class MyEl extends HTMLElement {
static get observedAttributes() {
return ['src'];
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (oldVal !== newVal) {
this.innerHtml = '';
readSrc(this, newVal).then(
data => {
this.innerHTML = `<pre>
${JSON.stringify(data,0,2)}
</pre>`;
}
);
}
}
}
// Define our web component
customElements.define('my-el', MyEl);
<!--
This component would go load its own data from "data.json"
<my-el src="data.json"></my-el>
<hr/>
The next component uses embedded data but still calls fetch as if it were a URL.
-->
<my-el src="data:json,[{"a":9},{"a":8},{"a":7}]"></my-el>
You can do that same this using XHR, but if your browser supports Web Components then it probably supports fetch. And there are several good fetch polyfills if you really need one.
The best advantage to using option 4 is that you can get your data from a URL and you can directly embed your data. And this is exactly how most pre-defined HTML elements, like <img> work.
UPDATE
I did think of a 5th way to get JSON data into an object. That is by using a <template> tag within your component. This still required you to call JSON.parse but it can clean up your code because you don't need to escape the JSON as much.
class MyEl extends HTMLElement {
connectedCallback() {
var data;
try {
data = JSON.parse(this.children[0].content.textContent);
}
catch(ex) {
console.error(ex);
}
this.innerHTML = '';
var pre = document.createElement('pre');
pre.textContent = JSON.stringify(data,0,2);
this.appendChild(pre);
}
}
// Define our web component
customElements.define('my-el', MyEl);
<my-el>
<template>[{"a":1},{"b":"<b>Hi!</b>"},{"c":"</template>"}]</template>
</my-el>
Passing Data Out
There are three ways to get data out of the component:
1) Read the value from a property. This is ideal since a property can be anything and would normally be in the format of the data you want. A property can return a string, an object, a number, etc.
2) Read an attribute. This requires the component to keep the attribute up to date and may not be optimal since all attributes are strings. So your user would need to know if they need to call JSON.parse on your value or not.
3) Events. This is probably the most important thing to add to a component. Events should trigger when state changes in the component. Events should trigger based on user interactions and just to alert the user that something has happened or that something is available. Traditionally you would include the relevant data in your event. This reduces the amount of code the user of your component needs to write. Yes, they can still read properties or attributes, but if your events include all relevant data then they probably won't need to do anything extra.
There is a 6th way that is really similar to #Intervalia's answer above but uses a <script> tag instead of a <template> tag.
This is the same approach used by a Markdown Element.
class MyEl extends HTMLElement {
connectedCallback() {
var data;
try {
data = JSON.parse(this.children[0].innerHTML);
}
catch(ex) {
console.error(ex);
}
this.innerHTML = '';
var pre = document.createElement('pre');
pre.textContent = JSON.stringify(data,0,2);
this.appendChild(pre);
}
}
// Define our web component
customElements.define('my-el', MyEl);
<my-el>
<script type="application/json">[{"a":1},{"b":"<b>Hi!</b>"},{"c":"</template>"}]</script>
</my-el>
If you are using Polymer based web components, the passing of data could be done by data binding. Data could be stored as JSON string within attribute of and passed via context variable.
<p>JSON Data passed via HTML attribute into context variable of and populating the variable into combobox.</p>
<dom-bind><template>
<iron-ajax url='data:text/json;charset=utf-8,
[{"label": "Hydrogen", "value": "H"}
,{"label": "Oxygen" , "value": "O"}
,{"label": "Carbon" , "value": "C"}
]'
last-response="{{lifeElements}}" auto handle-as="json"></iron-ajax>
<vaadin-combo-box id="cbDemo"
label="Label, value:[[cbDemoValue]]"
placeholder="Placeholder"
items="[[lifeElements]]"
value="{{ cbDemoValue }}"
>
<template>
[[index]]: [[item.label]] <b>[[item.value]]</b>
</template>
</vaadin-combo-box>
<vaadin-combo-box label="Disabled" disabled value="H" items="[[lifeElements]]"></vaadin-combo-box>
<vaadin-combo-box label="Read-only" readonly value="O" items="[[lifeElements]]"></vaadin-combo-box>
<web-elemens-loader selection="
#polymer/iron-ajax,
#vaadin/vaadin-element-mixin/vaadin-element-mixin,
#vaadin/vaadin-combo-box,
"></web-elemens-loader>
</template></dom-bind>
<script src="https://cdn.xml4jquery.com/web-elements-loader/build/esm-unbundled/node_modules/#webcomponents/webcomponentsjs/webcomponents-loader.js"></script><script type="module" src="https://cdn.xml4jquery.com/web-elements-loader/build/esm-unbundled/src/web-elemens-loader.js"></script>
Using a tiny lib such as Lego would allow you to write the following:
<my-element :tableRowProperties="[{p1:'v1', p2:'v2'}, {p1:'v1',p2:'v2'}, {p1:'v1',p2:'v2'}]"></my-element>
and within your my-element.html web-component:
<template>
<table>
<tr :for="row in state.tableRowProperties">
<td>${row.p1}</td>
<td>${row.p2}</td>
</tr>
</template>
<script>
this.init() {
this.state = { tableRowPropoerties: [] }
}
</script>
I know this has been answered, but here is an approach I took. I know it's not rocket science and there are probably reasons not to do it this way; however, for me, this worked great.
This is an indirect approach to pass in data where an attribute called wc_data is passed in the custom element which is a 'key' that can be used one time.
You can obviously do whatever with the wc-data like callbacks and "callins" into the custom-tag.
link to codesandbox
files:
wc_data.ts
export const wc_data: {
[name: string]: any,
get(key: string): any,
set(key: string, wc_data: any): any
} = {
get(key: string): any {
const wc_data = this[key];
delete this[key];
return wc_data;
},
set(p_key: string, wc_data: any) {
this[p_key] = wc_data;
}
}
CustomTag.ts
import { wc_data } from './wc_data';
const template = document.createElement('template');
template.innerHTML = `
<style>
.custom-tag {
font-size: 1.6em;
}
</style>
<button class="custom-tag">Hello <span name="name"></span>, I am your <span name="relation"></span></button>
`;
class CustomTag extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
callin() {
console.log('callin called');
}
connectedCallback() {
const v_wc_data = wc_data.get(this.getAttribute('wc-data'));
console.log('wc_data', v_wc_data);
const v_name = this.shadowRoot.querySelector('[name="name"]');
const v_relation = this.shadowRoot.querySelector('[name="relation"]');
v_name.innerHTML = v_wc_data.name;
v_relation.innerHTML = v_wc_data.relation;
const v_button = this.shadowRoot.querySelector('button');
v_button.style.color = v_wc_data.color;
v_wc_data.element = this;
v_button.addEventListener('click', () => v_wc_data.callback?.());
}
disconnectedCallback() {
}
}
window.customElements.define('custom-tag', CustomTag);
console.log('created custom-tag element');
export default {};
SomeTsFile.ts
wc_data.set('tq', {
name: 'Luke',
relation: 'father',
color: 'blue',
element: undefined,
callback() {
console.log('the callback worked');
const v_tq_element = this.element;
console.log(this.element);
v_tq_element.callin();
},
});
some html..
<div>stuff before..</div>
<custom-tag wc_data="tq" />
<div>stuff after...</div>
Thanks to the other contributors, I came up with this solution which seems somewhat simpler. No json parsing. I use this example to wrap the entire component in a-href to make the block clickable:
customElements.define('ish-marker', class extends HTMLElement {
constructor() {
super()
const template = document.getElementById('ish-marker-tmpl').content
const wrapper = document.createElement("a")
wrapper.appendChild( template.cloneNode(true) )
wrapper.setAttribute('href', this.getAttribute('href'))
const shadowRoot = this.attachShadow({mode: 'open'}).appendChild( wrapper )
}
})
<ish-marker href="https://go-here.com">
...
// other things, images, buttons.
<span slot='label'>Click here to go-here</span>
</ish-marker>
My understanding is that data is passed to a custom html element via its attributes and sent out by dispatching a CustomEvent.
JavaScript objects can obviously be sent out in the event's detail field, but what if the element needs a lot of data passed into it. Is there a way to provide it with an object in JavaScript.
What if the element for instance contains a variable number of parts that needs to be initialized or changed dynamically (e.g. a table with a variable number of rows)? I can imagine setting and modifying an attribute consisting of a JSON string that is parsed inside the component, but it does not feel like an elegant way to proceed:
<my-element tableRowProperties="[{p1:'v1', p2:'v2'}, {p1:'v1',p2:'v2'}, {p1:'v1',p2:'v2'}]"></my-element>
Or can you make the element listen to events from the outside that contains a payload of data?
Passing Data In
If you really want/need to pass large amounts of data into your component then you can do it four different ways:
1) Use a property. This is the simplest since you just pass in the Object by giving the value to the element like this: el.data = myObj;
2) Use an attribute. Personally I hate this way of doing it this way, but some frameworks require data to be passed in through attributes. This is similar to how you show in your question. <my-el data="[{a:1},{a:2}....]"></my-el>. Be careful to follow the rules related to escaping attribute values. If you use this method you will need to use JSON.parse on your attribute and that may fail. It can also get very ugly in the HTML to have the massive amount of data showing in a attribute.
3 Pass it in through child elements. Think of the <select> element with the <option> child elements. You can use any element type as children and they don't even need to be real elements. In your connectedCallback function your code just grabs all of the children and convert the elements, their attributes or their content into the data your component needs.
4 Use Fetch. Provide a URL for your element to go get its own data. Think of <img src="imageUrl.png"/>. If your already has the data for your component then this might seem like a poor option. But the browser provides a cool feature of embedding data that is similar to option 2, above, but is handled automatically by the browser.
Here is an example of using embedded data in an image:
img {
height: 32px;
width: 32px;
}
<img src="data:image/svg+xml;charset=utf8,%3C?xml version='1.0' encoding='utf-8'?%3E%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 314.7 314.7'%3E%3Cstyle type='text/css'%3E .st0{fill:transparent;stroke:%23231F20;stroke-width:12;} .st1{fill:%23231F20;stroke:%23231F20;stroke-width:10;stroke-linejoin:round;stroke-miterlimit:10;} %3C/style%3E%3Cg%3E%3Ccircle class='st0' cx='157.3' cy='157.3' r='150.4'/%3E%3Cpolygon class='st1' points='108,76.1 248.7,157.3 108,238.6'/%3E%3C/g%3E%3C/svg%3E">
And here is an example of using embedded data in a web component:
function readSrc(el, url) {
var fetchHeaders = new Headers({
Accept: 'application/json'
});
var fetchOptions = {
cache: 'default',
headers: fetchHeaders,
method: 'GET',
mode: 'cors'
};
return fetch(url, fetchOptions).then(
(resp) => {
if (resp.ok) {
return resp.json();
}
else {
return {
error: true,
status: resp.status
}
}
}
).catch(
(err) => {
console.error(err);
}
);
}
class MyEl extends HTMLElement {
static get observedAttributes() {
return ['src'];
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (oldVal !== newVal) {
this.innerHtml = '';
readSrc(this, newVal).then(
data => {
this.innerHTML = `<pre>
${JSON.stringify(data,0,2)}
</pre>`;
}
);
}
}
}
// Define our web component
customElements.define('my-el', MyEl);
<!--
This component would go load its own data from "data.json"
<my-el src="data.json"></my-el>
<hr/>
The next component uses embedded data but still calls fetch as if it were a URL.
-->
<my-el src="data:json,[{"a":9},{"a":8},{"a":7}]"></my-el>
You can do that same this using XHR, but if your browser supports Web Components then it probably supports fetch. And there are several good fetch polyfills if you really need one.
The best advantage to using option 4 is that you can get your data from a URL and you can directly embed your data. And this is exactly how most pre-defined HTML elements, like <img> work.
UPDATE
I did think of a 5th way to get JSON data into an object. That is by using a <template> tag within your component. This still required you to call JSON.parse but it can clean up your code because you don't need to escape the JSON as much.
class MyEl extends HTMLElement {
connectedCallback() {
var data;
try {
data = JSON.parse(this.children[0].content.textContent);
}
catch(ex) {
console.error(ex);
}
this.innerHTML = '';
var pre = document.createElement('pre');
pre.textContent = JSON.stringify(data,0,2);
this.appendChild(pre);
}
}
// Define our web component
customElements.define('my-el', MyEl);
<my-el>
<template>[{"a":1},{"b":"<b>Hi!</b>"},{"c":"</template>"}]</template>
</my-el>
Passing Data Out
There are three ways to get data out of the component:
1) Read the value from a property. This is ideal since a property can be anything and would normally be in the format of the data you want. A property can return a string, an object, a number, etc.
2) Read an attribute. This requires the component to keep the attribute up to date and may not be optimal since all attributes are strings. So your user would need to know if they need to call JSON.parse on your value or not.
3) Events. This is probably the most important thing to add to a component. Events should trigger when state changes in the component. Events should trigger based on user interactions and just to alert the user that something has happened or that something is available. Traditionally you would include the relevant data in your event. This reduces the amount of code the user of your component needs to write. Yes, they can still read properties or attributes, but if your events include all relevant data then they probably won't need to do anything extra.
There is a 6th way that is really similar to #Intervalia's answer above but uses a <script> tag instead of a <template> tag.
This is the same approach used by a Markdown Element.
class MyEl extends HTMLElement {
connectedCallback() {
var data;
try {
data = JSON.parse(this.children[0].innerHTML);
}
catch(ex) {
console.error(ex);
}
this.innerHTML = '';
var pre = document.createElement('pre');
pre.textContent = JSON.stringify(data,0,2);
this.appendChild(pre);
}
}
// Define our web component
customElements.define('my-el', MyEl);
<my-el>
<script type="application/json">[{"a":1},{"b":"<b>Hi!</b>"},{"c":"</template>"}]</script>
</my-el>
If you are using Polymer based web components, the passing of data could be done by data binding. Data could be stored as JSON string within attribute of and passed via context variable.
<p>JSON Data passed via HTML attribute into context variable of and populating the variable into combobox.</p>
<dom-bind><template>
<iron-ajax url='data:text/json;charset=utf-8,
[{"label": "Hydrogen", "value": "H"}
,{"label": "Oxygen" , "value": "O"}
,{"label": "Carbon" , "value": "C"}
]'
last-response="{{lifeElements}}" auto handle-as="json"></iron-ajax>
<vaadin-combo-box id="cbDemo"
label="Label, value:[[cbDemoValue]]"
placeholder="Placeholder"
items="[[lifeElements]]"
value="{{ cbDemoValue }}"
>
<template>
[[index]]: [[item.label]] <b>[[item.value]]</b>
</template>
</vaadin-combo-box>
<vaadin-combo-box label="Disabled" disabled value="H" items="[[lifeElements]]"></vaadin-combo-box>
<vaadin-combo-box label="Read-only" readonly value="O" items="[[lifeElements]]"></vaadin-combo-box>
<web-elemens-loader selection="
#polymer/iron-ajax,
#vaadin/vaadin-element-mixin/vaadin-element-mixin,
#vaadin/vaadin-combo-box,
"></web-elemens-loader>
</template></dom-bind>
<script src="https://cdn.xml4jquery.com/web-elements-loader/build/esm-unbundled/node_modules/#webcomponents/webcomponentsjs/webcomponents-loader.js"></script><script type="module" src="https://cdn.xml4jquery.com/web-elements-loader/build/esm-unbundled/src/web-elemens-loader.js"></script>
Using a tiny lib such as Lego would allow you to write the following:
<my-element :tableRowProperties="[{p1:'v1', p2:'v2'}, {p1:'v1',p2:'v2'}, {p1:'v1',p2:'v2'}]"></my-element>
and within your my-element.html web-component:
<template>
<table>
<tr :for="row in state.tableRowProperties">
<td>${row.p1}</td>
<td>${row.p2}</td>
</tr>
</template>
<script>
this.init() {
this.state = { tableRowPropoerties: [] }
}
</script>
I know this has been answered, but here is an approach I took. I know it's not rocket science and there are probably reasons not to do it this way; however, for me, this worked great.
This is an indirect approach to pass in data where an attribute called wc_data is passed in the custom element which is a 'key' that can be used one time.
You can obviously do whatever with the wc-data like callbacks and "callins" into the custom-tag.
link to codesandbox
files:
wc_data.ts
export const wc_data: {
[name: string]: any,
get(key: string): any,
set(key: string, wc_data: any): any
} = {
get(key: string): any {
const wc_data = this[key];
delete this[key];
return wc_data;
},
set(p_key: string, wc_data: any) {
this[p_key] = wc_data;
}
}
CustomTag.ts
import { wc_data } from './wc_data';
const template = document.createElement('template');
template.innerHTML = `
<style>
.custom-tag {
font-size: 1.6em;
}
</style>
<button class="custom-tag">Hello <span name="name"></span>, I am your <span name="relation"></span></button>
`;
class CustomTag extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
callin() {
console.log('callin called');
}
connectedCallback() {
const v_wc_data = wc_data.get(this.getAttribute('wc-data'));
console.log('wc_data', v_wc_data);
const v_name = this.shadowRoot.querySelector('[name="name"]');
const v_relation = this.shadowRoot.querySelector('[name="relation"]');
v_name.innerHTML = v_wc_data.name;
v_relation.innerHTML = v_wc_data.relation;
const v_button = this.shadowRoot.querySelector('button');
v_button.style.color = v_wc_data.color;
v_wc_data.element = this;
v_button.addEventListener('click', () => v_wc_data.callback?.());
}
disconnectedCallback() {
}
}
window.customElements.define('custom-tag', CustomTag);
console.log('created custom-tag element');
export default {};
SomeTsFile.ts
wc_data.set('tq', {
name: 'Luke',
relation: 'father',
color: 'blue',
element: undefined,
callback() {
console.log('the callback worked');
const v_tq_element = this.element;
console.log(this.element);
v_tq_element.callin();
},
});
some html..
<div>stuff before..</div>
<custom-tag wc_data="tq" />
<div>stuff after...</div>
Thanks to the other contributors, I came up with this solution which seems somewhat simpler. No json parsing. I use this example to wrap the entire component in a-href to make the block clickable:
customElements.define('ish-marker', class extends HTMLElement {
constructor() {
super()
const template = document.getElementById('ish-marker-tmpl').content
const wrapper = document.createElement("a")
wrapper.appendChild( template.cloneNode(true) )
wrapper.setAttribute('href', this.getAttribute('href'))
const shadowRoot = this.attachShadow({mode: 'open'}).appendChild( wrapper )
}
})
<ish-marker href="https://go-here.com">
...
// other things, images, buttons.
<span slot='label'>Click here to go-here</span>
</ish-marker>
I have a ReactJS app where I am
A) reading a JSON input that describes a form's structure
B) dynamically generating a form from this JSON input (using document.createElement(..))
The JSON would look something like this:
{
formElements: [
{
id: “dd1”,
type: “dropdown”,
options: [ {value: “first”}, {value: “second”}]
},
{
id: “tf1”,
type: “textfield”,
showIf: “dd1 == ‘second’”
}
]
}
Now the tricky thing is that the JSON input file not only describes which form elements (e.g. dropdown, radio button group, text field) etc should be present but it ALSO describes show/hide logic for each element. For example, if a particular dropdown selection is made, then a textfield should be shown (otherwise it should stay hidden).
This would normally be done in jQuery but I have heard jQuery is not a good idea with React.
If these were hardcoded form elements, I could easily code this show/hide logic. The problem is that the form elements are being dynamically generated (by reading that JSON file) and I need to apply this show/hide logic on the fly to these autogenerated form elements.
I'm not sure how to do this.
If any one has suggestions for approaches here, especially with examples, that would be much appreciated. Thank you!
You should still be able to apply conditional rendering logic to the JSX code that is generating your form, but have you looked into using an existing form library like react-form or redux-forms? If you're relatively new to react, this would be a much easier route to get the results you want. I can't recommend a particular form library, but react-form notes that it handles dynamic data.
Here is my rough sketch of how you could manage this without using redux or a built-in form library. This is a draft that was imagined but never executed, so treat it like psuedo-code and definitely not optimized:
//import base form components up here (input, checkbox, etc)
// Map the strings in your field object to the component imported or defined above
const fieldSelector = {
input : Input,
textarea: TextArea,
checkbox: CheckBox
}
Class CustomForm extends React.Component {
constructor(props) {
super(props);
const fields = {}
const byId = []
// Note if there is any asynchronous data, you might want to put this logic
// in componentDidMount
// Create an array with each 'id'
const byId = this.props.formData.map( item => item.id );
// Create a map object listing each field by its id
this.props.formData.forEach( (field) => {
fields[field.id] = field;
}
this.state = {
fields,
byId,
}
this.handleChange = this.handleChange.bind(this);
this.checkVisibility = this.checkVisibility.bind(this);
}
// Need to add some additional logic if you're using checkboxes
// Creates an event handler for each type of field
handleChange(id) {
return (event) => {
const updatedFields = {...this.state.fields};
updatedFields[id].value = event.target.value
this.state.byId.forEach( fieldId => {
updatedFields[fieldId].visible = checkVisibility(updatedFields, fieldId)};
}
this.setState({fields: updatedFields})
}
}
// You can either restructure your showIf or include a function above
// to parse our the elements of the expression.
checkVisibility(updatedFields, fieldId) {
const field = updatedFields[fieldId];
const showIfId = field.showIf.triggerFieldId;
const showIfValue = field.showIf.value;
const operator = field.showIf.operator;
switch(operator){
case '===':
return updatedFields[showIfId].value === ShowIfValue;
case '<':
return updatedFields[showIfId].value < ShowIfValue;
//...fill in rest of operators here
default:
return field.visible;
}
}
render() {
return this.state.byId.map( fieldId => {
const field = this.state.fields[fieldId];
const CustomField = FieldSelector[field.type]
return (
{field.visible &&
<CustomField {insert whatever props from field} />
}
);
});
}
}
export default CustomForm
As an example of what I'd like to do, in Draft.js I'd like to allow the users to include a slideshow of images in the page that could change externally later on. So if there is a slideshow defined in my app, the user could select that slideshow to put in their page, and if the images were changed later then they would automatically update on the page. A visual representation in the editor would a nice-to-have as well. Am I asking too much or is this possible in Draft.js?
It never fails. I post a question and almost immediately I find something that might answer my question. Draft.js has what is called "decorators", which are currently documented here: https://draftjs.org/docs/advanced-topics-decorators.html
Basically you'd create a series of decorators using functions / components. Strategy takes a function, component is obvious.
const compositeDecorator = new CompositeDecorator([
{
strategy: handleStrategy,
component: HandleSpan,
},
{
strategy: hashtagStrategy,
component: HashtagSpan,
},
]);
Strategies can be defined using regexes. This enables you to write your own syntax or use that of a template engine's for embedding widgets. The strategies in the documentation are a fairly good example:
// Note: these aren't very good regexes, don't use them!
const HANDLE_REGEX = /\#[\w]+/g;
const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g;
function handleStrategy(contentBlock, callback, contentState) {
findWithRegex(HANDLE_REGEX, contentBlock, callback);
}
function hashtagStrategy(contentBlock, callback, contentState) {
findWithRegex(HASHTAG_REGEX, contentBlock, callback);
}
function findWithRegex(regex, contentBlock, callback) {
const text = contentBlock.getText();
let matchArr, start;
while ((matchArr = regex.exec(text)) !== null) {
start = matchArr.index;
callback(start, start + matchArr[0].length);
}
}
And then here are the components:
const HandleSpan = (props) => {
return <span {...props} style={styles.handle}>{props.children}</span>;
};
const HashtagSpan = (props) => {
return <span {...props} style={styles.hashtag}>{props.children}</span>;
};