Angular Dynamic form observable property binding - forms

I have a problem with some dynamically generated forms and passing values to them. I feel like someone must have solved this, or I’m missing something obvious, but I can't find any mention of it.
So for example, I have three components, a parent, a child, and then a child of that child. For names, I’ll go with, formComponent, questionComponent, textBoxComponent. Both of the children are using changeDetection.OnPush.
So form component passes some values down to questionComponent through the inputs, and some are using the async pipe to subscribe to their respective values in the store.
QuestionComponent dynamically creates different components, then places them on the page if they match (so many types of components, but each questionComponent only handles on one component.
some code:
#Input() normalValue
#Input() asyncPipedValue
#ViewChild('questionRef', {read: ViewContainerRef}) public questionRef: any;
private textBoxComponent: ComponentFactory<TextBoxComponent>;
ngOnInit() {
let component =
this.questionRef.createComponent(this.checkboxComponent);
component.instance.normalValue = this.normalValue;
component.instance. asyncPipedValue = this. asyncPipedValue;
}
This works fine for all instances of normalValues, but not for asyncValues. I can confirm in questionComponent’s ngOnChanges that the value is being updated, but that value is not passed to textBoxComponent.
What I basically need is the async pipe, but not for templates. I’ve tried multiple solutions to different ways to pass asyncValues, I’ve tried detecting when asyncPipeValue changes, and triggering changeDetectionRef.markForChanges() on the textBoxComponent, but that only works when I change the changeDetectionStrategy to normal, which kinda defeats the performance gains I get from using ngrx.
This seems like too big of an oversight to not already have a solution, so I’m assuming it’s just me not thinking of something. Any thoughts?

I do something similar, whereby I have forms populated from data coming from my Ngrx Store. My forms aren't dynamic so I'm not 100% sure if this will also work for you.
Define your input with just a setter, then call patchValue(), or setValue() on your form/ form control. Your root component stays the same, passing the data into your next component with the async pipe.
#Input() set asyncPipedValue(data) {
if (data) {
this.textBoxComponent.patchValue(data);
}
}
patchValue() is on the AbstractControl class. If you don't have access to that from your question component, your TextBoxComponent could expose a similar method, that can be called from your QuestionComponent, with the implementation performing the update of the control.
One thing to watch out for though, if you're also subscribing to valueChanges on your form/control, you may want to set the second parameter so the valueChanges event doesn't fire immediately.
this.textBoxComponent.patchValue(data, { emitEvent: false });
or
this.textBoxComponent.setValue(...same as above);
Then in your TextBoxComponent
this.myTextBox.valueChanges
.debounceTime(a couple of seconds maybe)
.distinctUntilChanged()
.map(changes => {
this.store.dispatch(changes);
})
.subscribe();
This approach is working pretty well, and removes the need to have save/update buttons everywhere.

I believe I have figured out a solution (with some help from the gitter.com/angular channel).
Since the values are coming in to the questionComponent can change, and trigger it's ngOnChanges to fire, whenever there is an event in ngOnChanges, it needs to parse through the event, and bind and changes to the dynamic child component.
ngOnChanges(event) {
if (this.component) {
_.forEach(event, (value, key) => {
if (value && value.currentValue) {
this.component.instance[key] = value.currentValue;
}
});
}
}
This is all in questionComponent, it resets the components instance variables if they have changed. The biggest problem with this so far, is that the child's ngOnChanges doesn't fire, so this isn't a full solution. I'll continue to dig into it.

Here are my thoughts on the question, taking into account limited code snippet.
First, provided example doesn't seem to have anything to do with ngrx. In this case, it is expected that ngOnInit runs only once and at that time this.asyncPipedValue value is undefined. Consequently, if changeDetection of this.checkboxComponent is ChangeDetection.OnPush the value won't get updated. I recommend reading one excellent article about change detection and passing async inputs. That article also contains other not less great resources on change detection. In addition, it seems that the same inputs are passed twice through the component tree which is not a good solution from my point of view.
Second, another approach would be to use ngrx and then you don't need to pass any async inputs at all. Especially, this way is good if two components do not have the parent-child relationship in the component tree. In this case, one component dispatches action to put data to Store and another component subscribes to that data from Store.
export class DataDispatcherCmp {
constructor(private store: Store<ApplicationState>) {
}
onNewData(data: SomeData) {
this.store.dispatch(new SetNewDataAction(data));
}
}
export class DataConsumerCmp implements OnInit {
newData$: Observable<SomeData>;
constructor(private store: Store<ApplicationState>) {
}
ngOnInit() {
this.newData$ = this.store.select('someData');
}
}
Hope this helps or gives some clues at least.

