web component composition not always correctly rendered in Chrome - javascript

I am using a composition of web components which is correctly rendered in firefox and safari but not in chrome. Indeed, sometimes the nested components are not displayed in this browser and I cannot figure out why and how to solve the problem.
here is the code:
index.html fetches data from a json file and displays the 2 components accordingly.
<html>
<head>
<meta charset="UTF-8">
<script type="module" src="/rect-shape.js"></script>
<script type="module" src="/shape-container.js"></script>
</head>
<body>
<p>rect-shape:</p>
<script>
const innerEl = document.createElement('rect-shape');
document.body.appendChild(innerEl);
fetch('./shapes.json')
.then(response => response.json())
.then(object => innerEl.color = object.shapes[0].color);
</script>
<hr>
<p>shape-container:</p>
<script>
const outerEl = document.createElement('shape-container');
document.body.appendChild(outerEl);
fetch('./shapes.json')
.then(response => response.json())
.then(object => outerEl.shapes = object.shapes);
</script>
</body>
</html>
shapes.json: allows parameterizing the shapes from an external service
{
"shapes": [
{
"color": "red"
},
{
"color": "blue"
}
]
}
shape-container.js:
import '/rect-shape.js';
(function() {
const template = document.createElement('template');
template.innerHTML = `
<style>
rect-shape {
display: inline-table;
}
</style>
`;
class ShapeContainer extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
set shapes(data) {
try {
this.shapeArray = data;
this.renderShapes();
} catch (error) {
console.error(error);
}
}
renderShapes() {
this.shapeArray.forEach(shape => {
const innerElement = document.createElement('rect-shape');
this.shadowRoot.appendChild(innerElement);
innerElement.color = shape.color;
});
}
}
customElements.define('shape-container', ShapeContainer);
}());
rect-shape.js:
(function() {
const template = document.createElement('template');
template.innerHTML = `
<style>
svg {
height: 100;
}
</style>
<div>
<svg id="port-view" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" />
</svg>
</div>
`;
class RectShape extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
set color(value) {
if (value) {
this.setAttribute("color", value);
} else {
this.removeAttribute("color");
}
}
get color() {
return this.getAttribute("color");
}
static get observedAttributes() {
return ["color"];
}
attributeChangedCallback(name) {
switch(name) {
case "color":
this.colorizeRect();
break;
}
}
colorizeRect() {
this.shadowRoot.querySelector('rect')
.style.setProperty('fill', this.color);
}
}
customElements.define('rect-shape', RectShape);
}());
expected rendering:
rendering with chrome (sometimes):
How can I solve this problem?
Thanks,

I cleaned the code, keeping your functionality, where is Chrome going wrong?
class BaseElement extends HTMLElement {
constructor() {
super().attachShadow({mode: 'open'})
.append(document.getElementById(this.nodeName).content.cloneNode(true));
}
}
customElements.define('rect-shape', class extends BaseElement {
static get observedAttributes() {
return ["color"];
}
set color( value ) {
this.setAttribute("color", value);
}
attributeChangedCallback( name, oldValue, newValue ) {
if (name == "color") this.shadowRoot
.querySelector('rect')
.style
.setProperty('fill', newValue);
}
});
customElements.define('shape-container', class extends BaseElement {
set shapes( data ) {
data.forEach( shape => {
this.shadowRoot
.appendChild(document.createElement('rect-shape'))
.color = shape.color;
});
}
});
let shapeArray = {
"shapes": [{ "color": "red" }, { "color": "yellow" }, { "color": "blue" } ]
}
$Rect.color = 'rebeccapurple';
$Container.shapes = shapeArray.shapes;
<template id="RECT-SHAPE">
<style> svg { width: 70px }</style>
<svg viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" />
</svg>
</template>
<template id="SHAPE-CONTAINER">
<style> rect-shape { display: inline-table }</style>
</template>
rect-shape:
<br><rect-shape id=$Rect></rect-shape>
<br>shape-container:
<br><shape-container id=$Container></shape-container>

Related

Custom Elements rendering once only

