Can you manipulate the DOM directly while using Preactjs? - dom

I am looking into Preact for my next project.
Since it has no virtual DOM I am wondering if it, like React, prefers you to let the framework manipulate the DOM instead of doing so yourself directly.
Would Preact bump heads with another library that manipulates the DOM such as SVGjs?

Preact is non-destructive when it comes to DOM updates. The official guide already explains how to integrate external DOM manipulations into the preact component:
If using class-based component:
import { h, Component } from 'preact';
class Example extends Component {
shouldComponentUpdate() {
// IMPORTANT: do not re-render via diff:
return false;
}
componentWillReceiveProps(nextProps) {
// you can do something with incoming props here if you need
}
componentDidMount() {
// now mounted, can freely modify the DOM:
const thing = document.createElement('maybe-a-custom-element');
this.base.appendChild(thing);
}
componentWillUnmount() {
// component is about to be removed from the DOM, perform any cleanup.
}
render() {
return <div class="example" />;
}
}
If using hooks, then use memo function from preact/compat:
import { h } from 'preact';
import { useEffect } from 'preact/hooks';
import { memo } from 'preact/compat';
function Example(props) {
const [node, setNode] = setState(null);
useEffect(() => {
const elm = document.createElement('maybe-a-custom-element');
setNode(elm);
// Now do anything with the elm.
// Append to body or <div class="example"></div>
}, []);
return <div class="example" />;
}
// Usage with default comparison function
const Memoed = memo(Example);
// Usage with custom comparison function
const Memoed2 = memo(Example, (prevProps, nextProps) => {
// Only re-render when `name' changes
return prevProps.name === nextProps.name;
});
Also, note that Preact's render() function always diffs DOM children inside of the container. So if your container contains DOM that was not rendered by Preact, Preact will try to diff it with the elements you pass it. - Thus the meaning non-destructive.

Related

Dynamic tag name in Solid JSX

I would like to set JSX tag names dynamically in SolidJS. I come from React where it is fairly simple to do:
/* Working ReactJS Code: */
export default MyWrapper = ({ children, ..attributes }) => {
const Element = "div";
return (
<Element {...attributes}>
{children}
</Element>
)
}
but when I try to do the same thing in SolidJS, I get the following error:
/* Console output when trying to do the same in SolidJS: */
dev.js:530 Uncaught (in promise) TypeError: Comp is not a function
at dev.js:530:12
at untrack (dev.js:436:12)
at Object.fn (dev.js:526:37)
at runComputation (dev.js:706:22)
at updateComputation (dev.js:691:3)
at devComponent (dev.js:537:3)
at createComponent (dev.js:1236:10)
at get children [as children] (Input.jsx:38:5)
at _Hot$$Label (Input.jsx:7:24)
at #solid-refresh:10:42
I would like to know if I miss something here, or whether it is possible to achieve this in SolidJS in any other way.
Solid has a <Dynamic> helper component for that use.
import {Dynamic} from "solid-js/web";
<Dynamic component="div" {...attributes}>
{props.children}
</Dynamic>
Here is an alternative implementation covering simple cases like strings and nodes although you can extend it to cover any JSX element:
import { Component, JSXElement} from 'solid-js';
import { render, } from 'solid-js/web';
const Dynamic: Component<{ tag: string, children: string | Node }> = (props) => {
const el = document.createElement(props.tag);
createEffect(() => {
if(typeof props.children === 'string') {
el.innerText = String(props.children);
} else if (props.children instanceof Node){
el.appendChild(props.children);
} else {
throw Error('Not implemented');
}
});
return el;
};
const App = () => {
return (
<div>
<Dynamic tag="h2">This is an H2!</Dynamic>
<Dynamic tag="p">This is a paragraph!</Dynamic>
<Dynamic tag="div"><div>Some div element rendering another div</div></Dynamic>
</div>
)
}
render(App, document.body);
This works because Solid components are compiled into native DOM elements, however since we do not escape the output, it is dangerous to render any children directly, given that you have no control over the content.
This alternative comes handy when you need to render rich text from a content editable or a textarea, text that includes tags like em, strong etc. Just make sure you use innerHTML attribute instead of innerText.

Is it possible in vue3 to access the root DOM element in a child component slot? I am trying to use a 3rd party library (sortablejs) in vue3