Related

Flutter best practice for form submissions

I am having trouble finding good resources for what best practices would be for Flutter development, specifically for form handling.
Everything I find on form submissions is fairly clear, but the problem is they all have the validation logic and submission logic directly in the form widget. I don't like this as it seems it would get very convoluted very quickly with more than say 3 inputs and any sort of more than basic validation logic. It also seems to violate the separation of concerns thinking that I though was supposed to be a big thing in Flutter/Dar (at least from what I have read).
So my chosen solution for this was my FormHandler class, which I defined in the form_handler.dart file. It has some static methods for validation of input, some methods for submission handling, and a formInput of type Map<String, dynamic> for storing key value pairs of user input.
It works like this:
An instance of the FormHandler is created
The user inputs the data
On form.save(), for each user input, the input data is stored in the formInput map, with key being the title of the input, and the value being the user's input.
The submission button would run the validation and save functions and then take the data from formInput and send it to something like a database handler that would store it on the db
form_handler.dart:
class FormHandler {
// make new form handler with empty map
FormHandler({required this.formInput});
// for storing input key value pairs
Map<String, dynamic> formInput;
// Form submissions
// new course
void submitCourse({required formKey}){
final form = formKey.currentState;
// save on validate
if( form.validate() ){
form.save();
// then make new course via the database controller
}
}
// Input validations
static String? validateTextInput(String? input){
if( input == null || input.isEmpty ){
return 'Field must not be empty';
} else {
return null;
}
}
}
I'm just wondering if this is a good solution, what are some potential pitfalls, any suggestions etc.
It seems like a good solution to me, but I would like feedback from someone with more experience than me.
Thanks, Seth.
A common practice would be to create value objects or entities that hold logic to do validation on themselves. Perhaps with an interface (abstract class) as a base for those to make sure that you don't forget to implement such validation.
Thereby moving such validation logic from the UI and instead caring about what is important in your domain. It will be possible to validate in several places in your code, not just from the form, which is not something easily achievable with your proposed solution. Your solution still mix UI with logic via the formKey. You might want to validate "stuff" when you receive data from your backend, or do other calculations/changes to values later in the app.
You can refer to this youtube channel for guidance.
Here is a link for a video related to TextFields and Validation - https://youtu.be/2rn3XbBijy4

Is there a fix to highlight subsequent DOM changes using a Chrome Extension that currently only reads source code (+highlights keywords) on page load? [duplicate]