I want to be able to create reusable custom elements. With my current implementation, each of the custom elements renders only once. All the elements (all 4) are injected into the DOM, but only the first instance of each is rendered.
I have tried with both using ShadowDOM and not using it. Any ideas?
Screenshot from dev tools:
index.html (extract from <body>):
<body>
<funky-header></funky-header>
<funky-content></funky-content>
<funky-header></funky-header>
<funky-content></funky-content>
<script src="index.js" defer></script>
</body>
I have created a generic script to create custom elements from a .html file:
index.js:
const elements = [
{ name: 'funky-header', shadowDom: false },
{ name: 'funky-content', shadowDom: false }
]
async function registerCustomElement(elementName, shadowDom) {
console.log(`Registering ${elementName}`)
await fetch(`./${elementName}.html`)
.then(stream => stream.text())
.then(async markup => {
const doc = new DOMParser().parseFromString(markup, 'text/html');
const template = doc.querySelector('template[alpine]')
const templateContent = template.content
const styles = doc.querySelector('style[scoped]')
const styleContent = styles.textContent.toString()
const elements = templateContent.querySelectorAll('[class]')
class CustomElement extends HTMLElement {
constructor() {
super()
}
connectedCallback() {
console.log(`inserting ${elementName}`)
if (shadowDom) {
const shadowRoot = this.attachShadow({ mode: 'closed' })
shadowRoot.appendChild(template.content)
} else {
this.setAttribute(`data-x-${elementName}`, '')
elements.forEach(element => {
element.setAttribute(`data-x-${elementName}`, '')
})
const scopedStyles = styleContent.replaceAll(' {', '{').replaceAll('{', `[data-x-${elementName}] {`)
const styleTag = document.createElement('style')
styleTag.type = 'text/css'
styleTag.append(document.createTextNode(scopedStyles))
this.append(styleTag)
this.append(templateContent)
}
}
}
customElements.define(elementName, CustomElement)
})
.catch(err => {
console.log('ERROR:', err)
})
}
elements.forEach(element => registerCustomElement(element.name, element.shadowDom))
An example template file:
funky-header.html:
<template alpine>
<h1 class="font-black text-blue-800">This is my header</h1>
<p class="font-thin text-xs text-blue-600 my-text">This is a paragraph with a longer text, to simualte a descritpion.</p>
</template>
<style scoped>
.my-text {
color: purple;
}
</style>
The solution was as simple as replacing the this.append(templateContent) with this.innerHTML = template.innerHTML.

react-hotkeys cntrl+s while focus is in textarea

I am trying to be able to use cntrl+s while focus within a textarea using react-hotkeys.
this.keyMap = {
KEY: "ctrl+s"
};
this.handlers = {
KEY: (e) => {
e.preventDefault();
this.saveBtn(c);
}
};
<HotKeys keyMap={this.keyMap} handlers={this.handlers}>
<textarea/>
</HotKeys>
You need to use Control+s, not ctrl+s.
You need to call configure like that so it won't ignore textareas:
import { configure } from "react-hotkeys";
configure({
ignoreTags: []
});
Following is not solution it's work around but it fulfills the requirement...
[Please Note] Basically I have restricted access to Ctrl key in browser and then it
works fine though.
import { HotKeys } from 'react-hotkeys';
import React, { PureComponent, Component } from 'react';
import { configure } from 'react-hotkeys';
const COLORS = ['green', 'purple', 'orange', 'grey', 'pink'];
const ACTION_KEY_MAP = {
KEY: 'Control+s',
};
class Login extends Component {
constructor(props, context) {
super(props, context);
this.changeColor = this.changeColor.bind(this);
configure({
ignoreTags: ['div']
});
this.state = {
colorNumber: 0
};
}
changeColor(e) {
e.preventDefault();
this.setState(({ colorNumber }) => ({ colorNumber: colorNumber === COLORS.length - 1 ? 0 : colorNumber + 1 }));
}
KeyDown(e){
if(e.ctrlKey) e.preventDefault();
}
render() {
const handlers = {
KEY: this.changeColor
};
const { colorNumber } = this.state;
const style = {
width: 200,
height: 60,
left: 20,
top: 20,
opacity: 1,
background: COLORS[colorNumber],
};
return (
<HotKeys
keyMap={ACTION_KEY_MAP}
handlers={handlers}
>
<textarea
style={style}
className="node"
tabIndex="0"
onKeyDown={this.KeyDown}
></textarea>
</HotKeys>
);
}
}
export default Login;

Draft-JS - How to create a custom block with some non-editable text

