How scully decides when to render a member variable into HTML? - scully

I'm curious how Scully decides whether to render a template variable directly into HTML, or not. From what I tested, a simple string variable is rendered directly, but a complex object wasn't rendered (although it seemingly cached it).
Any guidance would be nice.
==
This is rendered into HTML nicely:
// component.ts
message?: any;
ngOnInit(): void {
this.message = this.state.useScullyTransferState(
'test-data',
this.http.get("https://reqres.in/api/users/2").pipe(map(a => JSON.stringify(a)))
)
}
..
// component.html
{{message | async}}
This is not rendered:
// component.ts
page?: PageObject;
ngOnInit(): void {
this.pageId = this.route.snapshot.paramMap.get('pageId') || '';
this.page = new PageObject(this.pageId, 'pages', this.firestore)
this.page.enableStaticRendering(this.transferStateService)
this.page.load() // pageObject uses scullyTransferService internally.
}
// component.html
{{page.description}}

Related

NEXTJS fix window is not defined on import [duplicate]

In my Next.js app I can't seem to access window:
Unhandled Rejection (ReferenceError): window is not defined
componentWillMount() {
console.log('window.innerHeight', window.innerHeight);
}
̶A̶n̶o̶t̶h̶e̶r̶ ̶s̶o̶l̶u̶t̶i̶o̶n̶ ̶i̶s̶ ̶b̶y̶ ̶u̶s̶i̶n̶g̶ ̶p̶r̶o̶c̶e̶s̶s̶.̶b̶r̶o̶w̶s̶e̶r ̶ ̶t̶o̶ ̶j̶u̶s̶t̶ ̶e̶x̶e̶c̶u̶t̶e̶ ̶ ̶y̶o̶u̶r̶ ̶c̶o̶m̶m̶a̶n̶d̶ ̶d̶u̶r̶i̶n̶g̶ ̶r̶e̶n̶d̶e̶r̶i̶n̶g̶ ̶o̶n̶ ̶t̶h̶e̶ ̶c̶l̶i̶e̶n̶t̶ ̶s̶i̶d̶e̶ ̶o̶n̶l̶y̶.
But process object has been deprecated in Webpack5 and also NextJS, because it is a NodeJS variable for backend side only.
So we have to use back window object from the browser.
if (typeof window !== "undefined") {
// Client-side-only code
}
Other solution is by using react hook to replace componentDidMount:
useEffect(() => {
// Client-side-only code
})
Move the code from componentWillMount() to componentDidMount():
componentDidMount() {
console.log('window.innerHeight', window.innerHeight);
}
In Next.js, componentDidMount() is executed only on the client where window and other browser specific APIs will be available. From the Next.js wiki:
Next.js is universal, which means it executes code first server-side,
then client-side. The window object is only present client-side, so if
you absolutely need to have access to it in some React component, you
should put that code in componentDidMount. This lifecycle method will
only be executed on the client. You may also want to check if there
isn't some alternative universal library which may suit your needs.
Along the same lines, componentWillMount() will be deprecated in v17 of React, so it effectively will be potentially unsafe to use in the very near future.
If you use React Hooks you can move the code into the Effect Hook:
import * as React from "react";
export const MyComp = () => {
React.useEffect(() => {
// window is accessible here.
console.log("window.innerHeight", window.innerHeight);
}, []);
return (<div></div>)
}
The code inside useEffect is only executed on the client (in the browser), thus it has access to window.
With No SSR
https://nextjs.org/docs/advanced-features/dynamic-import#with-no-ssr
import dynamic from 'next/dynamic'
const DynamicComponentWithNoSSR = dynamic(
() => import('../components/hello3'),
{ ssr: false }
)
function Home() {
return (
<div>
<Header />
<DynamicComponentWithNoSSR />
<p>HOME PAGE is here!</p>
</div>
)
}
export default Home
The error occurs because window is not yet available, while component is still mounting. You can access window object after component is mounted.
You can create a very useful hook for getting dynamic window.innerHeight or window.innerWidth
const useDeviceSize = () => {
const [width, setWidth] = useState(0)
const [height, setHeight] = useState(0)
const handleWindowResize = () => {
setWidth(window.innerWidth);
setHeight(window.innerHeight);
}
useEffect(() => {
// component is mounted and window is available
handleWindowResize();
window.addEventListener('resize', handleWindowResize);
// unsubscribe from the event on component unmount
return () => window.removeEventListener('resize', handleWindowResize);
}, []);
return [width, height]
}
export default useDeviceSize
Use case:
const [width, height] = useDeviceSize();
componentWillMount() lifecycle hook works both on server as well as client side. In your case server would not know about window or document during page serving, the suggestion is to move the code to either
Solution 1:
componentDidMount()
Or, Solution 2
In case it is something that you only want to perform in then you could write something like:
componentWillMount() {
if (typeof window !== 'undefined') {
console.log('window.innerHeight', window.innerHeight);
}
}
In the constructor of your class Component you can add
if (typeof window === 'undefined') {
global.window = {}
}
Example:
import React, { Component } from 'react'
class MyClassName extends Component {
constructor(props){
super(props)
...
if (typeof window === 'undefined') {
global.window = {}
}
}
This will avoid the error (in my case, the error would occur after I would click reload of the page).
global?.window && window.innerHeight
It's important to use the operator ?., otherwise the build command might crash.
Best solution ever
import dynamic from 'next/dynamic';
const Chart = dynamic(()=> import('react-apexcharts'), {
ssr:false,
})
A bit late but you could also consider using Dynamic Imports from next turn off SSR for that component.
You can warp the import for your component inside a dynamic function and then, use the returned value as the actual component.
import dynamic from 'next/dynamic'
const BoardDynamic = dynamic(() => import('../components/Board.tsx'), {
ssr: false,
})
<>
<BoardDynamic />
</>
I have to access the hash from the URL so I come up with this
const hash = global.window && window.location.hash;
Here's an easy-to-use workaround that I did.
const runOnClient = (func: () => any) => {
if (typeof window !== "undefined") {
if (window.document.readyState == "loading") {
window.addEventListener("load", func);
} else {
func();
}
}
};
Usage:
runOnClient(() => {
// access window as you like
})
// or async
runOnClient(async () => {
// remember to catch errors that might be raised in promises, and use the `await` keyword wherever needed
})
This is better than just typeof window !== "undefined", because if you just check that the window is not undefined, it won't work if your page was redirected to, it just works once while loading. But this workaround works even if the page was redirected to, not just once while loading.
I was facing the same problem when i was developing a web application in next.js This fixed my problem, you have to refer to refer the window object in a life cycle method or a react Hook. For example lets say i want to create a store variable with redux and in this store i want to use a windows object i can do it as follows:
let store
useEffect(()=>{
store = createStore(rootReducers, window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__())
}, [])
....
So basically, when you are working with window's object always use a hook to play around or componentDidMount() life cycle method
I wrapped the general solution (if (typeof window === 'undefined') return;) in a custom hook, that I am very pleased with. It has a similiar interface to reacts useMemo hook which I really like.
import { useEffect, useMemo, useState } from "react";
const InitialState = Symbol("initial");
/**
*
* #param clientFactory Factory function similiar to `useMemo`. However, this function is only ever called on the client and will transform any returned promises into their resolved values.
* #param deps Factory function dependencies, just like in `useMemo`.
* #param serverFactory Factory function that may be called server side. Unlike the `clientFactory` function a resulting `Promise` will not be resolved, and will continue to be returned while the `clientFactory` is pending.
*/
export function useClientSideMemo<T = any, K = T>(
clientFactory: () => T | Promise<T>,
deps: Parameters<typeof useMemo>["1"],
serverFactory?: () => K
) {
const [memoized, setMemoized] = useState<T | typeof InitialState>(
InitialState
);
useEffect(() => {
(async () => {
setMemoized(await clientFactory());
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return typeof window === "undefined" || memoized === InitialState
? serverFactory?.()
: memoized;
}
Usage Example:
I am using it to dynamically import libaries that are not compatible with SSR in next.js, since its own dynamic import is only compatible with components.
const renderer = useClientSideMemo(
async () =>
(await import("#/components/table/renderers/HighlightTextRenderer"))
.HighlightTextRendererAlias,
[],
() => "text"
);
As you can see I even implemented a fallback factory callback, so you may provide a result when initially rendering on the server aswell. In all other aspects this hook should behave similiar to reacts useMemo hook. Open to feedback.
For such cases, Next.js has Dynamic Import.
A module that includes a library that only works in the browser, it's suggested to use Dynamic Import. Refer
Date: 06/08/2021
Check if the window object exists or not and then follow the code along with it.
function getSelectedAddress() {
if (typeof window === 'undefined') return;
// Some other logic
}
For Next.js version 12.1.0, I find that we can use process.title to determine whether we are in browser or in node side. Hope it helps!
export default function Projects(props) {
console.log({ 'process?.title': process?.title });
return (
<div></div>
);
}
1. From the terminal, I receive { 'process?.title': 'node' }
2. From Chrome devtool, I revice { 'process?.title': 'browser' }
I had this same issue when refreshing the page (caused by an import that didn't work well with SSR).
What fixed it for me was going to pages where this was occurring and forcing the import to be dynamic:
import dynamic from 'next/dynamic';
const SomeComponent = dynamic(()=>{return import('../Components/SomeComponent')}, {ssr: false});
//import SomeComponent from '../Components/SomeComponent'
Commenting out the original import and importing the component dynamically forces the client-side rendering of the component.
The dynamic import is covered in Nextjs's documentation here:
https://nextjs.org/docs/advanced-features/dynamic-import
I got to this solution by watching the youtube video here:
https://www.youtube.com/watch?v=DA0ie1RPP6g
You can define a state var and use the window event handle to handle changes like so.
const [height, setHeight] = useState();
useEffect(() => {
if (!height) setHeight(window.innerHeight - 140);
window.addEventListener("resize", () => {
setHeight(window.innerHeight - 140);
});
}, []);
You can try the below code snippet for use-cases such as - to get current pathname (CurrentUrl Path)
import { useRouter } from "next/router";
const navigator = useRouter()
console.log(navigator.pathname);
For anyone who somehow cannot use hook (for example, function component):
Use setTimeout(() => yourFunctionWithWindow()); will allow it get the window instance. Guess it just need a little more time to load.
I want to leave this approach that I found interesting for future researchers. It's using a custom hook useEventListener that can be used in so many others needs.
Note that you will need to apply a little change in the originally posted one, like I suggest here.
So it will finish like this:
import { useRef, useEffect } from 'react'
export const useEventListener = (eventName, handler, element) => {
const savedHandler = useRef()
useEffect(() => {
savedHandler.current = handler
}, [handler])
useEffect(() => {
element = !element ? window : element
const isSupported = element && element.addEventListener
if (!isSupported) return
const eventListener = (event) => savedHandler.current(event)
element.addEventListener(eventName, eventListener)
return () => {
element.removeEventListener(eventName, eventListener)
}
}, [eventName, element])
}
If it is NextJS app and inside _document.js, use below:
<script dangerouslySetInnerHTML={{
__html: `
var innerHeight = window.innerHeight;
`
}} />

How can I safely manipulate DOM in a StencilJS component?

I'm trying to safely remove a DOM node from a component made whit StencilJS.
I've put the removing code in a public method - It's what I need.
But, depending on which moment this method is called, I have a problem. If it is called too early, it don't have the DOM node reference yet - it's undefined.
The code below shows the component code (using StencilJS) and the HTML page.
Calling alert.dismiss() in page script is problematic. Calling the same method clicking the button works fine.
There is a safe way to do this remove()? Do StencilJS provide some resource, something I should test or I should wait?
import {
Component,
Element,
h,
Method
} from '#stencil/core';
#Component({
tag: 'my-alert',
scoped: true
})
export class Alert {
// Reference to dismiss button
dismissButton: HTMLButtonElement;
/**
* StencilJS lifecycle methods
*/
componentDidLoad() {
// Dismiss button click handler
this.dismissButton.addEventListener('click', () => this.dismiss());
}
// If this method is called from "click" event (handler above), everything is ok.
// If it is called from a script executed on the page, this.dismissButton may be undefined.
#Method()
async dismiss() {
// Remove button from DOM
// ** But this.dismissButton is undefined before `render` **
this.dismissButton.remove();
}
render() {
return (
<div>
<slot/>
<button ref={el => this.dismissButton = el as HTMLButtonElement} >
Dismiss
</button>
</div>
);
}
}
<!DOCTYPE html>
<html lang="pt-br">
<head>
<title>App</title>
</head>
<body>
<my-alert>Can be dismissed.</my-alert>
<script type="module">
import { defineCustomElements } from './node_modules/my-alert/alert.js';
defineCustomElements();
(async () => {
await customElements.whenDefined('my-alert');
let alert = document.querySelector('my-alert');
// ** Throw an error, because `this.dismissButton`
// is undefined at this moment.
await alert.dismiss();
})();
</script>
</body>
</html>
There are multiple ways to delete DOM nodes in Stencil.
The simplest is to just call remove() on the element, like any other element:
document.querySelector('my-alert').remove();
Another would be to have a parent container that manages the my-alert message(s). This is especially useful for things like notifications.
#Component({...})
class MyAlertManager {
#Prop({ mutable: true }) alerts = ['alert 1'];
removeAlert(alert: string) {
const index = this.alerts.indexOf(alert);
this.alerts = [
...this.alerts.slice(0, index),
...this.alerts.slice(index + 1, 0),
];
}
render() {
return (
<Host>
{this.alerts.map(alert => <my-alert text={alert} />)}
</Host>
);
}
}
There are other options and which one to choose will depend on the exact use case.
Update
In your specific case I would just render the dismiss button conditionally:
export class Alert {
#State() shouldRenderDismissButton = true;
#Method()
async dismiss() {
this.shouldRenderDismissButton = false;
}
render() {
return (
<div>
<slot/>
{this.shouldRenderDismissButton && <button onClick={() => this.dismiss()}>
Dismiss
</button>
</div>
);
}
}
Generally I would not recommend manually manipulating the DOM in Stencil components directly since that could lead to problems with the next renders since the virtual DOM is out of sync with the real DOM.
And if you really need to wait for the component to render you can use a Promise:
class Alert {
loadPromiseResolve;
loadPromise = new Promise(resolve => this.loadPromiseResolve = resolve);
#Method()
async dismiss() {
// Wait for load
await this.loadPromise;
// Remove button from DOM
this.dismissButton.remove();
}
componentDidLoad() {
this.loadPromiseResolve();
}
}
I previously asked a question about waiting for the next render which would make this a bit cleaner but I don't think it's easily possible at the moment. I might create a feature request for this in the future.

Make all form controls read-only in template-based form

We have a template-based form that uses the various Angular Material controls (matInput, mat-select, etc). We want to create a 'read-only' type view for some users. My immediate thought was to simply check a user's privileges and then loop through all form controls and set them to disable if need be.
This appears to work for text input type controls but is not working for selects and checkboxes. Plus this grays-out the controls which isn't necessarily what we want.
Any ideas on how to best accomplish this? Perhaps somehow disable keyboard and mouse input within the form might be what we want but I have no idea how to go about this.
I think it is possible, but you will have to check all control types, for example MatInput exposes readonly property, it allows us to create simple directive which will switch readonly state for entire form:
import {Directive, ContentChildren, QueryList, AfterContentInit, Input} from '#angular/core';
import {NgModel} from '#angular/forms';
#Directive({
selector: 'form[readonly]'
})
export class FormReadonlyDirective implements AfterContentInit
{
#ContentChildren(NgModel)
public inputs:QueryList<NgModel>;
private controls:any[];
private _readonly:boolean = false;
public ngAfterContentInit():void
{
this.controls = this.getControls();
this.inputs.changes.subscribe(() =>
{
this.setReadonly(this.readonly);
});
this.setReadonly(this.readonly);
}
#Input()
public get readonly():boolean
{
return this._readonly;
}
public set readonly(value:boolean)
{
this._readonly = value != null && value !== false && `${value}` !== 'false';
this.setReadonly(value);
}
private setReadonly(value:boolean):void
{
if(this.controls != null)
{
this.controls.forEach(control =>
{
//todo: check
control.readonly = value;
});
}
}
private getControls():any[]
{
return this.inputs.map(control => control.valueAccessor);
}
}
Not tested, but you get the idea.

Angular 2 - Provide 3rd-party library with element for rendering

How would I provide a DOM element to a 3rd-party library in Angular 2?
For example, if Library is the hypothetical 3rd-party library, and I had to do something like:
var fakeId = document.getElementById('fake'); // e.g. canvas or SVG element
var sirRender = Library.confiscate(fakeId);
sirRender.drawMustache(); // accesses canvas context or manipulates SVG
I am using Typescript and ES6 decorator component syntax.
I'm imagining I can do something inside ngOnInit like:
#Component({
...
})
export class SirRender {
...
ngOnInit() {
// do something here?
}
...
}
My specific use case:
I'm trying to use this library called VexFlow, which takes a canvas element and renders svg. Specifically, there is an example:
var canvas = $("div.one div.a canvas")[0];
var renderer = new Vex.Flow.Renderer(canvas, Vex.Flow.Renderer.Backends.CANVAS);
var ctx = renderer.getContext();
var stave = new Vex.Flow.Stave(10, 0, 500);
stave.addClef("treble").setContext(ctx).draw();
So, I'm hoping the answer will work for this ^^ case.
In fact you can reference the corresponding ElementRef using #ViewChild. Something like that:
#Component({
(...)
template: `
<div #someId>(...)</div>
`
})
export class Render {
#ViewChild('someId')
elt:ElementRef;
ngAfterViewInit() {
let domElement = this.elt.nativeElement;
}
}
elt will be set before the ngAfterViewInit callback is called. See this doc: https://angular.io/docs/ts/latest/api/core/ViewChild-var.html.
For HTML elements added statically to your components template you can use #ViewChild():
#Component({
...
template: `<div><span #item></span></div>`
})
export class SirRender {
#ViewChild('item') item;
ngAfterViewInit() {
passElement(this.item.nativeElement)
}
...
}
This doesn't work for dynamically generated HTML though.
but you can use
this.item.nativeElement.querySelector(...)
This is frowned upon though because it's direct DOM access, but if you generate the DOM dynamically already you're into this topic already anyway.