Essentially I want to have a script execute when the contents of a DIV change. Since the scripts are separate (content script in the Chrome extension & webpage script), I need a way simply observe changes in DOM state. I could set up polling but that seems sloppy.
For a long time, DOM3 mutation events were the best available solution, but they have been deprecated for performance reasons. DOM4 Mutation Observers are the replacement for deprecated DOM3 mutation events. They are currently implemented in modern browsers as MutationObserver (or as the vendor-prefixed WebKitMutationObserver in old versions of Chrome):
MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
var observer = new MutationObserver(function(mutations, observer) {
// fired when a mutation occurs
console.log(mutations, observer);
// ...
});
// define what element should be observed by the observer
// and what types of mutations trigger the callback
observer.observe(document, {
subtree: true,
attributes: true
//...
});
This example listens for DOM changes on document and its entire subtree, and it will fire on changes to element attributes as well as structural changes. The draft spec has a full list of valid mutation listener properties:
childList
Set to true if mutations to target's children are to be observed.
attributes
Set to true if mutations to target's attributes are to be observed.
characterData
Set to true if mutations to target's data are to be observed.
subtree
Set to true if mutations to not just target, but also target's descendants are to be observed.
attributeOldValue
Set to true if attributes is set to true and target's attribute value before the mutation needs to be recorded.
characterDataOldValue
Set to true if characterData is set to true and target's data before the mutation needs to be recorded.
attributeFilter
Set to a list of attribute local names (without namespace) if not all attribute mutations need to be observed.
(This list is current as of April 2014; you may check the specification for any changes.)
Edit
This answer is now deprecated. See the answer by apsillers.
Since this is for a Chrome extension, you might as well use the standard DOM event - DOMSubtreeModified. See the support for this event across browsers. It has been supported in Chrome since 1.0.
$("#someDiv").bind("DOMSubtreeModified", function() {
alert("tree changed");
});
See a working example here.
Many sites use AJAX/XHR/fetch to add, show, modify content dynamically and window.history API instead of in-site navigation so current URL is changed programmatically. Such sites are called SPA, short for Single Page Application.
Usual JS methods of detecting page changes
MutationObserver (docs) to literally detect DOM changes.
Info/examples:
How to change the HTML content as it's loading on the page
Performance of MutationObserver to detect nodes in entire DOM.
Lightweight observer to react to a change only if URL also changed:
let lastUrl = location.href;
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
onUrlChange();
}
}).observe(document, {subtree: true, childList: true});
function onUrlChange() {
console.log('URL changed!', location.href);
}
Event listener for sites that signal content change by sending a DOM event:
pjax:end on document used by many pjax-based sites e.g. GitHub,
see How to run jQuery before and after a pjax load?
message on window used by e.g. Google search in Chrome browser,
see Chrome extension detect Google search refresh
yt-navigate-finish used by Youtube,
see How to detect page navigation on YouTube and modify its appearance seamlessly?
Periodic checking of DOM via setInterval:
Obviously this will work only in cases when you wait for a specific element identified by its id/selector to appear, and it won't let you universally detect new dynamically added content unless you invent some kind of fingerprinting the existing contents.
Cloaking History API:
let _pushState = History.prototype.pushState;
History.prototype.pushState = function (state, title, url) {
_pushState.call(this, state, title, url);
console.log('URL changed', url)
};
Listening to hashchange, popstate events:
window.addEventListener('hashchange', e => {
console.log('URL hash changed', e);
doSomething();
});
window.addEventListener('popstate', e => {
console.log('State changed', e);
doSomething();
});
P.S. All these methods can be used in a WebExtension's content script. It's because the case we're looking at is where the URL was changed via history.pushState or replaceState so the page itself remained the same with the same content script environment.
Another approach depending on how you are changing the div.
If you are using JQuery to change a div's contents with its html() method, you can extend that method and call a registration function each time you put html into a div.
(function( $, oldHtmlMethod ){
// Override the core html method in the jQuery object.
$.fn.html = function(){
// Execute the original HTML method using the
// augmented arguments collection.
var results = oldHtmlMethod.apply( this, arguments );
com.invisibility.elements.findAndRegisterElements(this);
return results;
};
})( jQuery, jQuery.fn.html );
We just intercept the calls to html(), call a registration function with this, which in the context refers to the target element getting new content, then we pass on the call to the original jquery.html() function. Remember to return the results of the original html() method, because JQuery expects it for method chaining.
For more info on method overriding and extension, check out http://www.bennadel.com/blog/2009-Using-Self-Executing-Function-Arguments-To-Override-Core-jQuery-Methods.htm, which is where I cribbed the closure function. Also check out the plugins tutorial at JQuery's site.
In addition to the "raw" tools provided by MutationObserver API, there exist "convenience" libraries to work with DOM mutations.
Consider: MutationObserver represents each DOM change in terms of subtrees. So if you're, for instance, waiting for a certain element to be inserted, it may be deep inside the children of mutations.mutation[i].addedNodes[j].
Another problem is when your own code, in reaction to mutations, changes DOM - you often want to filter it out.
A good convenience library that solves such problems is mutation-summary (disclaimer: I'm not the author, just a satisfied user), which enables you to specify queries of what you're interested in, and get exactly that.
Basic usage example from the docs:
var observer = new MutationSummary({
callback: updateWidgets,
queries: [{
element: '[data-widget]'
}]
});
function updateWidgets(summaries) {
var widgetSummary = summaries[0];
widgetSummary.added.forEach(buildNewWidget);
widgetSummary.removed.forEach(cleanupExistingWidget);
}

v2.ODataModel: which API is more preferred? "bindElement" or "read"?

I set the view.setModel(model), get the model for the view, and request for model.read("/entitySet('10000')").
The model is then filled up with /entitySet('10000')/properties.
But it is difficult to assign them to view fields, as now in the view, <Text text="{property}"> doesn't work. It has to be <Text text="{/entitySet('10000')/property}">.
On the other hand, if I set view's context binding to "/entitySet('10000')", then <Text text="{property}"> would start working.
Which one is the preferred method? When to use .read?
I almost never use .read if I want to use the results from an OData call directly in a binding context. The only time I use .read is if I want to manipulate the results before doing anything with them.
Look at this example from the sdk for instance: https://ui5.sap.com/#/entity/sap.ui.table.Table/sample/sap.ui.table.sample.OData
Syntax on this kind of binding is similar to read but with a few differences in events, and a few different types of methods depending on what you want to bind. Binding to a view for instance uses bindElement:
this.getView().bindElement("/entitySet('1000')");
After this, fields on that particular entity can be accessed as <Text text="{property}" />.
Here's an example from one of my current apps with events and some other call parameters:
this.getView().bindElement({
path: `/Orders('${currentOrderNumber}')`,
parameters: {
expand: 'Texts'
},
events: {
dataRequested: _ => this.getView().setBusy(true),
dataReceived: data => {
if (!this.getView().getBindingContext()) {
// navigate to `Not Found` view
}
},
change: _ => this.getView().setBusy(false)
}
});
For a table, it's slightly different, since it depends on the aggregation you wish to bind, such as
oTable.bindRows({
path: "properties"
});
Which is the same as:
<Table rows="{properties}" />
It's always important to be more expressive. Use the API that is specifically designed to do that one task.
Comparing the two variants:
myModel.read(sPath) with text="{/path/property}"
myControl.bindElement(sPath) with text="{property}"
I'd be perplexed about the 1st call whereas in the 2nd call, I'd know exactly what you want to achieve (You want to bind element. Alternatively, bindObject can be also used).
The same applies to the framework. Since you're telling exactly what you want to achieve, the framework can improve its behavior based on your intent. E.g.: in (route)PatternMatched handler when the user navigates to the same page, .bindElement with the same path won't trigger another request since the model already stored the entity from the previous call. It can show the result immediately.
With .read, however, the framework doesn't know what you want to achieve, so it sends the request right away regardless of the application state.
Additionally, the 1st variant is anything but future-proof. It relies on the cached results. It's almost a side-effect that it works at all. The problem is that there is no guarantee that this behavior will continue to work in later versions. Also there won't be read method in V4 ODataModel.
TL;DR
v2.ODataModel#read
Does not create context from the response. Repeating .read("<same path>") always sends a new request.
Less expressive. Encourages app developers to work with a client-side model (e.g. JSONModel).
Application loses context awareness, increasing TCO, less future-proof.
bindElement or bindObject
Creates context from the response and stores it internally so that the same request can return the data immediately.
Clearly expresses the intent; application as well as framework can work with the existing APIs.
More future-proof: v4.ODataModel does not support manual read. Imagine you've built your applications with the v2ODataModel.read-jsonModel.setData approach, and you need to migrate to v4. Have fun. :)
I honestly think that v2.ODataModel#read should have never become a public method. I wouldn't encourage anyone to use .read except of when reading the $count value manually.
If the entity values need to be formatted, there are formatters and binding types out of the box which are also easy to extend.
If the application needs to restructure the response body, usually it's a sign of a poor design of the data model or the service not conforming to the OData spec.
I almost agree with Jorg, but not entirely:
It really depends on what are you trying to achieve. If looking to display data from backend then the easiest way to go is with this.getView().bindElement()
but if you are in need to manipulate data before displaying (like formatting text, displaying images from base64 strings, etc) OR if you would like to create new entity using some of existing data, OR update the existing data - the this.getModel(sName).read() is the way to go - as you can set read entity with all its deep entities to JSONModel in successCallback and manipulate it from the localModel.
If using localModel the dataBinding is pretty much the same in the view - except you have to additionally give model name to take data from. So for example, if in successCallback of your Model.read() you set your data to Model named "localModel":
this.getModel().read(sObjectPath, {
urlParameters: {
$expand: ""
},
success: function (oData) {
// "localModel" is name you gave in onInit function to your new JSONMOdel, when setting it to View e.g. this.getView().setModel(oJSONMOdel, "localModel")
this.getModel("localModel").setData(oData);
}
})
then in XML view you would indicate that
<Text text="{localModel>/mainPropertyName}"/>
// for displaying deep entities as List items or in Table
<List items="{localModel>/deepEntityName}">
<StandardListItem title="{localModel>propertyNamefromDeepEntity}" />
</List>
From my experience working with more complex apps that Read-Only - I always use Model.read().

SAPUI5 bindAggregation complete for a Table

I am binding an aggregation to a table . I couldn't find an event which is triggered after the binding is complete . There is "updateFinished" event for sap.m.List , which is exactly what I am looking for in a Table (and a dropodown). I thought of using attachRequestCompleted() on the model , but the model is used at other places where I do not want this event to trigger.
Is there anyway to trigger a event once the databinding is complete on a Table (and a dropdown)?
Any help is appreciated.
Thanks in advance.
update: There is "updateFinished" event for table extended from ListBase. I am still not sure how I missed it before I posted this question. But, the question is still valid for a dropdown and TableSelectDialog controls.
I also stumbled upon that problem, but in a different Context.
I have a Grid layout in which I dynamically load Panels via an oData Model.
Therefore I have entered the path in my XML Grid-View element.
<l:Grid id="grid" content="{some path...}">...</l:Grid>
Now I wanted to set the grid view busy and when the data is loaded revert this.
Therefore I use the Binding of the grid view.
In the Controllers onInit method I have added:
this._oGrid = this.getView().byId("grid");
this.getRouter().attachRouteMatched(this._onRouteMatch.bind(this));
Please note that the bind method is not available in every browser. You need to apply a polyfill. (See https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Function/bind)
Also Bind has nothing to do with the binding :D
I needed to do this because the Binding is not available in the onInit function.
The _onRouteMatched function:
var oContent = this._oGrid.getBinding("content");
oContent.attachDataReceived(function(oData) {
this._oGrid.setBusy(false);
}.bind(this));
Now when the data is received the busy option is set to false.
If you want to show a 'loading' indicator for your table while the data is still loading (and thus not bound), I think the best approach is the following:
Use a dedicated JSONModel which holds only UI-specific stuff, like toggling enabled/readonly/busy/etc properties of controls.
In your case, something like:
var oUIModelData = {
tableIsBusy : false,
//etc, things like :
btnSubmitEnabled : false,
someControlVisible : true
};
var oUIModel = new sap.ui.model.json.JSONModel();
oUIModel.setData(oUIModelData);
sap.ui.getCore().setModel(oUIModel, "UIModel");
In your table definition, bind the busy property to {UIModel>/tableIsBusy} and set the table's property busyIndicatorDelay to 0 to avoid any delays
Just before you do your OData service call, set the property tableBusy to true. This will immediately show the busy overlay to your table:
sap.ui.getCore().getModel("UIModel").setProperty(tableIsBusy, true);
//here your OData read call
In your OData service's requestCompleted (and if needed, also in requestFailed) event handlers, set the busy property of the UIModel back to false:
sap.ui.getCore().getModel("UIModel").setProperty(tableIsBusy, false);
The big benefit of this approach is (IMHO) instead of relying on each control to check whether the data has been loaded, simply do it during the actual load.
And by binding these UI-related things to a (separate) model saves you from writing lots of extra code ;-)
In general you could solve the problem by using batch processing on the OData service. According to https://sapui5.netweaver.ondemand.com/docs/guide/6c47b2b39db9404582994070ec3d57a2.html:
Use OData model v2.
var oModel = new sap.ui.model.odata.v2.ODataModel(myServiceUrl);
Define a set of deferred batch groups.
oModel.setDeferredBatchGroups(["myGroupId", "myGroupId2"]);
Add the batch group information to the corresponding bindings, e.g:
{
path:"/myEntities",
parameters: {
batchGroupId: "myGroupId"
}
}
All read/query actions on bindings in a certain batch group will be held back until a .submitChanges(.) call on the batch group is made.
oModel.submitChanges({
batchGroupId: "myGroupId",
success: mySuccessHandler,
error: myErrorHandler
});
Use the success/error handlers to execute actions.
This rather generic approach gives you additional control such as trigger actions, grouping and event handling on the OData model.

Autoscale text input with JEditable.js?

I've been looking for a script that combines the autoGrowInput with the JEditable but found none.
Use https://github.com/MartinF/jQuery.Autosize.Input initialized automatically via jEditable's event data:
jQuery(element).editable(save_fn, {
data: function(value,settings} {
var target = event.target;
window.setTimeout(function(){
jQuery(target).find('input').autosizeInput();
});
return value;
}
});
It's worth noting that this event (data) fires before the input element is actually created, hence the use of the timeout. There doesn't seem to be an event available at the present time for after the input has been created.
Actually I have created a plugin that does exactly that. You can check the demo and the documentation. I tried to make it very intuitive. It has ajax capabilities, using the RESTful philosophy. If you liked the animation effect on the autoGrowInput, it will be really easy to add it to the plugin just by changing the css file, using the transition property.
If I get people to like it, I may be able to improve and add more features to it. Hope it helps.