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?
Related
Is there a way I can dynamically bind a string and the text it outputs without using setInterval? I want it to be similar to Angular and Vue though I want to do this with vanilla JS. I want to be able to open the console and change the value at any time and see the change output on my element. Thank you in advance!
I think your only two options are:
A. Edit the element directly, e.g.
myPublicElemeVariable.innerText = 'Bla'
B. Use a setter (or Proxy):
obj = {
get str() { return this.myStr; }
set str(val) {
elem.innerText = val;
this.myStr = val;
}
}
C. Just use a function/method!
If you mean you want change to be event-driven, there is already a very simple event framework in javascript - the EventTarget class as demonstrated by this Code Sandbox
//define a watchable thing
class ValueTarget extends EventTarget {
constructor(value = "") {
super();
this.setValue(value);
}
getValue() {
return this._value;
}
setValue(value) {
this._value = value;
this.dispatchEvent(new CustomEvent("change", { detail: { value } }));
}
}
//get the page elements
const inputElement = document.querySelector("input");
const outputElement = document.querySelector("h1");
//create a watchable thing
const fieldTarget = new ValueTarget("");
//wire events that will change the watchable
inputElement.addEventListener("input", function (e) {
fieldTarget.setValue(e.target.value);
});
//receive notifications from the watchable
fieldTarget.addEventListener("change", (e) => {
outputElement.textContent = e.detail.value;
});
You may be as well to build your own given how simple it is - maintains a list of listeners and calls them when notified. My work recently needed such a thing which I knocked up in Typescript at https://github.com/cefn/lauf/blob/main/modules/lauf-store/src/core/watchable.ts#L3-L21 and would therefore be very easy to redo in javascript.
I try to add a data binding to an element, which is created in JS, but it does not work. Either there is a string with the code or the value stay empty.
el.setAttribute('value', '{{value}}'); The content in the Input was is'{{value}}'
el.setAttribute('value', value); The content in the Input was is empty, because value is empty.
How is it possible to create a element dynamically and use data binding?
Creating element dynamically is another subject but here I tried to bind a value dynamically to an existing element dynamically, you may use like :
<sample-elem data = "{{data}}"></sample-elem>
...
<script>
class MyApp Polymer.Element {
static get is() { return "jobijoy-app"; }
static get properties() { return {
data: {
type:String,
value(): {return "";}
}
bindValue() {
this.set('data','Some data here');
}
customElements.define(MyApp.is, MyApp);
</script>
At this point you have a data property in sample-elem with data value is 'Some data here'
I am not sure but here with this code, you may create dynamically sample-elem
ready(){
super.ready();
var dynamicEl = document.createElement("sample-elem");
document.body.appendChild(dynamicEl);
}
var dynamicEl = document.createElement("sample-elem");
If you create an element using createElement you can use either one of these before you append it.
dynamicEl.value = this.value; // First way
dynamicEl.setAttribute('value', this.value; // Second way
this.value calls the property value set in the element where you try to create your other element programmatically.
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
I'm creating custom UI components using ES6 classes doing something like this:
class Dropdown {
constructor(dropdown) {
this.dropdown = dropdown;
this._init();
}
_init() {
//init component
}
setValue(val) {
//"public" method I want to use from another class
}
}
And when the page load I initiate the components like this:
let dropdown = document.querySelectorAll(".dropdown");
if (dropdown) {
Array.prototype.forEach.call(dropdown, (element) => {
let DropDownEl = new Dropdown(element);
});
}
But now I need to acces a method of one of these classes from another one. In this case, I need to access a method to set the value of the dropdown
based on a URL parameter, so I would like to do something like:
class SearchPage {
//SearchPage is a class and a DOM element with different components (like the dropdown) that I use as filters. This class will listen to the dispached events
//from these filters to make the Ajax requests.
constructor() {
this._page = document.querySelector(".search-page")
let dropdown = this._page.querySelector(".dropdown);
//Previously I import the class into the file
this.dropdown = new Dropdown(dropdown);
}
setValues(val) {
this.dropdown.setValue(val);
//Set other components' values...
}
}
But when I create this instance, another dropdown is added to the page, which I don't want.
I think an alternative is to create the components this way, inside other ones, and not like in the first piece of code. Is this a valid way? Should I create another Dropdown class that inherits from the original one?
A simple solution is to store the Dropdown instance on the element to avoid re-creating it:
class Dropdown {
constructor(element) {
if (element.dropdown instanceof Dropdown)
return element.dropdown;
this.element = element;
element.dropdown = this;
//init component
}
…
}
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
})}
/>