i'm expanding angular.dart tutorial for some simple CRUD operations. I'm trying to do nice, smart combobox (dropdown box) that is taking List of KeyValuePairs and returning Selected KeyValyePair.
Unfortunatly i'm getting this error.
Model did not stabilize in 10 digests. Last 3 iterations:
itemsMap: collection: Instance of 'KeyValuePair'[null -> 0], Instance of 'KeyValuePair'[null -> 1]...
There is code.
dropdown_box.dart
library dropdown_box;
import 'package:angular/angular.dart';
import 'dart:core';
import 'package:tutorial/service/KeyValuePair.dart';
import 'dart:async';
#Component(selector: 'dropdownbox', templateUrl: 'dropdown_box.html', publishAs: 'dropCtrl')
class DropDownComponent {
// #NgOneWay('items-map')
// Map<String, String> itemsMap;
#NgOneWay('items-map')
List<KeyValuePair> itemsMap;
#NgTwoWay('selected-keyvalue')
KeyValuePair selectedKeyValue;
// void printit(item) {
// new Future(() {
// print("${item.value + ' '+ item.key}");
// SelectedKeyValue = new KeyValuePair(item.key, item.value);
// });
String selectedKey;
void setKeyAsSelected() {
new Future(() {
Iterable keyvaluepairs = itemsMap.where((i) => i.key == selectedKey);
if (keyvaluepairs.length > 0) {
selectedKeyValue = keyvaluepairs.elementAt(0);
}
});
}
}
dropdown_box.html
<div class=dropdownbox">
<select ng-model="selectedKey" ng-change="setKeyAsSelected()">
<option ng-value=""></option>
<option ng-repeat="item in itemsMap" ng-value="item.key">
{{item.value}}
</option>
</select>
</div>
use in recipe_book.html
<dropdownbox items-map="categorieKvList" selected-keyvalue="selectedKV"></dropdownbox>
recipe_book.dart (important part only)
KeyValuePair selectedKV;
List<KeyValuePair> _categorieKvList = new List<KeyValuePair>();
List<KeyValuePair> get categorieKvList {
_categorieKvList.clear();
categories.forEach((f)=>
_AddToList(f, f));
return _categorieKvList;
}
_AddToList(key, value)
{
KeyValuePair kvpair = new KeyValuePair.fromValues(key, value); //new KeyValuePair().AddValues(key, value);
_categorieKvList.add(kvpair);
}
keyvaluepair.dart
library KeyValuePair;
import 'package:angular/angular.dart';
#Injectable()
class KeyValuePair {
dynamic key;
dynamic value;
// AddValues(key, value) {
// this.key = key;
// this.value = value;
// }
KeyValuePair();
KeyValuePair.fromValues(dynamic this.key, dynamic this.value);
}
The getter of categorieKvList returns a changed _categorieKvList with new KeyValu pairs every time it is called. This is interpreted by angular as change.
Alter the logic to so _categorieKvList is modified only when its data source (categories) changes.
Related
I met a problem while I implemented a angular application with material tree view.
thie tree view is dyniamic, that means that you can add child node if you click add button.
the HTML code looks like this
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle matTreeNodePadding>
<button mat-icon-button disabled></button>
<mat-checkbox class="checklist-leaf-node"
[checked]="checklistSelection.isSelected(node)"
(change)="todoLeafItemSelectionToggle(node)">{{node.item}}</mat-checkbox>
</mat-tree-node>
<mat-tree-node *matTreeNodeDef="let node; when: hasNoContent" matTreeNodePadding>
<button mat-icon-button disabled></button>
<mat-form-field>
<input matInput #itemValue placeholder="New item...">
</mat-form-field>
<mat-form-field appearance="legacy">
<input matInput type="text" [formControl]="locationField" [(ngModel)]="node.field" name="node" [matAutocomplete]="auto" placeholder="Field"/>
<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let filteredFieldResult of locationFieldResults" [value]="filteredFieldResult">
{{filteredFieldResult}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<button mat-button (click)="saveNode(node, itemValue.value)">Save</button>
</mat-tree-node>
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
<button mat-icon-button matTreeNodeToggle
[attr.aria-label]="'toggle ' + node.filename">
<mat-icon class="mat-icon-rtl-mirror">
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
</mat-icon>
</button>
<mat-checkbox [checked]="descendantsAllSelected(node)"
[indeterminate]="descendantsPartiallySelected(node)"
(change)="todoItemSelectionToggle(node)">{{node.item}}</mat-checkbox>
<button mat-icon-button (click)="addNewItem(node)"><mat-icon>add</mat-icon></button>
</mat-tree-node>
</mat-tree>
you can see that I have a input field with autocomplet.
and the ts code is as follow:
import {SelectionModel} from '#angular/cdk/collections';
import {FlatTreeControl} from '#angular/cdk/tree';
import {Component, Injectable} from '#angular/core';
import {MatTreeFlatDataSource, MatTreeFlattener} from '#angular/material/tree';
import {BehaviorSubject} from 'rxjs';
import { FormControl } from '#angular/forms';
/**
* Node for to-do item
*/
export class TodoItemNode {
children: TodoItemNode[];
item: string;
}
/** Flat to-do item node with expandable and level information */
export class TodoItemFlatNode {
item: string;
level: number;
expandable: boolean;
}
/**
* The Json object for to-do list data.
*/
const TREE_DATA = {
Groceries: {
'Almond Meal flour': null,
'Organic eggs': null,
'Protein Powder': null,
Fruits: {
Apple: null,
Berries: ['Blueberry', 'Raspberry'],
Orange: null
}
},
Reminders: [
'Cook dinner',
'Read the Material Design spec',
'Upgrade Application to Angular'
]
};
/**
* Checklist database, it can build a tree structured Json object.
* Each node in Json object represents a to-do item or a category.
* If a node is a category, it has children items and new items can be added under the category.
*/
#Injectable()
export class ChecklistDatabase {
dataChange = new BehaviorSubject<TodoItemNode[]>([]);
get data(): TodoItemNode[] { return this.dataChange.value; }
constructor() {
this.initialize();
}
initialize() {
// Build the tree nodes from Json object. The result is a list of `TodoItemNode` with nested
// file node as children.
const data = this.buildFileTree(TREE_DATA, 0);
// Notify the change.
this.dataChange.next(data);
}
/**
* Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
* The return value is the list of `TodoItemNode`.
*/
buildFileTree(obj: {[key: string]: any}, level: number): TodoItemNode[] {
return Object.keys(obj).reduce<TodoItemNode[]>((accumulator, key) => {
const value = obj[key];
const node = new TodoItemNode();
node.item = key;
if (value != null) {
if (typeof value === 'object') {
node.children = this.buildFileTree(value, level + 1);
} else {
node.item = value;
}
}
return accumulator.concat(node);
}, []);
}
/** Add an item to to-do list */
insertItem(parent: TodoItemNode, name: string) {
if (parent.children) {
parent.children.push({item: name} as TodoItemNode);
this.dataChange.next(this.data);
}
}
updateItem(node: TodoItemNode, name: string) {
node.item = name;
this.dataChange.next(this.data);
}
}
/**
* #title Tree with checkboxes
*/
#Component({
selector: 'tree-checklist-example',
templateUrl: 'tree-checklist-example.html',
styleUrls: ['tree-checklist-example.css'],
providers: [ChecklistDatabase]
})
export class TreeChecklistExample {
/** Map from flat node to nested node. This helps us finding the nested node to be modified */
flatNodeMap = new Map<TodoItemFlatNode, TodoItemNode>();
/** Map from nested node to flattened node. This helps us to keep the same object for selection */
nestedNodeMap = new Map<TodoItemNode, TodoItemFlatNode>();
public locationField: FormControl = new FormControl();
public locationFieldResults = ["abc", "asdfa", "asdfasd"]
/** A selected parent node to be inserted */
selectedParent: TodoItemFlatNode | null = null;
/** The new item's name */
newItemName = '';
treeControl: FlatTreeControl<TodoItemFlatNode>;
treeFlattener: MatTreeFlattener<TodoItemNode, TodoItemFlatNode>;
dataSource: MatTreeFlatDataSource<TodoItemNode, TodoItemFlatNode>;
/** The selection for checklist */
checklistSelection = new SelectionModel<TodoItemFlatNode>(true /* multiple */);
constructor(private _database: ChecklistDatabase) {
this.treeFlattener = new MatTreeFlattener(this.transformer, this.getLevel,
this.isExpandable, this.getChildren);
this.treeControl = new FlatTreeControl<TodoItemFlatNode>(this.getLevel, this.isExpandable);
this.dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
_database.dataChange.subscribe(data => {
this.dataSource.data = data;
});
this.locationField.valueChanges.subscribe(inputField => {
this.filterField(inputField);
}
);
}
getLevel = (node: TodoItemFlatNode) => node.level;
isExpandable = (node: TodoItemFlatNode) => node.expandable;
getChildren = (node: TodoItemNode): TodoItemNode[] => node.children;
hasChild = (_: number, _nodeData: TodoItemFlatNode) => _nodeData.expandable;
hasNoContent = (_: number, _nodeData: TodoItemFlatNode) => _nodeData.item === '';
/**
* Transformer to convert nested node to flat node. Record the nodes in maps for later use.
*/
transformer = (node: TodoItemNode, level: number) => {
const existingNode = this.nestedNodeMap.get(node);
const flatNode = existingNode && existingNode.item === node.item
? existingNode
: new TodoItemFlatNode();
flatNode.item = node.item;
flatNode.level = level;
flatNode.expandable = !!node.children;
this.flatNodeMap.set(flatNode, node);
this.nestedNodeMap.set(node, flatNode);
return flatNode;
}
/** Whether all the descendants of the node are selected. */
descendantsAllSelected(node: TodoItemFlatNode): boolean {
const descendants = this.treeControl.getDescendants(node);
const descAllSelected = descendants.every(child =>
this.checklistSelection.isSelected(child)
);
return descAllSelected;
}
/** Whether part of the descendants are selected */
descendantsPartiallySelected(node: TodoItemFlatNode): boolean {
const descendants = this.treeControl.getDescendants(node);
const result = descendants.some(child => this.checklistSelection.isSelected(child));
return result && !this.descendantsAllSelected(node);
}
/** Toggle the to-do item selection. Select/deselect all the descendants node */
todoItemSelectionToggle(node: TodoItemFlatNode): void {
this.checklistSelection.toggle(node);
const descendants = this.treeControl.getDescendants(node);
this.checklistSelection.isSelected(node)
? this.checklistSelection.select(...descendants)
: this.checklistSelection.deselect(...descendants);
// Force update for the parent
descendants.every(child =>
this.checklistSelection.isSelected(child)
);
this.checkAllParentsSelection(node);
}
/** Toggle a leaf to-do item selection. Check all the parents to see if they changed */
todoLeafItemSelectionToggle(node: TodoItemFlatNode): void {
this.checklistSelection.toggle(node);
this.checkAllParentsSelection(node);
}
/* Checks all the parents when a leaf node is selected/unselected */
checkAllParentsSelection(node: TodoItemFlatNode): void {
let parent: TodoItemFlatNode | null = this.getParentNode(node);
while (parent) {
this.checkRootNodeSelection(parent);
parent = this.getParentNode(parent);
}
}
/** Check root node checked state and change it accordingly */
checkRootNodeSelection(node: TodoItemFlatNode): void {
const nodeSelected = this.checklistSelection.isSelected(node);
const descendants = this.treeControl.getDescendants(node);
const descAllSelected = descendants.every(child =>
this.checklistSelection.isSelected(child)
);
if (nodeSelected && !descAllSelected) {
this.checklistSelection.deselect(node);
} else if (!nodeSelected && descAllSelected) {
this.checklistSelection.select(node);
}
}
/* Get the parent node of a node */
getParentNode(node: TodoItemFlatNode): TodoItemFlatNode | null {
const currentLevel = this.getLevel(node);
if (currentLevel < 1) {
return null;
}
const startIndex = this.treeControl.dataNodes.indexOf(node) - 1;
for (let i = startIndex; i >= 0; i--) {
const currentNode = this.treeControl.dataNodes[i];
if (this.getLevel(currentNode) < currentLevel) {
return currentNode;
}
}
return null;
}
/** Select the category so we can insert the new item. */
addNewItem(node: TodoItemFlatNode) {
const parentNode = this.flatNodeMap.get(node);
this._database.insertItem(parentNode!, '');
this.treeControl.expand(node);
}
/** Save the node to database */
saveNode(node: TodoItemFlatNode, itemValue: string) {
const nestedNode = this.flatNodeMap.get(node);
this._database.updateItem(nestedNode!, itemValue);
}
private filterField(value: string): string[] {
if (value) {
this.locationFieldResults = this.locationFieldResults.filter((searchFieldResult) => {
return searchFieldResult.indexOf(value) !== -1;
});
} else {
this.locationFieldResults = this.locationFieldResults;
}
return this.locationFieldResults;
}
}
the problem is: If I tipped some content in input field, by adding new child node, the tipped content in old input field disappeared.
any solutions are expected.
this is example from angular material, you can copy the html and ts file into it,
https://stackblitz.com/angular/nnxeaxmrdob?file=src%2Fapp%2Ftree-checklist-example.ts
I've been working on something that uses a shared dart package through for firestore and come across an interesting issue.
I have a business logic object that is basically as follows:
class HomeBloc {
final Firestore _firestore;
CollectionReference _ref;
HomeBloc(this._firestore) {
_ref = _firestore.collection('test');
}
Stream<List<TestModel>> get results {
return _ref.onSnapshot.asyncMap((snapshot) {
return snapshot.docs.map((ds) => TestModel(ds.get('data') as String)).toList();
}
}
}
Given the following code component:
#Component(
selector: 'my-app',
templateUrl: 'app_component.html',
directives: [coreDirectives],
pipes: [commonPipes]
)
class AppComponent extends OnInit {
HomeBloc bloc;
Stream<List<TestModel>> results;
AppComponent() {
}
#override
void ngOnInit() {
print("Initializing component");
fb.initializeApp(
//...
);
getData();
}
Future<void> getData() async {
final store = fb.firestore();
bloc = HomeBloc(store);
}
}
I would expect the following to work, but it does not:
<div *ngIf="bloc != null">
<h2>Loaded properly</h2>
<ul>
<li *ngFor="let item of bloc.results | async">
{{item.data}}
</li>
</ul>
</div>
However, if I instead change getData and the html to the following:
Future<void> getData() async {
final store = fb.firestore();
bloc = HomeBloc(store);
results = bloc.results;
}
// HTML
<ul *ngFor="let item of results | async">
Everything works as expected. What's going on here?
The answer is that the get method is creating a new list every time its accessed, which isn't giving Angular an oppotunity to render the items before resetting. The correct implementation of HomeBloc:
class HomeBloc {
final Firestore _firestore;
CollectionReference _ref;
HomeBloc(this._firestore) {
_ref = _firestore.collection('test');
_results = _ref.onSnapshot.asyncMap((snapshot) {
return snapshot.docs.map((ds) => TestModel(ds.get('data') as String)).toList();
}
Stream<List<TestModel>> _results;
Stream<List<TestModel>> get results => _results;
}
I'm trying to create an angular dart component dynamically. I know it's not a best practice but I have to because of how my angular widgets are being inserted.
I based my work off of:
How to add a component programatically in Angular.Dart?
The code samples on longer work because of changes in the Angular Dart library.
I got this code to work but it's inconsistent. The solution was the Timer.run() to fire the scope.apply. The problem with that is:
It stinks to make a call like that and would perform terribly with lots of components
It seems to work randomly. Most of the time it does but occasionally it doesn't do the {{foo}} replacements
void main() {
IBMModule module = new IBMModule();
AngularModule angularModule = new AngularModule();
Injector injector = applicationFactory()
.addModule(module)
.run();
AppComponent appComponent = injector.get(AppComponent);
appComponent.addElement("<brazos-input-string label='test'/>");
}
class MyValidator implements NodeValidator {
bool allowsElement(Element element) {
return true;
}
bool allowsAttribute(Element element, String attributeName, String value) {
return true;
}
}
#Injectable()
class AppComponent {
NodeValidator validator;
Compiler _compiler;
DirectiveInjector _directiveInjector;
DirectiveMap _directiveMap;
NodeTreeSanitizer _nodeTreeSanitizer;
Injector _appInjector;
Scope _scope;
AppComponent(this._directiveInjector, this._compiler, this._directiveMap, this._nodeTreeSanitizer, this._appInjector, this._scope) {
validator = new MyValidator();
}
void addElement(String elementHTML) {
DivElement container = querySelector("#container");
DivElement inner = new DivElement();
container.append(inner);
Element element = new Element.html(elementHTML, validator: validator);
// inner.setInnerHtml(elementHTML, validator: validator);
ViewFactory viewFactory = _compiler.call([element], _directiveMap);
if (_scope != null) {
Scope childScope = _scope.createProtoChild();
View newView = viewFactory.call(childScope, _directiveInjector);
newView.nodes.forEach((node) => inner.append(node));
Timer.run(() => childScope.apply());
} else {
print("scope is null");
}
}
}
class IBMModule extends Module {
IBMModule() {
bind(BrazosInputStringComponent);
bind(BrazosTextAreaComponent);
bind(BrazosButtonComponent);
bind(ProcessDataProvider, toImplementation: ActivitiDataProvider);
bind(AppComponent);
}
}
I'm trying to make a directive similar to ng-model to work with file inputs. I am new to AngularDart so don't really know how to go about doing this.
Here is some code I managed to write by looking at the ng-model source files but it (obviously) doesn't work.
#Decorator(
selector: 'input[type=file][file-model]',
map: const {'file-model': '&filesSelected'})
class FileModel {
final InputElement inputElement;
String expression;
final Scope scope;
String _inputType;
List<File> files;
var filesSelected;
FileModel(this.inputElement, this.scope, NodeAttrs attrs) {
expression = attrs["file-model"];
inputElement.onChange.listen((event) {
files = inputElement.files;
filesSelected(files);
});
}
}
Any suggestions?
I've got it working.
#Decorator(
selector: 'input[type=file][file-model]',
map: const {'file-model': '&filesSelected'})
class FileModel {
Element inputElement;
String expression;
final Scope scope;
String _inputType;
List<File> files;
var listeners = {};
FileModel(this.inputElement, this.scope) {
}
initListener(var stream, var handler) {
int key = stream.hashCode;
if (!listeners.containsKey(key)) {
listeners[key] = handler;
stream.listen((event) => handler({r"files": (inputElement as InputElement).files}));
}
}
set filesSelected(value) => initListener(inputElement.onChange, value);
}
Usage: <input type="file" multiple file-model="cmp.filesSelected(files)">
For getting the files upon the change event, you can also just pass them via $event.target.files. Example:
import 'package:angular2/core.dart';
#Component(
selector: 'my-app',
template: r'''
<h1>{{title}}</h1>
<input class="btn" type="file" (change)="onFileUpload($event.target.files)">
''')
class AppComponent {
String title = 'File transfer';
onFileUpload(List<File> files) {
if (files == null) return;
// read file content as dataURL
final File file = files[0];
final FileReader reader = new FileReader();
reader.onLoad.listen((_) {
print(reader.result);
});
reader.readAsDataUrl(file);
}
}
I'm setting up custom Polymer elements that need to display data from the model (Property objects that need to be passed around).
I can successfully display the values of the properties, but when I update a value inside a Property object, the value is not updated inside the element.
What am I missing? I based myself on the following examples:
https://github.com/sethladd/dart-polymer-dart-examples/tree/master/web/bind_component_field_to_model_field
https://github.com/sethladd/dart-polymer-dart-examples/tree/master/web/getter_setter_as_attribute
wb-control-text.html
<polymer-element name="wb-control-text">
<template>
<div>
<label for="{{id}}">{{label}}</label>
<input type="text" id="{{id}}" value="{{value}}" />
<button on-click="{{updateValue}}">Update me</button>
</div>
</template>
<script type="application/dart" src="wb-control-text.dart"></script>
</polymer-element>
wb-control-text.dart
import "package:polymer/polymer.dart";
import "package:rqn_workbench/wb-property.dart";
#CustomTag("wb-control-text")
class WorkbenchControlTextElement extends PolymerElement {
final TextProperty _idProperty = new TextProperty("id", value: "1");
final TextProperty _valueProperty = new TextProperty("value", value:"Bla");
final TextProperty _labelProperty = new TextProperty("label", value:"Label!");
WorkbenchControlTextElement.created() : super.created();
#published String get id => _idProperty.value;
void set id(String id) { _idProperty.value = id; }
#published String get value => _valueProperty.value;
void set value(String value) { _valueProperty.value = value; }
#published String get label => _labelProperty.value;
void set label(String label) { _labelProperty.value = label; }
void updateValue() {
value += "Bla";
print(value); // Will print the appended value
}
}
wb-property.dart
library workbench_property;
import "package:polymer/polymer.dart";
#observable
class Property {
String key;
}
#observable
class TextProperty extends Property {
String value;
TextProperty ( String key, {String value} ) {
this.key = key;
this.value = value;
}
}
The solution was to use #reflectable on the getters and setters, so it could be picked up by the PathObserver.