In vue2 I could use this.$el
export default {
render() {
return this.$slots.default[0]
},
mounted() {
Sortable.create(this.$el, {});
})
}
If, in vue3 I try to use this.$slots.default()[0] I can't see how to target the element.
If I use a template ref, I can get the div, but not the contained slot.
The closest question / answer I have found is here Vue 3 Composition API - How to get the component element ($el) on which component is mounted
but this also seems to give the div, but not the slot $el.
This was extremely powerful in vue2 because sortable could be passed a ul, or a div, or another constructed sortable vue component in a slot, and work without the element having to be defined in the child component and I can't work out how to replicate this in vue3.
I originally came across this in a screen cast by Adam Wathan: "Building a Sortable Component with Vue.js", but this was vue2.
I've come up with the following (perhaps there are better out there)
Use template ref:
<template>
<div ref="root">
<slot></slot>
</div>
</template>
Then in the script:
import { ref, onMounted } from 'vue'
export default {
setup() {
const root = ref(null)
onMounted(() => {
// the DOM element will be assigned to the ref after initial render
// console.log(root.value.children[0]) // this is your $el
let el = root.value.children[0]
Sortable.create(el, {})
})
return {
root
}
}
}

Draftail mention plugin for wagtail

I would like to customise the draftail editor in wagtail in order to have "mentions" functionality (I am using the draft js plugin)
Because my react skills are extremely poor and my knowledge of draftail/draftjs very limited, I am using https://github.com/vixdigital/wagtail-plugin-base as a starting point (without knowing much about what it does) and trying to muddle through the task
I have managed to get it so the mention functionality appears in wagtail. The problem is that it appears in a "sub" editor in the toolbar
enter image description here
How can I avoid creating a sub editor and have it work in the body of the existing editor? Below are the two main JS files in my "wagtail-plugin-base"
index.js
import AutoComplete from './AutoComplete/AutoComplete.js';
window.draftail.registerControl(AutoComplete)
AutoComplete.js
import React, { Component } from '../../node_modules/react';
import createMentionPlugin, { defaultSuggestionsFilter } from '../../node_modules/draft-js-mention-plugin';
import editorStyles from './editorStyles.css';
import { mentions } from './mentions'
import { DraftailEditor, BLOCK_TYPE, INLINE_STYLE, createEditorState } from "draftail"
const mentionPlugin = createMentionPlugin();
const AutoComplete = class SimpleMentionEditor extends Component {
state = {
editorState: createEditorState,
suggestions: mentions,
};
onChange = (editorState) => {
this.setState({
editorState,
});
};
onSearchChange = ({ value }) => {
this.setState({
suggestions: defaultSuggestionsFilter(value, mentions),
});
};
onAddMention = () => {
// get the mention object selected
}
focus = () => {
this.editor.focus();
};
render() {
const { MentionSuggestions } = mentionPlugin
return (
<div>
<DraftailEditor
editorState={this.state.editorState}
onChange={this.onChange}
plugins={[mentionPlugin]}
ref={(element) => { this.editor = element; }}
/>
<MentionSuggestions
onSearchChange={this.onSearchChange}
suggestions={this.state.suggestions}
onAddMention={this.onAddMention}
/>
</div>
);
}
}
export default AutoComplete;
Any pointers much appreciated!

Ag-Grid Link with link in the cell