Create an instance of a React class from a string

I have a string which contains a name of the Class (this is coming from a json file). This string tells my Template Class which layout / template to use for the data (also in json). The issue is my layout is not displaying.
Home.jsx:
//a template or layout.
var Home = React.createClass({
render () {
return (
<div>Home layout</div>
)
}
});
Template.jsx:
var Template = React.createClass({
render: function() {
var Tag = this.props.template; //this is the name of the class eg. 'Home'
return (
<Tag />
);
}
});
I don't get any errors but I also don't see the layout / Home Class. I've checked the props.template and this logs the correct info. Also, I can see the home element in the DOM. However it looks like this:
<div id='template-holder>
<home></home>
</div>
If I change following line to:
var Tag = Home;
//this works but it's not dynamic!
Any ideas, how I can fix this? I'm sure it's either simple fix or I'm doing something stupid. Help would be appreciated. Apologies if this has already been asked (I couldn't find it).
Thanks,
Ewan
This will not work:
var Home = React.createClass({ ... });
var Component = "Home";
React.render(<Component />, ...);
However, this will:
var Home = React.createClass({ ... });
var Component = Home;
React.render(<Component />, ...);
So you simply need to find a way to map between the string "Home" and the component class Home. A simple object will work as a basic registry, and you can build from there if you need more features.
var components = {
"Home": Home,
"Other": OtherComponent
};
var Component = components[this.props.template];
No need to manually map your classes to a dictionary, or "registry", as in Michelle's answer. A wildcard import statement is already a dictionary!
import * as widgets from 'widgets';
const Type = widgets[this.props.template];
...
<Type />
You can make it work with multiple modules by merging all the dictionaries into one:
import * as widgets from 'widgets';
import * as widgets2 from 'widgets2';
const registry = Object.assign({}, widgets, widgets2);
const widget = registry[this.props.template];
I would totally do this to get dynamic dispatch of react components. In fact I think I am in a bunch of projects.
I had the same problem, and found out the solution by myself. I don't know if is the "best pratice" but it works and I'm using it currently in my solution.
You can simply make use of the "evil" eval function to dynamically create an instance of a react component. Something like:
function createComponent(componentName, props, children){
var component = React.createElement(eval(componentName), props, children);
return component;
}
Then, just call it where you want:
var homeComponent = createComponent('Home', [props], [...children]);
If it fits your needs, maybe you can consider something like this.
Hope it helps.
I wanted to know how to create React classes dynamically from a JSON spec loaded from a database and so I did some experimenting and figured it out. My basic idea was that I wanted to define a React app through a GUI instead of typing in code in a text editor.
This is compatible with React 16.3.2. Note React.createClass has been moved into its own module.
Here's condensed version of the essential parts:
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import createReactClass from 'create-react-class'
const spec = {
// getDefaultProps
// getInitialState
// propTypes: { ... }
render () {
return React.createElement('div', null, 'Some text to render')
}
}
const component = createReactClass(spec)
const factory = React.createFactory(component)
const instance = factory({ /* props */ })
const str = ReactDOMServer.renderToStaticMarkup(instance)
console.log(str)
You can see a more complete example here:
https://github.com/brennancheung/02-dynamic-react/blob/master/src/commands/tests/createClass.test.js
Here is the way it will work from a string content without embedding your components as statically linked code into your package, as others have suggested.
import React from 'react';
import { Button } from 'semantic-ui-react';
import createReactClass from 'create-react-class';
export default class Demo extends React.Component {
render() {
const s = "return { render() { return rce('div', null, rce(components['Button'], {content: this.props.propA}), rce(components['Button'], {content: 'hardcoded content'})); } }"
const createComponentSpec = new Function("rce", "components", s);
const componentSpec = createComponentSpec(React.createElement, { "Button": Button });
const component = React.createElement(createReactClass(componentSpec), { propA: "content from property" }, null);
return (
<div>
{component}
</div>
)
}
}
The React class specification is in string s. Note the following:
rce stands for React.createElement and given as a first param when callingcreateComponentSpec.
components is a dictionary of extra component types and given as a second param when callingcreateComponentSpec. This is done so that you can provide components with clashing names.
For example string Button can be resolved to standard HTML button, or button from Semantic UI.
You can easily generate content for s by using https://babeljs.io as described in https://reactjs.org/docs/react-without-jsx.html. Essentially, the string can't contain JSX stuff, and has to be plain JavaScript. That's what BabelJS is doing by translating JSX into JavaScript.
All you need to do is replace React.createElement with rce, and resolve external components via components dictionary (if you don't use external components, that you can skip the dictionary stuff).
Here is equivalent what in the code above. The same <div> with two Semantic UI Buttons in it.
JSX render() code:
function render() {
return (
<div>
<Button content={this.props.propA}/>
<Button content='hardcoded content'/>
</div>
);
}
BabelJS translates it into:
function render() {
return React.createElement("div", null, React.createElement(Button, {
content: this.props.propA
}), React.createElement(Button, {
content: "hardcoded content"
}));
}
And you do replacement as outlined above:
render() { return rce('div', null, rce(components['Button'], {content: this.props.propA}), rce(components['Button'], {content: 'hardcoded content'})); }
Calling createComponentSpec function will create a spec for React class.
Which then converted into actual React class with createReactClass.
And then brought to life with React.createElement.
All you need to do is return it from main component render func.
When you use JSX you can either render HTML tags (strings) or React components (classes).
When you do var Tag = Home, it works because the JSX compiler transforms it to:
var Template = React.createElement(Tag, {});
with the variable Tag in the same scope and being a React class.
var Tag = Home = React.createClass({
render () {
return (
<div>Home layout</div>
)
}
});
When you do
var Tag = this.props.template; // example: Tag = "aClassName"
you are doing
var Template = React.createElement("aClassName", null);
But "aClassName" is not a valid HTML tag.
Look here