I have a row-based (table-like) custom element, which renders its template dynamically based on a bindable columns attribute. It composes the row template in string and uses ViewCompiler, ViewFactory and View to render.
import {inject} from 'aurelia-dependency-injection';
import {bindable, ViewCompiler, ViewResources} from 'aurelia-templating';
#inject(ViewCompiler, ViewResources)
export class MyDynamicGrid {
#bindable columns: any[];
#bindable rows: any[];
constructor(viewCompiler: ViewCompiler, viewResources: ViewResources) {
const template = '<template><custom-element></custom-element></template>'; //this is rather complex in practice
viewResources.registerElement('custom-element', /* HtmlBehaviorResource? */);
this._viewFactory = viewCompiler.compile(template, viewResources);
}
_render() : void {
const view = this._viewFactory.create(/* some container */);
view.bind(someContext, someOverrideContext);
//attach view to the DOM
}
}
This works fine, until the custom template contains standard HTML elements. Once I start putting custom elements in the template, it stops working. It still renders the HTML, but the custom element's behaviour is not being attached by Aurelia.
I'm aware, that all custom elements should be "registered" in order to be used. "Normal registration" happens either in the view via <require>, or in the view-model #viewResources, or registering it globally.
In this particular case, however, the injected ViewCompiler only inherits the view resources of the view-model's parents. My question is: how can be any additional view-resources registered? I'm aware of the second parameter in ViewCompiler's compile method, but couldn't make it work. The only way I was able to make it work, if I register it globally.
Note: this question is focusing on registering view resources. The dynamic rendering works just fine
I've found a solution by diggin into the docs+github. I've created two samples for two different approaches:
Create HtmlBehaviorResource instance manually, this is to register one specific element.
Sample (based on: https://github.com/aurelia/templating-resources/blob/master/test/repeat-integration.spec.js)
import {CustomElement} from 'my-components/my-custom-element';
import {inject, Container} from 'aurelia-dependency-injection';
import {ViewCompiler, ViewResources} from 'aurelia-templating';
import {metadata} from 'aurelia-metadata';
#inject(ViewCompiler, ViewResources, Container)
export class MyDynamicGrid {
//bindables and constructor is ommitted
init(): void {
const resource: HtmlBehaviorResource = metadata.get(metadata.resource, CustomElement);
resource.initialize(this._container, CustomElement);
resource.load(this._container, CustomElement)
.then(() => {
resource.register(this._viewResources);
this._viewFactory = viewCompiler.compile(template, this._viewResources);
});
}
}
Remark: the line resource.register(this._viewResources); is equivalent to this._viewResources.registerElement('custom-element', resource);. The only difference is the first reads the name from convention or decorator.
Use ViewEngine and import (a) whole module(s). This is more appropriate if importing multiple resources, even different types (attribute, element, converter, etc.) and from different files.
Sample:
import {CustomElement} from 'my-components/my-custom-element';
import {inject} from 'aurelia-dependency-injection';
import {ViewCompiler, ViewResources, ViewEngine} from 'aurelia-templating';
#inject(ViewCompiler, ViewResources, ViewEngine)
export class MyDynamicGrid {
//bindables and constructor is ommitted
init(): void {
this._viewEngine
.importViewResources(['my-components/my-custom-element'], [undefined], this._viewResources)
.then(() => {
this._viewFactory = viewCompiler.compile(template, this._viewResources);
});
}
}
I've found your question while struggling with the same problem, but I think I managed to make it work with resources parameter of compile. Here is my setup:
I wrapped the compilation into a helper class:
#autoinject
export class ViewFactoryHelper {
constructor(resources, viewCompiler, container, loader) {
}
compileTemplate(html, bindingContext) {
let viewFactory = this.viewCompiler.compile(html, this.resources);
let view = viewFactory.create(this.container);
view.bind(bindingContext);
return view;
}
}
And then the client looks like this:
#autoinject
export class SomethingToCreateDynamicControls {
constructor(viewFactoryHelper, myCustomConverter, anotherCustomConverter) {
viewFactoryHelper.resources.registerValueConverter('myCustomConverter', myCustomConverter);
viewFactoryHelper.resources.registerValueConverter('anotherCustomConverter', anotherCustomConverter);
}
makeControl(model) {
let html = '...'; // HTML that uses those converters in normal way
let compiledTemplate = this.viewFactoryHelper.compileTemplate(html, model);
// ...
}
}
UPDATE: So far I'm not able to call registerElement instead of registerValueConverter with desired effect, so my answer is probably not a good one yet. I'll keep trying...
Related
I have a rich text element in Contentful that has an embedded entry within it. I am attempting to have that embedded entry render inside a custom Angular component. However, when I pass in options to the Contentful documentToHtmlString function, it displays the tag, but does not render anything or even trigger the console.log() function I have inside the custom component typescript file.
TYPESCRIPT for the rendering
convertToHTML(document:any[]) {
const options = {
renderNode: {
[BLOCKS.EMBEDDED_ENTRY]: (node:any, next:any) => `<embed-element [node]="${node}" [content]="${next(node.content)}"></embed-element>`
}
}
let unsafe = documentToHtmlString(document, options);
return this.sanitizer.bypassSecurityTrustHtml(unsafe);
}
HTML for the rendering
<span [innerHTML]="convertToHTML(article.fields.content)"></span>
I have loaded the custom element <embed-element></embed-element> in the providers section of the app.module.ts file as well.
import { EmbedElementComponent } from './shared/components/embed-element/embed-element.component';
#NgModule({
declarations: [
...
EmbedElementComponent
],
providers: [
...
EmbedElementComponent
]
})
Then inside typescript file for the custom element, I just simply have a console.log in the onInit function for testing purposes. I am not seeing this occur in the console.
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'embed-element',
templateUrl: './embed-element.component.html',
styleUrls: ['./embed-element.component.css']
})
export class EmbedElementComponent implements OnInit {
#Input() node: any;
#Input() content: any;
constructor() { }
ngOnInit(): void {
console.log(this.node);
}
}
And in the HTML for the custom element, I removed the <p> tag just in case it was stripping it out as "unsafe" and replaced it with the following:
EMBEDDED ELEMENT WORKS!!!!!
Finally, I see nothing appear on screen for this custom element once everything is rendered. Inside the inspect element however, this is what I get.
<embed-element [node]="[object Object]" [content]=""></embed-element>
How do I manage to make the custom element actually get called in this aspect? And at least receive the console log message I am requesting inside the custom element?
Not sure if you still needed another solution, but I came across ngx-dynamic-hooks https://github.com/MTobisch/ngx-dynamic-hooks - and was able to reuse my custom components within the inner html.
const cmpt_data = {
title: 'This is a test'
};
const headerFooterRichTextOption: Partial<Options> = {
renderNode: {
["embedded-entry-inline"]: (node, next) => `${this.embeddedContentfulEntry(node.nodeType, node.data)}`,
["paragraph"]: (node, next) => `<span>${next(node.content)}</span>`,
}
};
const d = documentToHtmlString(tt.value, headerFooterRichTextOption);
const hSanit = this.sanitizer.bypassSecurityTrustHtml(d);
embeddedContentfulEntry(nodeType: any, data: any) {
return "<custom-component [title]='context.title'></custom-component>";
}
<ngx-dynamic-hooks [content]="hSanit" [context]="cpmt_data"></ngx-dynamic-hooks>
I believe that using a custom component for this type of situation was not working because it was rendering outside of Angulars scope or after initial components initiation.
I resolved this issue by essentially just removing the custom element all together and creating a function that renders the embedded element as I wanted.
// Notice the new function call, renderCustomElement()
convertToHTML(document:any[]) {
const options = {
renderNode: {
[BLOCKS.EMBEDDED_ENTRY]: (node:any, next:any) => this.renderCustomElement(node)
}
}
let unsafe = documentToHtmlString(document, options);
return this.sanitizer.bypassSecurityTrustHtml(unsafe);
}
And finally that function that simply creates some basic html with Bootstrap styling and returns that back to be rendered.
renderCustomElement(node:any) {
let link = `/support-center/article/${node.data.target.fields.category[0].sys.id}/${node.data.target.sys.id}`;
return `<style>.card:hover{box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 15%) !important;}</style><a class="card clickable mb-2 text-decoration-none text-dark" href="${link}" target="_blank"><div class="card-header">Article</div><div class="card-body"><h4 class="fw-light">${node.data.target.fields.title}</h4><p class="fw-light pb-0 mb-0">${node.data.target.fields.preview}</p></div></a>`
}
I have a class "MessageDisplay" of which I want to call the function sendMassageToServer from the outside. I´ve built a helper function to call from another file. But how do you export functions that are inside classes?
These data are just examples.
main.js
export function sendSpeechToServer(query){
MessageDisplay.sendMessageToServer(query);
}
class MessageDisplay extends React.Component {
constructor(props) {
super(props);
this.state = {message : []};
}
(export const??) sendMessageToServer(searchQuery) {
...code
}
}
We are accesing the sendSpeechToServer() function from another file. Unortunately I am not even able to reach sendMessageToServer() from inside sendSpeechToServer().
Any help surely is appreciated. :)
EDIT:
The answer is found. For any other people:
export function sendSpeechToServer(query){
let test = new MessageDisplay();
test.sendMessageToServer(query);
}
Better way to separate component(MessageDisplay) and sendMessageToServer.
Then you can import sendMessageToServer awry where. You can inject sendMessageToServer like a props:
// main.js
import { sendMessageToServer } from './api';
<MessageDisplay sendMessage={sendMessageToServer} />
// MessageDisplay.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class MessageDisplay extends Component {
static propTypes = {
sendMessage: PropTypes.func.isRequired,
}
handleClick = (e) => {
e.preventDefault();
this.props.sendMessage();
};
render() {
return (<button onClick={this.handleClick}>Send to</button>)
}
}
export default MessageDisplay;
It useful for testing.
Instantiating a component manually for general purposes like let test = new MessageDisplay() is an antipattern, this indicates that a class is misused.
React component classes are primarily intended to make lifecycle hooks available and maintain state. They can sparsely benefit from inheritance (besides the relationship with React.Component) and other OOP traits.
The fact that it's possible to use component method as new MessageDisplay().sendMessageToServer(query) means that it was a mistake to make it component method in the first place. Classes aren't supposed to be glorified namespaces; ES modules play the role of namespaces in modern JavaScript.
A proper way is to extract the method and use it in both places as regular helper function. Functional approach is considered idiomatic in React.
export function sendSpeechToServer(query){
let test = new MessageDisplay();
test.sendMessageToServer(query);
}
it is bad, because you should not, create new react.component instance with new keyword,
better use static function like this
static sendMessageToServer(searchQuery) {
...code
}
and then
export function sendSpeechToServer(query){
MessageDisplay.sendMessageToServer(query);
}
I want to use mobx without using decorators. Usually I use decorate from mobx package but in this particular case, I could not find a way to make it work.
Original code :
import { observable } from 'mobx'
import { create, persist } from 'mobx-persist'
class Order {
#persist('object')
#observable
currentOrder = null
}
What I tried :
import { observable, decorate } from 'mobx'
import { create, persist } from 'mobx-persist'
import { compose } from 'recompose'
class Order {
currentOrder = null
}
decorate(Order, {
currentOrder: compose(persist('object'), observable),
})
The error comes from persist telling serializr decorator is not used properly.
Any idea why this is different from above and does not work ?
TL;DR
Property Decorators requires a very specific composition implementation.
Solution Demo:
Full Answer
Property Decorators are basically a function of the form:
(target, prop, descriptor) => modifiedDescriptor
So, in order to compose two Property Decorators you need to pass the 1st decorator's result as a third argument of the 2nd decorator (along with target and prop).
Recompose.compose (same as lodash.flowRight) applies functions from right to left and passing the result as a single argument to the next function.
Thus, you can't use Recompose.compose for composing decorators, but you can easily create a composer for decorators:
/* compose.js */
export default (...decorators) => (target, key, descriptor) =>
decorators.reduce(
(accDescriptor, decorator) => decorator(target, key, accDescriptor),
descriptor
);
Then we use it in order to compose observable and persist("object").
/* Order.js */
import { observable, decorate, action } from "mobx";
import { persist } from "mobx-persist";
import compose from "./compose";
class Order {
currentOrder = null;
}
export default decorate(Order, {
currentOrder: compose(
observable,
persist("object")
)
});
[21/8/18] Update for MobX >=4.3.2 & >=5.0.4:
I opened PRs (which have been merged) for MobX5 & MobX4 in order to support multiple decorators OOB within the decorate utility function.
So, this is available in MobX >=4.3.2 & >= 5.0.4:
import { decorate, observable } from 'mobx'
import { serializable, primitive } from 'serializr'
import persist from 'mobx-persist';
class Todo {
id = Math.random();
title = "";
finished = false;
}
decorate(Todo, {
title: [serializable(primitive), persist('object'), observable],
finished: observable
})
An easier solution is to have
class Stuff {
title = ''
object = {
key: value
}
}
decorate(Todo, {
title: [persist, observable],
object: [persist('object'),observable]
})
No need to install the serializr package. The above functionality is built into mobx persist.
I have created a simple class in Angular using the CLI starter. When I imported the file and tried to use the class it didn't work. It was basically just an empty object being returned. I played around trying to figure out what the issue was and decided to capitalize the file name from store to Store in the import and it works.
Class:
export class Store {
private cards: Object = {};
private previousId: number = -1;
private addCard(card: Card) {
this.cards[card['id']] = card;
return card['id'];
}
public getCard(id) {
return this.cards[id];
}
public create(text: string) {
// Do some stuff...
}
}
Class Usage:
ngOnInit() {
this.getColumsConfig();
this.store = new Store();
console.log(this.store);
}
After newing up the class I got an empty object:
I named the file store.ts, and I initially imported it as follows:
import { Store } from '../../services/store';
I then renamed the import to the following and it worked:
import { Store } from '../../services/Store';
Does anyone know why? I tried to google this but had no success.
File named in IED:
This was happening because I forgot the add the store service to the providers array.
I am using an ionic framework package, and I have a sidebar class. There is a class called ionSideMenu.snapper, which, looking at the source code, is defined when the ionSideMenuContainer template is rendered, so I get around that by doing the following:
import {Template} from "meteor/templating";
import {ReactiveField} from "meteor/peerlibrary:reactive-field";
export const Snapper = new ReactiveField(false);
// now when the side menu is rendered, `IonSideMenu.snapper` should be defined.
Template['ionSideMenuContainer'].onRendered(() => Snapper(IonSideMenu.snapper));
However, I want to create a static method on my Sidebar class that will await for the Snapper to be defined, then will run the disabled function.
export class Sidebar extends BlazeComponent {
static disable() {
// await the snapper to be defined
Snapper().disable();
}
}
How could this awaiting function be done, such that I could call it from any other template and it will disable when it is rendered?
Believe I have found an answer. Posting it if anyone needs it.
import {Tracker} from "meteor/tracker";
// ...
class Sidebar extends BlazeComponent {
static _awaitSnapperReady(callback) {
Tracker.autorun(c => {
const snapper = Snapper();
if (snapper) {
callback(snapper);
c.stop(); // end the current computation.
}
});
}
static disable() {
Sidebar._awaitSnapperReady(snapper => snapper.disable());
}
}