I am building angular 4 app with ag-grid and I am having an issue with trying to figure out how to put a link in the cell. Can anybody help me with that issue?
Thanks
Please check this demo
cellRenderer: function(params) {
return ''+ params.value+''
}
In this demo, the cell value for the column 'city' is a hyperlink.
I struggled with this the other day and it was bit more complex than I first thought. I ended up with creating a renderer component to which I send in the link and that needed a bit on NgZone magic to work all the way. You can use it in your column definition like this:
cellRendererFramework: RouterLinkRendererComponent,
cellRendererParams: {
inRouterLink: '/yourlinkhere',
}
Component where inRouterLink is the link that you send in and params.value is the cell value. That means that you can route to your angular route that could look something like 'yourlink/:id'. You could also simplify this a bit if you don't want a more generic solution by not sending in the link and just hard coding the link in the template and not using the cellRendererParams.
import { Component, NgZone } from '#angular/core';
import { Router } from '#angular/router';
import { AgRendererComponent } from 'ag-grid-angular';
#Component({
template: '<a [routerLink]="[params.inRouterLink,params.value]" (click)="navigate(params.inRouterLink)">{{params.value}}</a>'
})
export class RouterLinkRendererComponent implements AgRendererComponent {
params: any;
constructor(
private ngZone: NgZone,
private router: Router) { }
agInit(params: any): void {
this.params = params;
}
refresh(params: any): boolean {
return false;
}
// This was needed to make the link work correctly
navigate(link) {
this.ngZone.run(() => {
this.router.navigate([link, this.params.value]);
});
}
}
And register it in
#NgModule({
imports: [
AgGridModule.withComponents([
RouterLinkRendererComponent,
])
],
})
UPDATE: I have written a blog post about this: https://medium.com/ag-grid/enhance-your-angular-grid-reports-with-formatted-values-and-links-34fa57ca2952
This is a bit dated, but it may help someone. The solution with typescript on Angular 5 is similar to what C.O.G has suggested.
In the component's typescript file, the column definition can contain a custom cell rendering function.
columnDefs = [
{headerName: 'Client', field: 'clientName' },
{headerName: 'Invoice Number', field: 'invoiceNumber',
cellRenderer: (invNum) =>
`<a href="/invoice/${invNum.value}" >${invNum.value}</a>` },
];
The lambda function is called while rendering the cell. The 'value' of the parameter that gets passed is what you can use to generate custom rendering.
Inspired by #Michael Karén
This is a improved version that is more flexible.
We can set what text to display in link
We can pass more than 2 routerLink parameters
Resolve routerLink according to data
Support target
Display text only if link is not applicable
And more if you wanted to add, just further edit this component
import { Component } from '#angular/core';
import { ICellRendererAngularComp } from 'ag-grid-angular';
export interface IRouterLinkRendererComponentOptions {
routerLinkParams?: any[];
linkDescription?: string;
textOnly?: string;
target?: string;
}
#Component({
template: `
<a *ngIf="params.textOnly == null; else textOnlyBlock"
[routerLink]="params.routerLinkParams"
[target]="params.target ? params.target : '_self'"
>
{{ params.linkDescription }}
</a>
<ng-template #textOnlyBlock>
{{ params.textOnly }}
</ng-template>
`
})
export class RouterLinkRendererComponent implements ICellRendererAngularComp {
params: IRouterLinkRendererComponentOptions;
agInit(params: any): void {
this.params = params.routerLinkRendererComponentOptions(params);
}
refresh(params: any): boolean {
return true;
}
}
So that we can dynamically resolve parameters and return text only if wanted in column definition by
{
...
cellRendererFramework: RouterLinkRendererComponent,
cellRendererParams: {
routerLinkRendererComponentOptions: (param): IRouterLinkRendererComponentOptions => {
if (param.data.dispatch_adjustment) {
return {
routerLinkParams: ['/adjustments', param.data.dispatch_adjustment.id, 'edit'],
linkDescription: '#' + param.data.dispatch_adjustment.id
};
} else {
return {
textOnly: '-'
};
}
}
},
...
},
Instead of using href in cellRenderer , it's better to use cellrenderer framework as route link works in it.
Another Disadvantage is if you use href then the entire angular application will reload again it changes the navigation state from imperative to popstate. The angular router works on the imperative state.
I had implemented something similar to Michael and Tom, with only [routerLink] and no (click) handler. But recently I started getting the dreaded warning:
Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?
After experimenting for awhile I found this post and added the navigate click handler function, which made the application start working again, however I found that the 'Navigation triggered outside Angular zone' message was still appearing in the logs.
So while the (click)="navigate()" call triggers the navigation inside the ngZone,the [routerLink] call is still being made, which bothered me. I really didn't want two attempts to navigate to happen - in case anything changed with a future API update.
I decided to replace the anchor tag with a span pseudoLink.
.pseudoLink {
color: blue;
text-decoration: underline;
cursor: pointer;
}
#Component({
template: '<span class="pseudoLink" (click)="navigate()">{{mytitle}}</span>'
})
navigate() {
this.ngZone.run(
() => {
console.log("LinkRendererComponent: navigate: (", this.mylink, ")");
this.router.navigate([this.mylink]);
}
);
}
this.mylink is defined in the agInit() method based on parameters passed in via cellRendererParams.
This works well for my main purpose which is to make the cell look like a link. Only thing I lost was the URL path popup in the browser status bar.
Hope this might help someone else.
Using a cell renderer is the correct solution but missing from the top answer is stopping the click event from reaching AgGrid:
cellRenderer: ({value}) => {
const a = document.createElement('a');
a.innerText = a.href = value;
a.target = '_blank';
// Prevent click from reaching AgGrid
a.addEventListener('click', event => { event.stopPropagation() });
return a;
}
If the click bubbles up to AgGrid it will cause row selection changes, etc if those are enabled.
I created a generic component that is usable for any link cell, uses no workarounds, and logs no warnings.
Usage
columnDefs = [
{
colId: 'My Column',
cellRendererFramework: AgGridLinkCellComponent,
cellRendererParams: {
// `text` and `link` both accept either an string expression (same as `field`) or a function that gets ICellRendererParams
text: 'title',
link: (params: ICellRendererParams) => `/my-path/${_.get(params, 'data.id')}`
}
}
]
Register the component in your AppModule:
imports: [
AgGridModule.withComponents([
AgGridLinkCellComponent
])
]
The component itself:
import * as _ from 'lodash';
import {Component} from '#angular/core';
import {AgRendererComponent} from 'ag-grid-angular';
import {ICellRendererParams} from 'ag-grid-community';
#Component({
selector: 'app-ag-grid-link-cell-component',
template: '<a [routerLink]="link">{{ text }}</a>',
})
export class AgGridLinkCellComponent implements AgRendererComponent {
link: string;
text: string;
constructor() {
}
agInit(params: ICellRendererParams): void {
this.refresh(params);
}
refresh(params: ICellRendererParams): boolean {
const dataParams = params.colDef.cellRendererParams;
this.link = _.isFunction(dataParams.link) ? dataParams.link(params) : _.get(params.data, dataParams.link);
this.text = _.isFunction(dataParams.text) ? dataParams.link(params) : _.get(params.data, dataParams.text);
return false;
}
}
We had this problem, and its not straightforward.
We ended up solving it in a different way as we use AdapTable on top of ag-Grid.
So we created an AdapTable Action Column and in the RenderFunction provided the link. That worked best for us as we didnt always want the Link to appear so we could use the ShouldRender function to decide whether or not we wanted to display link for each row.