In Draft-JS, I would like a basic custom block, rendering an <h1> element. I would like to add some text before my h1, that the user cannot edit. The text is here to inform people that this block is for Title. So I would like to add "TITLE" in front of the block that is not editable.
What is the best way to achieve this in Draft JS?
You can achieve your aim by applying contentEditable={false} and readOnly property on the node that should be read-only:
class MyCustomBlock extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="my-custom-block">
<h1
contentEditable={false} // <== !!!
readOnly // <== !!!
>
Not editable title
</h1>
<div className="editable-area">
<EditorBlock {...this.props} />
</div>
</div>
);
}
}
Check working demo in the hidden snippet below:
const {Editor, CharacterMetadata, DefaultDraftBlockRenderMap, ContentBlock, EditorBlock, genKey, ContentState, EditorState} = Draft;
const { List, Map, Repeat } = Immutable;
class MyCustomBlock extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="my-custom-block">
<h1
contentEditable={false}
readOnly
>
Not editable title
</h1>
<div className="editable-area">
<EditorBlock {...this.props} />
</div>
</div>
);
}
}
function blockRendererFn(contentBlock) {
const type = contentBlock.getType();
if (type === 'MyCustomBlock') {
return {
component: MyCustomBlock,
props: {}
};
}
}
const RenderMap = new Map({
MyCustomBlock: {
element: 'div',
}
}).merge(DefaultDraftBlockRenderMap);
const extendedBlockRenderMap = Draft.DefaultDraftBlockRenderMap.merge(RenderMap);
class Container extends React.Component {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty()
};
}
_handleChange = (editorState) => {
this.setState({ editorState });
}
_onAddCustomBlock = () => {
const selection = this.state.editorState.getSelection();
this._handleChange(addNewBlockAt(
this.state.editorState,
selection.getAnchorKey(),
'MyCustomBlock'
))
}
render() {
return (
<div>
<div className="container-root">
<Editor
placeholder="Type"
blockRenderMap={extendedBlockRenderMap}
blockRendererFn={blockRendererFn}
editorState={this.state.editorState}
onChange={this._handleChange}
/>
</div>
<button onClick={this._onAddCustomBlock}>
ADD CUSTOM BLOCK
</button>
</div>
);
}
}
ReactDOM.render(<Container />, document.getElementById('react-root'));
const addNewBlockAt = (
editorState,
pivotBlockKey,
newBlockType = 'unstyled',
initialData = new Map({})
) => {
const content = editorState.getCurrentContent();
const blockMap = content.getBlockMap();
const block = blockMap.get(pivotBlockKey);
if (!block) {
throw new Error(`The pivot key - ${ pivotBlockKey } is not present in blockMap.`);
}
const blocksBefore = blockMap.toSeq().takeUntil((v) => (v === block));
const blocksAfter = blockMap.toSeq().skipUntil((v) => (v === block)).rest();
const newBlockKey = genKey();
const newBlock = new ContentBlock({
key: newBlockKey,
type: newBlockType,
text: '',
characterList: new List(),
depth: 0,
data: initialData,
});
const newBlockMap = blocksBefore.concat(
[[pivotBlockKey, block], [newBlockKey, newBlock]],
blocksAfter
).toOrderedMap();
const selection = editorState.getSelection();
const newContent = content.merge({
blockMap: newBlockMap,
selectionBefore: selection,
selectionAfter: selection.merge({
anchorKey: newBlockKey,
anchorOffset: 0,
focusKey: newBlockKey,
focusOffset: 0,
isBackward: false,
}),
});
return EditorState.push(editorState, newContent, 'split-block');
};
body {
font-family: Helvetica, sans-serif;
}
.container-root {
border: 1px solid black;
padding: 5px;
margin: 5px;
}
.my-custom-block {
background-color: cadetblue;
margin: 15px 0;
font-size: 16px;
position: relative;
}
.editable-area {
background-color: lightblue;
height: 50px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.0/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/draft-js/0.10.0/Draft.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/draft-js/0.7.0/Draft.css" rel="stylesheet"/>
<div id="react-root"></div>

How do I invalidate the old images when request for a new one is initiated in React?

Have a look my code below, I have create a pen for it too: https://codepen.io/segmentationfaulter/pen/YEEaxK
The problem is that when I click on change image button, the old image stays there for a while until the new image is loaded. What I want is that the old image is invalidated straight away and empty space is shown for the new image. How can I achieve this in react? If you can't spot the issue, try opening it in private mode so that caching doesn't affect it.
const images = [
'http://via.placeholder.com/550x550',
'http://via.placeholder.com/750x750'
]
class MyImage extends React.Component {
constructor() {
super()
this.state = {
currentImageIndex: 0
}
}
changeImage() {
this.setState((prevState) => {
if (prevState.currentImageIndex) {
return {
currentImageIndex: 0
}
} else {
return {
currentImageIndex: 1
}
}
})
}
render() {
return ( <
div >
<
img src = {
this.props.images[this.state.currentImageIndex]
}
/> <
button onClick = {
this.changeImage.bind(this)
} >
change image <
/button> <
/div>
)
}
}
ReactDOM.render( < MyImage images = {
images
}
/>, document.getElementById('root'))
img {
display: block;
}
button {
margin-top: 15px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root">
<div>
Try to assign a random key prop to the img element every time it switches to another image. This forces react to recreate the element.
constructor () {
super()
this.state = {
currentImageIndex: 0,
imgKey: Math.random()
}
}
changeImage () {
this.setState((prevState) => {
if (prevState.currentImageIndex) {
return { currentImageIndex: 0, imgKey: Math.random() }
} else {
return { currentImageIndex: 1, imgKey: Math.random() }
}
})
}
render () {
return (
<div>
<img key={this.state.imgKey} src={this.props.images[this.state.currentImageIndex]} />
<button onClick={this.changeImage.bind(this)}>
change image
</button>
</div>
)
}
}
You could use, for example, onLoad prop from img where you toggle a loading in state so you can render the placeholder.
The strategy around it is:
display the placeholder and hide the image if loading is true
hide the loading when the image has been loaded.
PS: I added a timeout so you can see the placeholder in action.
const images = [
'http://via.placeholder.com/550x550',
'http://via.placeholder.com/750x750'
]
class Images extends React.Component {
constructor() {
super()
this.state = {
currentImageIndex: 0,
loading: true,
}
}
handleLoad() {
setTimeout(() => {
this.setState({ loading: false })
}, 1000)
}
changeImage() {
this.setState((prevState) => {
if (prevState.currentImageIndex) {
return {
currentImageIndex: 0
}
}
return {
currentImageIndex: 1
}
})
}
render() {
const { loading, currentImageIndex } = this.state
return (
<div>
{loading && <div>placeholder comes here</div>}
<img
onLoad={this.handleLoad.bind(this)}
src={images[currentImageIndex]}
style={{ display: loading ? 'none' : 'block' }}
/>
<button onClick={this.changeImage.bind(this)}>
change image
</button>
</div>
)
}
}
ReactDOM.render(
<Images />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Possible way may be:
put empty picture if state is changed
fetch the image and then replace empty image from response

Component not rerendering - ReactJS

I have something similar to a notes app, and want to be able to drag and drop cards from one group to another (by using react-dnd). Naturally, after a card is dropped, I want to remove it from the source group and add it to the target group. Removing works fine, but the card is not being rendered in the target group. Here is the relevant code:
App = React.createClass({
getInitialState: function() {
...
return {
appState: appState
}
}
removeCard: function(card) {
var content = this.state.appState[card.groupId].content;
content.splice(content.indexOf(card), 1);
this.setState({ appState: this.state.appState });
},
addCard: function(card, target) {
var content = this.state.appState[target.groupId].content;
content.splice(content.indexOf(target) + 1, 0, card);
this.setState({ appState: this.state.appState });
},
onCardDrop: function(source, target) {
this.addCard(source, target); // didn't work
this.removeCard(source); // worked
},
render: function() {
var that = this;
var appState = this.state.appState;
return (
<div>
{_.map(appState, function(group) {
return (
<Group
key={group.id}
id={group.id}
group={group}
onCardDrop={that.onCardDrop} />
)
})}
</div>
)
}
});
So, the card is removed from the source group, but it never appears in the target group even though the console.log of the target group shows the card is there. Is it possible that for some reason the component is not rerendering.
The Group and Card components are rendering ul and li respectively.
I took some time to make a working example based on the code you provided... but it did work. No problems in the code you provided. This indicates that the problem lies elsewhere in your code.
I cannot give you a complete answer because the snippet you provided does not follow the Minimal, Complete, and Verifiable example rule. Though it is minimal, it's incomplete, and also not verifiable.
What I can do is paste the whole code that I made here and hope that it will be useful to you.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
<script src="https://npmcdn.com/react-dnd-html5-backend#2.1.2/dist/ReactDnDHTML5Backend.min.js"></script>
<script src="https://npmcdn.com/react-dnd#2.1.0/dist/ReactDnD.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore.js"></script>
<style>
ul {
display: inline-block;
padding: 10px;
width: 100px;
border: 1px solid gray;
vertical-align: top;
}
li {
display: block;
padding: 0;
width: 100px;
text-align: center;
box-sizing: border-box;
position: relative;
}
li.group {
}
li.card {
height: 100px;
border: 1px solid black;
line-height: 100px;
margin-top: 5px;
font-size: 25px;
font-weight: bold;
cursor: move;
}
li > span {
vertical-align: middle;
display: inline-block;
}
</style>
</head>
<body>
<div id="example"></div>
<script type="text/babel">
window.ItemTypes = {
CARD: "card",
GROUP_TITLE: "group-title"
};
</script>
<script type="text/babel">
var cardSource = {
beginDrag: function (props) {
return { cardId: props.id, groupId: props.groupId, card: props.card };
}
};
function collect(connect, monitor) {
return {
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
}
}
var cardTarget = {
drop: function (props, monitor) {
var item = monitor.getItem();
console.log(item.card)
console.log(props.card)
props.onCardDrop(item.card, props.card);
},
canDrop: function (props, monitor) {
var item = monitor.getItem();
return item.cardId != props.id;
}
};
function collectTgt(connect, monitor) {
return {
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop()
};
}
window.Card = React.createClass({
propTypes: {
connectDragSource: React.PropTypes.func.isRequired,
isDragging: React.PropTypes.bool.isRequired,
isOver: React.PropTypes.bool.isRequired,
canDrop: React.PropTypes.bool.isRequired
},
renderOverlay: function (color) {
return (
<div style={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
zIndex: 1,
opacity: 0.5,
backgroundColor: color,
}} />
);
},
render: function() {
var connectDragSource = this.props.connectDragSource;
var isDragging = this.props.isDragging;
var connectDropTarget = this.props.connectDropTarget;
var isOver = this.props.isOver;
var canDrop = this.props.canDrop;
return connectDropTarget(connectDragSource(
<li className="card" style={{opacity: isDragging ? 0.5 : 1}}
><span>{this.props.card.name}-{this.props.card.groupId}</span>
{isOver && !canDrop && this.renderOverlay('red')}
{!isOver && canDrop && this.renderOverlay('yellow')}
{isOver && canDrop && this.renderOverlay('green')}
</li>
));
}
});
window.Card = ReactDnD.DragSource(ItemTypes.CARD, cardSource, collect)(window.Card);
window.Card = ReactDnD.DropTarget(ItemTypes.CARD, cardTarget, collectTgt)(window.Card);
</script>
<script type="text/babel">
window.Group = React.createClass({
render: function() {
console.log(this.props.group)
var that = this;
return (
<ul>
<li className="group">Group #{this.props.group.id}</li>
{_.map(this.props.group.content, function(card) {
return (
<Card
key={card.name}
id={card.name}
groupId={card.groupId}
card={card}
onCardDrop={that.props.onCardDrop}
/>
)
})}
</ul>
);
}
});
</script>
<script type="text/babel">
window.App = React.createClass({
getInitialState: function() {
return {
appState: [
{
id: 0,
content: [
{
groupId: 0,
name: "C1"
},
{
groupId: 0,
name: "C2"
},
{
groupId: 0,
name: "C3"
},
{
groupId: 0,
name: "C4"
}
]
},
{
id: 1,
content: [
{
groupId: 1,
name: "C5"
},
{
groupId: 1,
name: "C6"
},
{
groupId: 1,
name: "C7"
},
{
groupId: 1,
name: "C8"
}
]
}
]
};
},
removeCard: function(card) {
var content = this.state.appState[card.groupId].content;
content.splice(content.indexOf(card), 1);
this.setState({ appState: this.state.appState });
},
addCard: function(card, target) {
var content = this.state.appState[target.groupId].content;
content.splice(content.indexOf(target) + 1, 0, card);
card.groupId = target.groupId;
this.setState({ appState: this.state.appState });
},
onCardDrop: function(source, target) {
this.removeCard(source); // worked
this.addCard(source, target); // worked
},
render: function() {
var that = this;
var appState = this.state.appState;
return (
<div>
{_.map(appState, function(group) {
return (
<Group
key={group.id}
id={group.id}
group={group}
onCardDrop={that.onCardDrop}
/>
)
})}
</div>
)
}
});
window.App = ReactDnD.DragDropContext(ReactDnDHTML5Backend)(window.App);
</script>
<script type="text/babel">
ReactDOM.render(
<App />,
document.getElementById('example')
);
</script>
</body>
</html>

Categories

Resources