I have a model:
export default Model.extend({
title: attr('string'),
attributes: attr('jsonb')
});
Where attributes is a custom json filed stored as jsonb in Postgres.
let say:
{
"name":"Bob",
"city":""
}
So I can easily manipulate attributes using template
<form.element .. #property="attributes.city"/> or model.set('attributes.city','city name')
Problem: hasDirtyAttributes do not changing because technically we have old object. But when I try to copy object let say
JSON.parse(JSON.stringify(this.get('attributes')) hasDirtyAttributes works as expected
So how to write some Mixin for a Model or other workaround which on the change of any attribute property will mark hasDirtyAttributes as true. I will update whole object so doesn't matter which property actually was changed.
Same problem: https://discuss.emberjs.com/t/hasdirtyattributes-do-not-work-with-nested-attributes-json-api/15592
existing solution doesn't work for me at all:
ember-dirtier
ember-data-relationship-tracker
ember-data-model-fragments (a lot of changes under the hood and broke my app)
Update:
Some not perfect idea that help better describe what I'm want to achieve:
Let say we adding observer to any object fileds:
export default Model.extend({
init: function(){
this._super();
this.set('_attributes', Object.assign({}, this.get('attributes'))); //copy original
Object.keys(this.get('attributes')).forEach((item) => {
this.addObserver('attributes.'+ item, this, this.objectObserver);
});
}
...
})
And observer:
objectObserver: function(model, filed){
let privateFiled = '_' + filed;
if (model.get(privateFiled) != model.get(filed)) { //compare with last state
model.set(privateFiled, this.get(filed));
model.set('attributes', Object.assign({}, this.get('attributes')) );
}
}
It's works, but when I change one filed in object due to copying object objectObserver faired again on every filed. So in this key changing every filed in object I mark observed filed as dirty
The further ember development will reduce using of event listener and two-way binding, actually Glimmer components supports only One-way Data Flow. So to be friendly with future versions of emberusing one-way data flow is good approach in this case to. So In my case as I use ember boostrap solution looks like
<form.element #controlType="textarea" #onChange={{action 'attributeChange'}}
where attributeChange action do all works.
New Glimmer / Octane style based on modifier and looks like:
{{!-- templates/components/child.hbs --}}
<button type="button" {{on "click" (fn #onClick 'Hello, moon!')}}>
Change value
</button>
Related
I'm trying to create an input that converts the user's pressed keys to the last single upper-cased character (if the user types 'a' -> 'A', 'abc' -> 'C').
This is my code:
<template>
<b-form-input
ref="firstNameInitial"
v-model="firstNameInitial"
/>
</template>
<script>
export default {
firstNameInitial: {
get () {
return this.$store.state.certificateAdd.firstNameInitial
},
async set (val) {
const newValue = StringUtils.convertToNameInitial(val)
this.$store.commit('certificateAdd/setFirstNameInitial', newValue)
// Tried this as well
this.$refs.firstNameInitial.$el.value = newValue
}
}
}
</script>
This works fine if I type distinct letters, but if I type "aa" the input will show "Aa" since the getter isn't recomputed (vuex still holds the same data: "A").
Even if I trick getter by refencing some dummy local data that gets incremented in the setter the component will not re-render because the value prop is not changed; but the underlining HTML input's value will always update even if Vue doesn't step in to change it.
I've even tried manipulating the DOM directly but that doesn't work either.
Any ideas on how this behaviour could be implemented.
You're not supposed to use a mutation since it is synchronous. You should use a vuex action because of the way DOM will handle the updates. If you have it as an action, I think it may work fine.
If it does not work, maybe try both: await this.$nextTick() and then action (in this order).
Although I feel this answer is relatively close to my problem and got some reputation, I don't get it right. I read a lot of posts on how to use the "new" style of Observer-pattern ((...).pipe(map(...)).subscribe(...) and *ngFor="... | async") in Angular and now also stumbled across How to Avoid Observables in Angular. I don't want to avoid reactive behaviour; I want to have changes in the REST-API to be reflected "live" to the user without reloading the Observer. That's why I want to subscribe an object (and therefore also its properties) to an Observable (and the 'might-be-there' values from the data-stream in it), right?
In my template I have:
<p>Werte:<br><span *ngFor="let attribute of _attributes | slice:0:5; index as h">
{{attribute.name}}: <strong>{{getParamNameAt(h)}}</strong> </span><br>
<span *ngFor="let attribute of _attributes | slice:5: _attributes.length; index as h">
{{attribute.name}}: <strong>{{getParamNameAt(h + 5)}}</strong> </span></p>
In my component I have:
private _attributes: Attribute[];
constructor(private attributesService: BkGenericaAttributesService) {
attributesService.getAttributes().subscribe({ next: attributes => this._attributes = attributes });
}
getParamNameAt(h: number): string {
let attrVal = this.bkHerstArtNr.charAt(h);
return attributes[h].subModule.find(param => param.subModuleValue === attrVal).subModuleName;
}
and as service I have:
const localUrl = '../../assets/json/response.json';
#Injectable()
export class BkGenericaAttributesMockService implements BkGenericaAttributesService {
constructor(private http: HttpClient) {
}
getAttributes(): Observable<Attribute[]> {
return this.http.get<Attribute[]>(localUrl).pipe(
tap((attributes : Attribute[]) => attributes.map((attribute : Attribute) => console.log("Piping into the http-request and mapping one object after another: " + attribute.name))),
map((attributes : Attribute[]) => attributes.map((attribute : Attribute) => new Attribute(attribute.id, attribute.name, attribute.title, attribute.description,
(attribute.parameters ? attribute.parameters.map((parameter : Parameter) => new Parameter(parameter.id,
parameter.name, parameter.value)) : [])))));
}
My problem running the application at this point is the 'to-create-on-stream' Attribute-objects and "nested" Parameters[]-array (created by pushing Parameter-objects into it) pushed into the _attributes-array from the httpClient's Observable: Piping into the http-request and mapping one object after another: undefined.
Apart from this - is my construct the right way to read values from a JSON-file (or alternatively an API-stream, which may change while a user visits the SPA) into properties of multiple objects displayed on the Angular view?
With the answer mentioned above - or the way I thought I have to translate it into my code - I start to doubt that I'm really understanding (and using) the reactive Angular way with Data-Providers <= Observables => Operators => Subscribers and finally Observers displayed to the user.
I really am confused (as you can read), because a lot of answers and descriptions that I found so far use either older patterns (before Angular 5.5?) and/or partially contradict each other.
Do I handle the API-changes in the right place? Has the array for the template's *ngFor-directives to be an Observer (handled with | async) or will the changes of respectively within the array be handled by the model behind the template and the template grabs its new values (with interpolation and property binding) and also directives with a change in the components properties without asyncing?
Briefly:
Is there a for-dummies default instruction to "stream-read" values from a http-request into multiple Typescript-objects and their properties concurrently displayed in a template with on-stream-changing directives rendered only in the relevant DOM nodes, the Angular 8 opinionated way? "Stream-reading" meaning: pulling and pushing (only) if there are changes in the API, without wasting resources.
If I understand right, you want the server to push changes to the client, as soon as they happen, so that the client can react and render accordingly, right ?
this.http.get is a single call, that will resolve with a snapshot of the data. Meaning that it won't automatically update just because some data changed in your backend.
If you want to notify the client about new data or even send that data directly to the client, you'll need websockets.
There is also some problems with code:
if you .subscribe(), you'll need to .unsubscribe(), otherwise you'll end up with a memory leak.
param => param.value === attrVal, where is attrVal coming from, I don't see it being set aynwhere in the method ?
You should use the async pipe, as it unsubscribes automatically for you.
You don't neet to create class instances via new in your service, instead your Attribute typing should be an interface.
I have a use case where a Model has a JSON attribute used for arbitrary configurations: configuration: DS.attr().
I have a configurationService (initialized in every component/route/controller/…) with computed properties for easily retrieving this configurations throughout the application
Since the JSON configurations are quite large & have variable depth I can’t have a computed property for every single one.
Unfortunately, being the Service a singleton computed properties don’t detect the changes in the JSON keys (a.k.a. deep watch).
I there a way to force deep watching a JSON attribute?
Example:
services/configuration.js:
// These below can also be computed.alias, same effect
configuration: Ember.computed(‘account.configuration', function() {
// account is set at application’s route:
// set(this, ‘configurationService.account', account);
return this.get(‘account.configuration’);
}),
profile: Ember.computed('configuration.profile', function() {
return this.get('configuration.profile');
}),
any/given/component.js:
configurationService: Ember.inject.service(‘configuration’),
…
// These won’t detect changes
randomConfig: Ember.computed.alias(’configurationService.profile.foo.bar.randomConfig')
Considering the configuration object was: {configuration: {profile: {foo: {bar: {randomConfig: false}}}}}, if I somehow change randomConfig to true, it won’t be detected
Note: I considered https://github.com/lytics/ember-data-model-fragments but discarded it since it’s too verbose, the issue is that our configuration object can become quite large and deep, dynamic & unpredictable
I tried computed.alias as well with no success.
Any hints or alternatives would be appreciated :)
UPDATE:
I tried with an object Transform (as suggested in Slack) and it doesn’t observe it deeply: https://gist.github.com/benoror/272f0ae893f80276ac1553ae048e6b20#file-object-js
You need to transform the plain JS-Object to an Ember-Object, and so for its child objects:
import Ember from 'ember';
import DS from 'ember-data';
function emberify(obj) {
if(obj instanceof Array) {
return Ember.A(obj.map(i => emberify(i)));
} else if (obj instanceof Object) {
return Ember.Object.create(Object.keys(obj)
.reduce((hash, key) => {
hash[key] = emberify(obj[key]);
}, {}));
}
}
export default DS.Transform.extend({
deserialize: function(value) {
return emberify(obj);
}
}
I have an Ember app consuming a rails based webservice.
On the Rails side, I have some enums, they are simply arrays.
Now, I would like to retreive those enums in the Ember app, and render them for select values.
The webservice returns a JSON response :
get '/grades.json'
{"grades":["cp","ce1","ce2","cm1","cm2"]}
On the Ember side, I created a GradesRoute like this :
App.GradesRoute = Ember.Route.extend({
model: function () {
return Em.$.getJSON('api/v1/grades.json')
}
}));
Then, I think I need it in the controllers where these enums are in use:
App.StudentsController = Ember.ArrayController.extend({
needs: ['grades'],
grades: Ember.computed.alias('controllers.grades')
}
));
So at least I thought I could iterate over the grades in the students template.
{{#each grade in grades}}
{{grade}}
{{/each}}
But I get no output at all... debugging from the template and trying templateContext.get('grades').get('model') returns an empty array []
Any idea on how I could load and access this data ?
So I ended up with ApplicationRoute, which is the immediate parent of StudentsRoute, so needs is relevant in this case.
App.ApplicationRoute = Ember.Route.extend({
setupController: function(controller) {
Em.$.getJSON('api/v1/enums.json').then(function(data){
controller.set('grades', data['grades']);
controller.set('states', data['states']);
}
}
});
Now I can create an alias for each enums I need to use accross my app.
App.StudentsController = Ember.ArrayController.extend({
needs: ['application'],
grades: Ember.computed.alias('controllers.application.grades'),
states: Ember.computed.alias('controllers.application.states')
});
I'm still not confident enough to be sure this is the way to go, any suggestion is welcome !
You just have some of your paths mixed up. In StudentsController, controllers.grades refers to the actual controller, not it's model. The following code should clear things up as it's a bit more explicit in naming.
App.StudentsController = Ember.ArrayController.extend({
needs: ['grades'],
gradesController: Ember.computed.alias('controllers.grades'),
grades: Ember.computed.alias('gradesController.model.grades')
});
Also, be aware that using needs only works if your grades route is a direct parent of your students route. If it's not a direct parent, you won't get back the data you want.
I have a form that have a three field group that on a click of a "Add New" buttom other three field group will be added. That part is working great.
I want to add a validation so all three fields are required in order to add a new group.
For reference here is the code working: http://jsfiddle.net/5g8Xc/
var ContactsModel = function(contacts) {
var self = this;
self.contacts = ko.observableArray(ko.utils.arrayMap(contacts, function(contact) {
return { firstName: contact.firstName, fathersLast: contact.fathersLast, country: contact.country };
}));
self.addContact = function() {
self.contacts.push({
firstName: "",
fathersLast: "",
country: ""
});
};
self.removeContact = function(contact) {
self.contacts.remove(contact);
};
};
Any clue on how to implement this validation? I was trying to use jquery validation to do that but I think that is possible with KnockoutJS.
Appreciate any advice.
As stated already, the validation plugin will be the most elegant, less re-inventive solution.
Edit: After commentary implementation utilizing validation plugin
With that aside, you have a couple options.
If you are confident the contact object will always contain only required fields, a not very robust implementation would be iterate over the properties of the contact ensuring each has some value.
A little more robust, but still lacking the elegance of the plugin, implementation would be to maintain an array of required fields and use that array for validation. You can reference my example for this setup. Essentially, each required property is mapped to observables. Changes made to the value of any observable property triggers (via a subscription) a mutation call for a dummy observable that is used in a computed. This is required since a computed can't call valueHasMutated. The mutation call triggers the computed to reevaluate, thus updating the UI.