Meteor: apply function after rendering things from mongodb

I am using sage cell to convert html to math stuff
Template.home.rendered = function(){
\\ apply sagecell and mathjax
}
However, the content that are rendered comes from mongo, so it's sometimes loaded after sage cell is applied to it. I can do something like this
Template.home.rendered = function(){
Deps.autorun(function(){
if (Content.findOne({_id: ...})){
\\ apply sagecell and mathjax
}
});
}
It's better but still doesn't work all the time. Is there other things I can use to detect the content is completely rendered?
Edited with new response:
<template name='pendingAnswer'>
The answer to your question, coming back whenever, is:
{{>answer}}
</template>
<template name='answer'>
{{fromSage}}
</template>
Template.answer.helpers({
fromSage: function () {
Session.get('fromSage');
}
});
Invoked whenever - from a button, from navigating to the page, on blur...
function GetAnswerFromSage(data) {
callHTTP(website,data, callbackFromSage)
}
function callbackFromSage(err, data) {
if (err) then log(err);
Session.set('fromSage', data);
}
Earlier: try transform upon retrieval of mongo:
From Meteor Doc
// An Animal class that takes a document in its constructor
Animal = function (doc) {
_.extend(this, doc);
};
_.extend(Animal.prototype, {
makeNoise: function () {
console.log(this.sound);
}
});
// Define a Collection that uses Animal as its document
Animals = new Meteor.Collection("Animals", {
transform: function (doc) { return new Animal(doc); }
});
// Create an Animal and call its makeNoise method
Animals.insert({name: "raptor", sound: "roar"});
Animals.findOne({name: "raptor"}).makeNoise(); // prints "roar"
The script
<script type='text/javascript' src="http://sagecell.sagemath.org/static/embedded_sagecell.js"></script>
that is supposed to be in the head needs to be removed and instead be loaded after the content is completely loaded like so:
Template.content.rendered = function(){
// sage
Deps.autorun(function(){
if (Session.get('contentChanged')){
// loading this script causes mathjax to run
$.getScript("http://sagecell.sagemath.org/static/embedded_sagecell.js", function(d, textStatus){
if (textStatus=='success'){
// this converts <div class='compute'> to a sage cell
sagecell.makeSagecell({
inputLocation: 'div.compute',
evalButtonText: 'Evaluate',
hide: ['editorToggle']
});
}
})
}
})
and if I go from 1 content template to another content template, it seems that nothing is rerendered and so the mathjax was not applied. The only fix I can think is to force a page reload:
Template.content.events({
'click a': function(evt){
evt.preventDefault();
location.href = evt.currentTarget.href;
}
})
which makes the site much slower, unfortunately.