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.
Related
Is there some chance to simple update _viewContainer??
I am trying create directive for material-button component, that add hidden classic button for submit form by hit enter key. I need to add simple html to _viewContainer, something like this
import 'package:angular/angular.dart';
#Directive(
selector: '[kpMaterialSubmit]',
)
class KpMaterialSubmit {
final TemplateRef _templateRef;
final ViewContainerRef _viewContainer;
KpMaterialSubmit(this._viewContainer, this._templateRef);
#Input()
set kpMaterialSubmit(bool showButton) {
_viewContainer.clear();
if (showButton) {
_viewContainer.createEmbeddedView(_templateRef);
//THIS IS THE IMPORTANT PART
_viewContainer.createMyCustomView('<button class="hidden">Submit</button>');
} else {
_viewContainer.createEmbeddedView(_templateRef);
}
}
}
I have done something similar and it works. I don't know if there is a better way.
Instead of using html text directly, create a component.
Add the import to the component template:
import 'package:<your package>/<path to component>/<component source name>.template.dart';
Then add the component with the createComponent() method (instead of createMyCustomView()):
_viewContainer.createComponent(<componentName>NgFactory);
That's all!
If you need to style the component or add any attribute you can get the reference and derive the HtmlElement instance:
ComponentRef _componentRef = _viewContainer.createComponent(<componentName>NgFactory);
HtmlElement element = _componentRef.location;
element.style.background = 'green';
element.setAttribute('value', 'text');
I'm trying to create a custom form control in Angular (v5). The custom control is essentially a wrapper around an Angular Material component, but with some extra stuff going on.
I've read various tutorials on implementing ControlValueAccessor, but I can't find anything that accounts for writing a component to wrap an existing component.
Ideally, I want a custom component that displays the Angular Material component (with some extra bindings and stuff going on), but to be able to pass in validation from the parent form (e.g. required) and have the Angular Material components handle that.
Example:
Outer component, containing a form and using custom component
<form [formGroup]="myForm">
<div formArrayName="things">
<div *ngFor="let thing of things; let i = index;">
<app-my-custom-control [formControlName]="i"></app-my-custom-control>
</div>
</div>
</form>
Custom component template
Essentially my custom form component just wraps an Angular Material drop-down with autocomplete. I could do this without creating a custom component, but it seems to make sense to do it this way as all the code for handling filtering etc. can live within that component class, rather than being in the container class (which doesn't need to care about the implementation of this).
<mat-form-field>
<input matInput placeholder="Thing" aria-label="Thing" [matAutocomplete]="thingInput">
<mat-autocomplete #thingInput="matAutocomplete">
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
{{ option }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
So, on the input changing, that value should be used as the form value.
Things I've tried
I've tried a few ways of doing this, all with their own pitfalls:
Simple event binding
Bind to keyup and blur events on the input, and then notify the parent of the change (i.e. call the function that Angular passes into registerOnChange as part of implementing ControlValueAccessor).
That sort of works, but on selecting a value from the dropdown it seems the change events don't fire and you end up in an inconsistent state.
It also doesn't account for validation (e.g. if it's "required", when a value isn;t set the form control will correctly be invalid, but the Angular Material component won't show as such).
Nested form
This is a bit closer. I've created a new form within the custom component class, which has a single control. In the component template, I pass in that form control to the Angular Material component. In the class, I subscribe to valueChanges of that and then propagate the changes back to the parent (via the function passed into registerOnChange).
This sort of works, but feels messy and like there should be a better way.
It also means that any validation applied to my custom form control (by the container component) is ignored, as I've created a new "inner form" that lacks the original validation.
Don't use ControlValueAccessor at all, and instead just pass in the form
As the title says... I tried not doing this the "proper" way, and instead added a binding to the parent form. I then create a form control within the custom component as part of that parent form.
This works for handling value updates, and to an extent validation (but it has to be created as part of the component, not the parent form), but this just feels wrong.
Summary
What's the proper way of handling this? It feels like I'm just stumbling through different anti-patterns, but I can't find anything in the docs to suggest that this is even supported.
Edit:
I've added a helper for doing just this an angular utilities library I've started: s-ng-utils. Using that you can extend WrappedFormControlSuperclass and write:
#Component({
selector: 'my-wrapper',
template: '<input [formControl]="formControl">',
providers: [provideValueAccessor(MyWrapper)],
})
export class MyWrapper extends WrappedFormControlSuperclass<string> {
// ...
}
See some more documentation here.
One solution is to get the #ViewChild() corresponding to the inner form components ControlValueAccessor, and delegating to it in your own component. For example:
#Component({
selector: 'my-wrapper',
template: '<input ngDefaultControl>',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NumberInputComponent),
multi: true,
},
],
})
export class MyWrapper implements ControlValueAccessor {
#ViewChild(DefaultValueAccessor) private valueAccessor: DefaultValueAccessor;
writeValue(obj: any) {
this.valueAccessor.writeValue(obj);
}
registerOnChange(fn: any) {
this.valueAccessor.registerOnChange(fn);
}
registerOnTouched(fn: any) {
this.valueAccessor.registerOnTouched(fn);
}
setDisabledState(isDisabled: boolean) {
this.valueAccessor.setDisabledState(isDisabled);
}
}
The ngDefaultControl in the template above is to manually trigger angular to attach its normal DefaultValueAccessor to the input. This happens automatically if you use <input ngModel>, but we don't want the ngModel here, just the value accessor. You'll need to change DefaultValueAccessor above to whatever the value accessor is for the material dropdown - I'm not familiar with Material myself.
I'm a bit late to the party but here is what I did with wrapping a component which might accept formControlName, formControl, or ngModel
#Component({
selector: 'app-input',
template: '<input [formControl]="control">',
styleUrls: ['./app-input.component.scss']
})
export class AppInputComponent implements OnInit, ControlValueAccessor {
constructor(#Optional() #Self() public ngControl: NgControl) {
if (this.ngControl != null) {
// Setting the value accessor directly (instead of using the providers) to avoid running into a circular import.
this.ngControl.valueAccessor = this;
}
}
control: FormControl;
// These are just to make Angular happy. Not needed since the control is passed to the child input
writeValue(obj: any): void { }
registerOnChange(fn: (_: any) => void): void { }
registerOnTouched(fn: any): void { }
ngOnInit() {
if (this.ngControl instanceof FormControlName) {
const formGroupDirective = this.ngControl.formDirective as FormGroupDirective;
if (formGroupDirective) {
this.control = formGroupDirective.form.controls[this.ngControl.name] as FormControl;
}
} else if (this.ngControl instanceof FormControlDirective) {
this.control = this.ngControl.control;
} else if (this.ngControl instanceof NgModel) {
this.control = this.ngControl.control;
this.control.valueChanges.subscribe(x => this.ngControl.viewToModelUpdate(this.control.value));
} else if (!this.ngControl) {
this.control = new FormControl();
}
}
}
Obviously, don't forget to unsubscribe from this.control.valueChanges
I have actually been wrapping my head around this for a while and I figured out a good solution that is very similar (or the same) as Eric's.
The thing he forgot to account for, is that you can't use the #ViewChild valueAccessor until the view has actually loaded (See #ViewChild docs)
Here is the solution: (I am giving you my example which is wrapping a core angular select directive with NgModel, since you are using a custom formControl, you will need to target that formControl's valueAccessor class)
#Component({
selector: 'my-country-select',
templateUrl: './country-select.component.html',
styleUrls: ['./country-select.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: CountrySelectComponent,
multi: true
}]
})
export class CountrySelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges {
#ViewChild(SelectControlValueAccessor) private valueAccessor: SelectControlValueAccessor;
private country: number;
private formControlChanged: any;
private formControlTouched: any;
public ngAfterViewInit(): void {
this.valueAccessor.registerOnChange(this.formControlChanged);
this.valueAccessor.registerOnTouched(this.formControlTouched);
}
public registerOnChange(fn: any): void {
this.formControlChanged = fn;
}
public registerOnTouched(fn: any): void {
this.formControlTouched = fn;
}
public writeValue(newCountryId: number): void {
this.country = newCountryId;
}
public setDisabledState(isDisabled: boolean): void {
this.valueAccessor.setDisabledState(isDisabled);
}
}
NgForm is providing an easy way to manage your forms without injecting any data in a HTML form. Input data must be injected at the component level not in a classic html tag.
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm)>...</form>
Other way is to create a form component where all the data model is binded using ngModel ;)
I have a laravel app and a Vue instance attached to the body (or a div, just inside the body).
const app = new Vue({
el: '#app'
});
I think it makes sense to use the Vue instance for stuff relating to the layout (eg header, nav, footer logic).
Now I have a form that is visible on a specific route (e.g. example.com/thing/create). I want to add some logic to it, e.g. hiding a field based on selected option in the form. It is logic meant for just this form (not to be reused). I prefer not to put all the logic inline with the form but put it in the app.js. I could put it in the Vue instance bound to the body but that sounds odd as it only applies to the form that is much deeper into the dom.
I want to leave the markup of the form in the blade template (that inherits the layout).
I tried creating a component but am not sure how to bind this inside the main Vue instance. What is the best way to handle things for this form, put it in the app.js and have it somewhat structured, putting the variables somewhat into scope. Or is it really necessary to remove the main Vue instance bound to the full layout code?
What I tried was something like this, but it does not work (attaching it to the <form id="object-form"> seems to fail:
var ObjectForm = {
template: function() { return '#object-form'},
data: function() {
return {
selectedOption: 1
}
},
computed: {
displayField: function() {
// return true or false depending on form state
return true;
}
}
};
Things do work if I remove the #app Vue instance or when I put everything directly in the app Vue instance. But that seems messy, if I have similar variables for another form they should be seperated somewhat.
I would appreciate some advice regarding the structure (differentiate page layout and page specific forms) and if possible some example to put the form logic inside the main app.js.
I hope this helps kind of break things down for you and helps you understand Vue templating.
It is best to take advantage of Vue's components. For you it would look something like this. Some of this code depends on your file structure, but you should be able to understand it.
In your app.js file (or your main js file)
Vue.component('myform',require('./components/MyForm.vue'));
const app = new Vue({
el: "#app"
});
Then create the MyForm.vue file
<template>
<form>
Put Your Form Markup Here
</form>
</template>
<script>
// Here is where you would handle the form scripts.
// Use methods, props, data to help set up your component
module.exports = {
data: function() {
return {
selectedOption: 1
}
},
computed: {
displayField: function() {
// return true or false depending on form state
return true;
}
},
methods: {
// add methods for dynamically changing form values
}
}
</script>
Then you will be able to just call in your blade file.
<myform></myform>
I found out how to do it. The trick was to use an inline template. Surround the form in the view with:
<object-form inline-template>
<form>...</form>
</object-form>
Where object-form is the name of the component. In the ObjectForm code above I remove the template, like this:
var ObjectForm = {
data: function() {
return {
selectedOption: 1
}
},
computed: {
displayField: function() {
// return true or false depending on form state
return true;
}
}
};
I attach the component within the root vue app like this:
const app = new Vue({
el: 'body',
components: {
'object-form': ObjectForm
}
});
This way I can use the form as it was generated from the controller and view and I can separate it from the root (attached to body) methods and properties.
To organize it even better I can probably store the ObjectForm in a seperate .vue file the way #Tayler Foster suggested.
I'd like to use headroom.js with React. Headroom.js docs say:
At it's most basic headroom.js simply adds and removes CSS classes from an element in response to a scroll event.
Would it be fine to use it directly with elements controlled by React? I know that React fails badly when the DOM structure is mutated, but modifying just attributes should be fine. Is this really so? Could you show me some place in official documentation saying that it's recommended or not?
Side note: I know about react-headroom, but I'd like to use the original headroom.js instead.
EDIT: I just tried it, and it seems to work. I still don't know if it will be a good idea on the long run.
If React tries to reconcile any of the attributes you change, things will break. Here's an example:
class Application extends React.Component {
constructor() {
super();
this.state = {
classes: ["blue", "bold"]
}
}
componentDidMount() {
setTimeout(() => {
console.log("modifying state");
this.setState({
classes: this.state.classes.concat(["big"])
});
}, 2000)
}
render() {
return (
<div id="test" className={this.state.classes.join(" ")}>Hello!</div>
)
}
}
ReactDOM.render(<Application />, document.getElementById("app"), () => {
setTimeout(() => {
console.log("Adding a class manually");
const el = document.getElementById("test");
if (el.classList)
el.classList.add("grayBg");
else
el.className += ' grayBg';
}, 1000)
});
And here's the demo: https://jsbin.com/fadubo/edit?js,output
We start off with a component that has the classes blue and bold based on its state. After a second, we add the grayBg class without using React. After another second, the component sets its state so that the component has the classes blue, bold, and big, and the grayBg class is lost.
Since the DOM reconciliation strategy is a black box, it's difficult to say, "Okay, my use case will work as long as React doesn't define any classes." For example, React might decide it's better to use innerHTML to apply a large list of changes rather than setting attributes individually.
In general, if you need to do manual DOM manipulation of a React component, the best strategy is to wrap the manual operation or plugin in its own component that it can 100% control. See this post on Wrapping DOM Libs for one such example.
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