I am developing an Angular2 app, and I faced a problem:
I have a set of different objects that can be selected using UI. Each of this objects has a set of options (different for different objects) that could be edited using UI. Now, I am using DynamicComponentLoader to insert a specific component for currently selected object, so it can handle its options correctly.
The problem is that I don't know how to bind data of currently selected object to a dynamically inserted options component.
#Component({
selector: 'dynamic',
template: `<div>Options:</div>
<div>Property1: <input type="number" /></div>
<div>Property2: <input type="text" /></div>`
// template: `<div>Options:</div>
// <div>Property1: <input type="number" [(ng-model)]="currentSelection.property1" /></div>
// <div>Property2: <input type="text" [(ng-model)]="currentSelection.property1" /></div>`
})
class DynamicComponent {
}
#Component({
selector: 'my-app',
template: `
<div>
<h2>Selected: {{currentSelection.name}}!</h2>
<div #container></div>
</div>
`
})
class App {
currentSelection = {name: 'Selection1', property1: 10, property2: 'test'};
constructor(private loader: DynamicComponentLoader, private elementRef: ElementRef) {
loader.loadIntoLocation(DynamicComponent, elementRef, 'container');
}
}
Here is a plunker to help you understand my question:
With angular2 and Rxjs, "Observables" are almost always the answer.
If i understood your problem correctly, you need to make your DynamicComponent an "Observer" and your container "an Observable or even better a Subject (In case your container needs to subscribe to another observable to receive selections from)". Then, after loading your dynamic component, subscribe it to your container.
Whenever the selection changes on your container, you push the new selection to your subscribers. This way, you can load multiple dynamic components and all will receive your pushes.
The Container:
class App {
currentSelection = {};
selections = [
{name: 'Selection1', property1: 10, property2: 'test'},
{name: 'Selection2', property1: 20, property2: 'test2'}
];
subject:Subject<any> = new Subject();
constructor(private loader: DynamicComponentLoader, private elementRef: ElementRef) {
}
ngOnInit(){
this.loader.loadIntoLocation(DynamicComponent, this.elementRef, 'container', this.injector)
.then(compRef =>this.subject.subscribe(compRef.instance));
// subscribe after loading the dynamicComponent
}
// set the new selection and push it to subscribers
changeSelection(newSelection){
this.currentSelection = newSelection;
this.subject.next(this.currentSelection);
}
}
The Observer:
class DynamicComponent implements Observer{
public currentSelection = {};
next(newSelection){
this.currentSelection = newSelection;
}
}
Here is your plunker working after my edits, "provided I changed the imports to the newest angular beta.6"
I know this is a quite old question. But hopefully someone will benefit from this answer.
Here is what you can do, move your code from constructor to ngOnInit and use promises for assigning dynamic value.
ngOnInit(){
this.dynamicComponentLoader.loadIntoLocation(DynamicComponent, this.elementRef,'container').then((component)=>{
component.instance.currentSelection = currentSelection;
});
}
Related
I have a VueJs component that allows the v-model="form.title" parameter:
<template>
<wysiwyg-editor :value="modelValue" #update="$emit('update:modelValue', $event.target.value)" />
</template>
<script>
import WysiwygEditor from './WysiwygEditor'
export default {
emits: ['update:modelValue'],
components: { WysiwygEditor },
props: {
modelValue: {
type: String,
required: false,
default: null
}
},
watch: {
modelValue (to) {
this.editor.setValue(to)
}
}
}
</script>
Problem of that is that whenever the user writes in the input field, the data is $emit to the parent, which gets the new value and triggers down the watch on the current element, which updates the value back, causing writing issues.
I need to have the watch property because the parent can modify the value that needs to be reflected in this component.
I thought about using the mounted callback on the component, but the component stays on the page, so it isn't mounted at every change from the parent.
How can I do to avoid a circular call?
I want to create a component that provides a list of selections to the user but allows only one selection to be made at any given time. The functionality of the mat-radio-group seems to fit that bill the best, but I don't want the actual radio button to be rendered next to the labels within my radio group. I want to expand the label and make any (change) event fire from a click event on the label itself.
What is the least "hacky" way to eliminate the radio buttons from my radio group, while keeping the labels as they were?
you can also make a custom form control. The idea is that has a .html like
<select-component [(ngModel)]="valor">
<div select value="1">One</div>
<div select>Two</div>
</select-component>
We are going to make a directive that the selector was [select]
#Directive({
selector: '[select]',
})
export class SelectDirective implements AfterViewInit {
#Input('value')value:any;
control:any;
#HostBinding('class.selected')
get isSelected(){
return this.control && this.control.value==this.value?true:undefined
}
#HostBinding('class.select')setClass(){return true}
#HostListener('click') onclick() {
console.log(this.value);
if (this.control)
this.control.setValue(this.value)
}
constructor(private el:ElementRef){}
ngAfterViewInit()
{
this.value=this.value ||this.el.nativeElement.innerHTML
}
}
See that, in ngAfterViewInit we give value to this.value as the innerHTML of the div case you has not defined the value
There're two class binding, one .select -this alow us give .css to our component from app.main.component, one .selected, when the div was "selected".
The SelectComponent is a tipical custom form control, the "interesting" is that, in ngAfterViewInit, we ask about the "select" directive inside to allow comunicate the directive and the component
#Component({
selector: 'select-component',
template: `<ng-content></ng-content>`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SelectComponent),
multi: true
}
]
})
export class SelectComponent implements ControlValueAccessor,AfterViewInit {
#ContentChildren(SelectDirective)selects:QueryList<SelectDirective>;
value:any;
disabled:boolean=false;
onChange:any;
onTouched:any;
writeValue(value: any[]|any): void {
this.value=value;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled=isDisabled
}
ngAfterViewInit()
{
this.selects.forEach(x=>{
x.control=this
})
}
setValue(value)
{
this.value=value
this.onChange(value)
}
}
And, voila!, the stackblitz
I'm a noob and I'm trying to compile a JSX following this tutorial :
http://xabikos.com/2015/03/18/Using-Reactjs-net-in-Web-Forms/
using reactjs.net``
In my helloWorld.jsx
class PageContent extends React.Component {
componentDidMount() {
this.state = { name: "Hello World from ComponentDidMount" };
}
constructor(props) {
super(props);
this.state = { name: "Hello World" };
}
componentWillMount() {
this.state = { name: "Hello World from ComponentWIllMount" };
}
render() {
return <h1>{this.state.name}</h1>
}
}
In my ReactConfig.cs
ReactSiteConfiguration.Configuration
.AddScript("~/Scripts/components/helloWorld.jsx");
In my Default.aspx.cs
var env = AssemblyRegistration.Container.Resolve<IReactEnvironment>
();
var objectModel = new { user = "React User" };
var reactComponent = env.CreateComponent("PageContent",
objectModel);
PageContent.Text = reactComponent.RenderHtml();
The page seems to works fine
it prints
"Hello World from Component Will Mount"
But when I comment the Component Will Mount, I dont receive anything from DidMount, it only prints
"Hello World"
Does anybody know why is this method never called ?
Thank you in advance
Your problem is you're not using this.setState. You are manually assigning a new value in this.state. React does not know to call render again to update your component, which is why your component doesn't update. It's not that this.state has not changed. It's that React hasn't been told to update what's shown on the screen. So, here's what happens with your code in terms of life cycles:
constructor: initializes everything (duh)
componentWillMount: In your case, setting this.state.name. render does not occur yet
render: renders this.state.name, which you have set in your componentWillMount
componentDidMount: sets this.state.name to your new value. no instructions to update your render function
If you use this.forceUpdate() in your componentDidMount, after you assign this.state.name to its new value, I believe it would update, but this is very bad practice. Use this.setState instead.
componentDidMount() {
this.setState({ name: "Hello World from ComponentDidMount" });
}
componentWillMount() {
this.setState({ name: "Hello World from ComponentWillMount" });
}
As you've noticed in your code, this.setState isn't entirely necessary in componentWillMount, because render has not been called yet. But it's good to just keep things consistent.
I am following the Meteor - Angular2 tutorial and things work fine.
The only point not working is the automatic binding with Angular2 UI for the 'details view'. For instance, if I navigate to the details view of Party1 the data of Party1 is correctly loaded and made visible on the Angular2 'details view'. If, afterwards, the data of Party1 is changed (e.g. via Mongo shell) such change is sent to the browser (via WebSockets) where 'details view' is displayed, but the new data is not shown on the view.
Here is the code of the PartyDetailsComponent class.
export class PartyDetailsComponent extends MeteorComponent implements OnInit, CanActivate {
partyId: string;
party: Party;
constructor(private route: ActivatedRoute, private ngZone: NgZone) {
super();
}
ngOnInit() {
this.route.params
.map(params => params['partyId'])
.subscribe(partyId => {
this.partyId = partyId;
this.subscribe('party', this.partyId, () => {
this.party = Parties.findOne(this.partyId);
}, true);
});
}
saveParty() {
Parties.update(this.party._id, {
$set: {
name: this.party.name,
description: this.party.description,
location: this.party.location
}
});
}
canActivate() {
const party = Parties.findOne(this.partyId);
console.log(party);
return (party && party.owner == Meteor.userId());
}
}
Here is the template of of PartyDetailsComponent
<form *ngIf="party" (submit)="saveParty()">
<label>Name</label>
<input type="text" [(ngModel)]="party.name" name="name">
<label>Description</label>
<input type="text" [(ngModel)]="party.description" name="description">
<label>Location</label>
<input type="text" [(ngModel)]="party.location" name="location">
<button type="submit">Save</button>
<a [routerLink]="['/']">Cancel</a>
</form>
Thanks in advance for any help
I actually found the answer to my questions just reading more of the Tutorial.
I can get automatic update ofthe UI once the underlying Mongo doc changes just adding Meteo autorun() method appropriately in the subscription code.
Here is the code that works
ngOnInit() {
this.route.params
.map(params => params['partyId'])
.subscribe(partyId => {
this.partyId = partyId;
this.subscribe('party', this.partyId, () => {
this.autorun(() => {
this.party = Parties.findOne(this.partyId);
}, true);
}, true);
});
}
What is not totally clear to me is why if you use directly the Meteo Mongo cursors (e.g. via *ngFor in the template) autorun is not needed.
I'm just wondering if someone has already been able to change the color of a row, in React Griddle, by clicking on it (just once).
I'm experimenting things with JQuery, and even with Griddle Metadata, but it may be done in a cleaner way ?
Edit : I'm using React 15, Griddle inside MantraJS/Meteor, getting the data in my react Component using a Mantra container.
I can get the data by using onClick event, but not able to switch the background color in the onClick event, or playing with Metadatas.
Thanks !
EDIT : I use another view to display the content of the table, so for now I don't need to change the background of my tables cells, but if I found a solution I'll complete this post
You can use react-griddle props rowMetadata and onRowClick to do this:
class ComponentWithGriddle extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedRowId: 0,
};
}
onRowClick(row) {
this.setState({ selectedRowId: row.props.data.id });
}
render() {
const rowMetadata = {
bodyCssClassName: rowData => (rowData.id === this.state.selectedRowId ? 'selected' : ''),
};
return (
<Griddle
...
rowMetadata={rowMetadata}
onRowClick={this.onRowClick.bind(this)}
/>
);
}
}
Now this adds a selected class to the selected <tr> elements, so you can use custom styles to add colors or whatever styles you want to apply to the selected row.
Note that a more convenient API for selecting rows has been called for in the Griddle Github issues.
For whatever reason, I couldn't get Waiski's answer to work for me at all. I'm assuming that something must have changed in Griddle over the past two years. It looks like the current prevailing advice on the Web is to "implement row selection as a plugin", but I couldn't find any examples of that either. After a long hard look at the code for the Position plugin’s TableEnhancer on GitHub and a bunch of trial and error I eventually managed to cobble together the following row selection plugin for Griddle in TypeScript:
import * as React from "react";
import * as Redux from "redux";
import Griddle, { connect, GriddlePlugin, components } from "griddle-react";
export type RowId = string | number;
export type RowClickHandler = (event: React.MouseEvent<Element>, rowId: RowId) => void;
export type RowIdGetter<TData> = (rowData: TData) => RowId;
export interface IRowEnhancerProps {
rowClickHandler: RowClickHandler;
rowId: RowId;
isSelected: boolean;
}
export class RowSelector<TData> {
private _rowClickHandler: RowClickHandler = null;
private _rowIdGetter: RowIdGetter<TData>;
constructor(rowClickHandler: RowClickHandler, rowIdGetter: (rowData: TData) => RowId) {
this._rowClickHandler = rowClickHandler;
this._rowIdGetter = rowIdGetter;
}
public rowIdToSelect: RowId;
public plugin: GriddlePlugin = {
components: {
RowEnhancer: (OriginalComponent: React.ComponentClass<components.RowProps>) =>
this.rowSelectionEnhancer(OriginalComponent)
}
}
private rowSelectionEnhancer(
OriginalComponent: React.ComponentClass<components.RowProps>
): React.ComponentClass<components.RowProps> {
const rowDataSelector = (state, { griddleKey }) => {
return state
.get('data')
.find(rowMap => rowMap.get('griddleKey') === griddleKey)
.toJSON();
};
return Redux.compose(
connect((state, props) => {
const rowData: TData = rowDataSelector(state, props as { griddleKey });
const rowId: RowId = this._rowIdGetter(rowData);
return {
...props,
rowClickHandler: this._rowClickHandler,
rowId: rowId,
isSelected: rowId.toString() === this.rowIdToSelect.toString()
};
})
)(class extends React.Component<IRowEnhancerProps, any>{
public render() {
return (
<OriginalComponent
{...this.props}
onClick={(event) => this.props.rowClickHandler(event, this.props.rowId)}
className={this.props.isSelected ? "selected" : ""}
/>
);
}
});
}
}
Here's a rough outline of how it's used by a component. (Note that I had to selectively extract this example from a much larger and more complicated component, so there might be some errors/inconsistencies; sorry about that. It should still give a good overall idea of the approach.)
import * as React from "react";
import Griddle, { RowDefinition, plugins, GriddlePlugin} from "griddle-react";
import * as MyGriddlePlugins from "../GriddlePlugins";
export interface IPartInfo {
serialNumber: number,
name: string,
location: string
}
export interface IPartListProps{
parts: IPartInfo[],
selectedSerialNumber: number
}
export class PartList extends React.Component<IPartListProps, void > {
private rowSelector: MyGriddlePlugins.RowSelector<IPartInfo>;
private rowIdGetter: MyGriddlePlugins.RowIdGetter<IPartInfo>;
constructor(props?: IPartListProps, context?: any) {
super(props, context);
this._rowClickHandler = this._rowClickHandler.bind(this);
this.rowSelector = new MyGriddlePlugins.RowSelector(
this._rowClickHandler,
this._rowIdGetter);
}
private _rowClickHandler: MyGriddlePlugins.RowClickHandler =
(event: React.MouseEvent<Element>, selectedSerialNumber: MyGriddlePlugins.RowId) => {
if (selectedSerialNumber !== this.props.selectedSerialNumber) {
/*
Set state, dispatch an action, do whatever. The main point is that you
now have the actual event from the click on the row and the id value from
your data in a function on your component. If you can trigger another
render pass from here and set a fresh value for this.rowSelector.rowIdToSelect
then the "selected" CSS class will be applied to whatever row this click
event just came form so you can style it however you like.
*/
}
}
private _rowIdGetter: (rowData: IPartInfo) => MyGriddlePlugins.RowId =
(rowData: IPartInfo) => rowData.serialNumber;
public render(): JSX.Element {
this.rowSelector.rowIdToSelect = this.props.selectedSerialNumber;
return (
<div>
<Griddle
data={this.props.parts}
plugins={[plugins.LocalPlugin, this.rowSelector.plugin]}
>
<RowDefinition>
<ColumnDefinition id="name" title="Part Name" />
<ColumnDefinition id="location" title="Installed Location" />
<ColumnDefinition id="serailNumber" title="Serial Number" />
</RowDefinition>
</Griddle>
</div>
);
}
}
So, what's actually going on here? The component creates an instance of the plugin class at instantiation time, passing in an event handler to capture the click on the row and an accessor function to retrieve your ID value (not an inscrutable internal ID) from a row of your data. Just before the component returns its rendering, a value is set on the component's instance of the plugin, that way, when Griddle renders the plugin has the data to figure out when it's on a selected row and then adjust the CSS accordingly. The handler function from your component is then assigned to the row's onClick handler so your component can get the data from the click and do whatever it needs to do.
This passes the "It works for me" test (on React 15.6) which in my case is a straightforward master/detail view driven by a traditional table implemented through Griddle. I have no idea how well it would work with some of Griddle's more advanced features.