Manually create a computed property - javascript

Here is the problem. I'm currently creating a mixin to handle form validation. The problem is that I want to create in the init method a computed property isFormValid which is a 'computed.and' property of the 'is<fieldName>Valid' other properties.
I can create it but then it never updates. I guess I need to add observers but maybe someone will have a better solution ?
EDIT
Here is some clarification.
My controller got that property:
App.FormViewController = Ember.Controller.extend(App.ValidatorMixin, {
validations: {
field1: {
errLvl: App.Validation.ErrLvl.ERROR,
type: App.Validation.Type.TEXT,
pattern: /^\d{6}$/,
message: 'Error message'
},
field2: {
//somecode
}
}
});
The mixin is define as follow:
App.ValidatorMixin = Ember.Mixin.create({
init: function() {
this._super();
var self = this;
Ember.keys(this.validations).forEach(function(prop) {
self.set('is' + prop.capitalize() + 'Valid', false); //Is changed when the field is valid
});
}
});
The isFormValid property should be an Ember.computed.and of all these is<fieldName>Valid
EDIT2
Every input on the form are defined by the input helper and this mixin:
App.Mixin.ValidatableInput = Ember.Mixin.create({
focusOut: function() {
this.validate();
},
//Do the validation
validate: function() {
//I'm currently moving that part to the controller because it's part
//of the logic but it was easier for a start to write it here
//We update the is<fieldName>Valid property
this.get('parentView.controller').set('is' + this.get('name').capitalize() + 'Valid', !hasError);
//Then some DOM manipulation to attach the error message
}
});
And finally my view looks like that
{{view App.CustomTextField name="field1" value=field1}}
{{view view.buttonCreate disabled=isFormInvalid}}

From within your init method you can setup a computed property using defineProperty.
Ember.defineProperty(myObject, 'myProperty', Ember.computed(function computed() {
return 'myValue';
}).property('myObserver'));

Would this work for you? (expanding off #Wildhoney, I can't comment). You can specify multiple properties if you have several:
Ember.defineProperty(myObject, 'myProperty', Ember.computed(function computed() {
return 'myValue';
}).property('myObserver.#each.myObservedProperty'));
I've had a bit of luck using jQuery Validate in Ember by triggering the .validate() call and rules on didInsertElement for your form view, and then a .valid() check when you focus out of each Ember.TextField. I would highly recommend it for form validation.

Related

meteor: recalculating helper value after typing some input values

My template looks like this:
<input type="text" name="inputValue" id="inputVal">
{{#each group}}
<section>
<h2>{{title}}</h2>
{{#each element}}
<p>{{description}} {{numberValue}} {{unit}}</p>
{{/each}}
</section>
{{/each}}
So far this is working. But now I want to do a little calculating by multiplicate the value of the inputfield with the numberValue (group.element.numberValue) and use this with {{calculatedValue}} in the template:
<p>{{description}} {{calculatedValue}} {{unit}}</p>
Therefore I need a helper:
Template.usedTemplate.helpers({
'calculatedValue': function() {
return document.getElementById("#inputVal").value * this.numberValue;
}
});
I hope this is the correct coding for creating the helper as the values are used in an each-loop.
But my problem is, that the user types the inputValue after the site has been loaded. So what do I have to do, to recalculate the rows after the user typed some values in the input field?
(Also it would be great if there would be a kind of output info text at the beginning as there is no value given by default. After typing some value the result is been shown.)
I think you can simply use Session or ReactiveVar to solve your problem
Session.setDefault("input-value", "")
Template.usedTemplate.events({
'keypress #inputVal': function (event) {
Session.set("input-value", Template.instance().$("#inputVal").val());
}
});
Template.usedTemplate.helpers({
'calculatedValue': function() {
return Session.get("input-value") * this.numberValue;
}
});
or use ReactiveVar
Template.usedTemplate.onCreated(function(){
this.inputValue = new ReactiveVar("")
})
Template.usedTemplate.events({
'keypress #inputVal': function (event) {
Template.instance().set(Template.instance().$("#inputVal").val());
}
});
Template.usedTemplate.helpers({
'calculatedValue': function() {
return Template.instance().get() * this.numberValue;
}
});
I suggest using ReactiveVar, because Session is global val.
And you should add ReactiveVar to your app before using.
meteor add reactive-var
I haven't tested this code, but try this:
var userInput = new Tracker.Dependency;
Template.usedTemplate.events({
'keypress #inputVal': function (event) {
userInput.changed();
}
});
Template.usedTemplate.helpers({
'calculatedValue': function() {
userInput.depend();
return Template.instance().$("#inputVal").val() * this.numberValue;
}
});
Side note: document.getElementById() already expects an ID as an argument, so you don't need the # prefix.
Normally, the calculatedValue helper will not update automatically because it contains no reactive method calls, such as someCollection.find() or Session.get('xyz'). But using Tracker.Dependency, you can make anything reactive.

How to persist local CSS change to server record?

I have a client method that grays out a rendered document in the DOM by adding a class whose opacity is set to 0.4:
'click .detailCheckbox': function(ev){
var detail = $(ev.target).parent();
if(!detail.hasClass('toggle')){
detail.addClass('toggle');
} else {
detail.removeClass('toggle');
}
}
When I reload the page, though, the DOM element is no longer grayed out, because I never updated the document on the server.
Am I going to have to get super creative here, or am I missing a simple way to solve this?
You can use the meteor-persistent-session package.
Session.setPersistent(key, value) //store a persistent session variable (persistent)
For example.
if(!detail.hasClass('toggle')){
detail.addClass('toggle');
Session.setPersistent('opacity',0.4)
} else {
detail.removeClass('toggle');
Session.clear('opacity')
}
I solved it by creating a new field in the model, which is set (or unset) to the class name that has opacity 0.4
Here is the Meteor method insert method that contains the field checkboxStatus
addDetail: function(detailFormData){
if(! Meteor.userId()){
throw new Meteor.Error('not-authorized');
}
detailsCollection.insert({
detail: detailFormData.detail,
parentId: detailFormData.parentId,
checkboxStatus: detailFormData.checkboxStatus
});
}
I call it on the clicking of the checkbox:
'click .detailCheckbox': function(ev){
ev.preventDefault();
Meteor.call('setToggle', this._id);
}
And the setToggle Meteor method checks the toggle status, and then updates the document accordingly
setToggle: function(detailId){
var checked_detail = detailsCollection.findOne({_id: detailId});
if(checked_detail.checkboxStatus != 'toggle'){
detailsCollection.update(detailId, {
$set: {checkboxStatus: 'toggle'}
});
} else {
detailsCollection.update(detailId, {
$set: {checkboxStatus: 'untoggle'}
});
}
}
Which is then called by a template helper method
checkboxStatus: function(){
var checked_detail = detailsCollection.findOne({_id: this._id});
return checked_detail.checkboxStatus;
}
whose value is returned in the class tag of the template item itself
<li id={{_id}} class="detailViewEntry {{checkboxStatus}}">

Meteor JS: What is the best way to store states for a specific template instance?

I'm learning about Session and reactive data sources in Meteor JS. They work great for setting global UI states. However, I can't figure out how to scope them to a specific instance of a template.
Here's what I'm trying to do
I have multiple contenteditable elements on a page. Below each is an "Edit" button. When the user clicks on the Edit button, it should focus on the element and also show "Save" and "Cancel" buttons.
If the user clicks "Cancel", then any changes are eliminated, and the template instance should rerender with the original content.
Here's the code I have so far
// Helper
Template.form.helpers({
editState: function() {
return Session.get("editState");
}
});
// Rendered
Template.form.rendered = function(e){
var $this = $(this.firstNode);
var formField = this.find('.form-field');
if (Session.get("editState")) formField.focus();
};
// Event map
Template.form.events({
'click .edit-btn' : function (e, template) {
e.preventDefault();
Session.set("editState", "is-editing");
},
'click .cancel-btn' : function (e, template) {
e.preventDefault();
Session.set("editState", null);
},
});
// Template
<template name="form">
<div class="{{editState}}">
<p class="form-field" contenteditable>
{{descriptionText}}
</p>
</div>
Edit
Save
Cancel
</template>
// CSS
.edit-btn
.cancel-btn,
.save-btn {
display: inline-block;
}
.cancel-btn,
.save-btn {
display: none;
}
.is-editing .cancel-btn,
.is-editing .save-btn {
display: inline-block;
}
The problem
If I have more than one instance of the Form template, then .form-field gets focused for each one, instead of just the one being edited. How do I make so that only the one being edited gets focused?
You can render a template with data, which is basically just an object passed to it when inserted in to a page.
The data could simply be the key to use in the Session for editState.
eg, render the template with Template.form({editStateKey:'editState-topForm'})
you could make a handlebars helper eg,
Handlebars.registerHelper('formWithOptions',
function(editStateKey){
return Template.form({editStateKey:editStateKey})
});
then insert it in your template with
{{{formWithOptions 'editState-topForm'}}} (note the triple {, })
Next, change references from Session.x('editState') to Session.x(this.editStateKey)/ Session.x(this.data.editStateKey)
Template.form.helpers({
editState: function() {
return Session.get(this.editStateKey);
}
});
// Rendered
Template.form.rendered = function(e){
var $this = $(this.firstNode);
var formField = this.find('.form-field');
if (Session.get(this.data.editStateKey)) formField.focus();
};
// Event map
Template.form.events({
'click .edit-btn' : function (e, template) {
e.preventDefault();
Session.set(this.editStateKey, "is-editing");
},
'click .cancel-btn' : function (e, template) {
e.preventDefault();
Session.set(this.editStateKey, null);
},
});
Note: if you are using iron-router it has additional api's for passing data to templates.
Note2: In meteor 1.0 there is supposed to be better support for writing your own widgets. Which should allow better control over this sort of thing.
As a matter of policy I avoid Session in almost all cases. I feel their global scope leads to bad habits and lack of good discipline regarding separation-of-concerns as your application grows. Also because of their global scope, Session can lead to trouble when rendering multiple instances of a template. For those reasons I feel other approaches are more scalable.
Alternative approaches
1 addClass/removeClass
Instead of setting a state then reacting to it elsewhere, can you perform the needed action directly. Here classes display and hide blocks as needed:
'click .js-edit-action': function(event, t) {
var $this = $(event.currentTarget),
container = $this.parents('.phenom-comment');
// open and focus
container.addClass('editing');
container.find('textarea').focus();
},
'click .js-confirm-delete-action': function(event, t) {
CardComments.remove(this._id);
},
2 ReactiveVar scoped to template instance
if (Meteor.isClient) {
Template.hello.created = function () {
// counter starts at 0
this.counter = new ReactiveVar(0);
};
Template.hello.helpers({
counter: function () {
return Template.instance().counter.get();
}
});
Template.hello.events({
'click button': function (event, template) {
// increment the counter when button is clicked
template.counter.set(template.counter.get() + 1);
}
});
}
http://meteorcapture.com/a-look-at-local-template-state/
3 Iron-Router's state variables
Get
Router.route('/posts/:_id', {name: 'post'});
PostController = RouteController.extend({
action: function () {
// set the reactive state variable "postId" with a value
// of the id from our url
this.state.set('postId', this.params._id);
this.render();
}
});
Set
Template.Post.helpers({
postId: function () {
var controller = Iron.controller();
// reactively return the value of postId
return controller.state.get('postId');
}
});
https://github.com/iron-meteor/iron-router/blob/devel/Guide.md#setting-reactive-state-variables
4 Collection data
Another approach is to simply state by updating data in your collection. Sometimes this makes perfect sense.
5 update the data context
Session is often the worse choice in my opinion. Also I don't personally use #3 as I feel like being less tied to iron-router is better incase we ever want to switch to another router package such as "Flow".

Ember.js checkbox for array controller computed property

I have a checkbox that I would like to trigger a simple 'select all' functionality. The problem is that I can't figure out how to connect the checkbox's action to an action in my controller so that I can actually update the records.
App.LanguagesController = Ember.ArrayController.extend({
actions: {
toggleAllVisibility: function() {
var newVisibility = !this.get('allAreVisible');
var needingVisibilityChange = this.filterBy('visible', !newVisibility);
needingVisibilityChange.setEach('visible', newVisibility);
}
},
allAreVisible: function(param) {
return this.filterBy('visible', false).get('length') === 0;
}.property('#each.visible'),
});
In my template, I have the following input helper
{{input type='checkbox' checked=allAreVisible}}
This properly updates the checkbox when I change the elements manually (i.e. if all of them are selected, then checkbox updates), but no actions fire when I toggle the checkbox.
It looks like in older versions of Ember.js I could simply add an action parameter to the input helper but that doesn't work anymore. I'm assuming I need to setup something that observes when the computed property attempts to change, but I couldn't find anything in the docs or other help.
I also tried extending checkbox to send the click event:
App.AllLanguagesCheckbox = Ember.Checkbox.extend(Ember.ViewTargetActionSupport, {
click: function() {
this.triggerAction({
action: 'toggleAllVisibility'
});
}
});
And then loaded that in my template with
{{view App.AllLanguagesCheckbox checkedBinding=allAreVisible}}
That allows the checkbox to trigger the action, but does not update based on the computed property in the controller.
I feel like I'm missing something obvious here.
EDIT
Based on kingpin2k's answer below, here's the working controller code:
App.LanguagesController = Ember.ArrayController.extend({
toggleAllVisibility: function() {
var newVisibility = !this.get('controller').get('allAreVisible');
var needingVisibilityChange = this.get('controller').filterBy('visible', !newVisibility);
needingVisibilityChange.setEach('visible', newVisibility);
},
allAreVisible: function(param) {
return this.filterBy('visible', false).get('length') === 0;
}.property('#each.visible'),
});
It's not called with the normal scope so you have to explicitly go through the controller to get the model array, but it works as expected now.
Here's the accompanying input helper:
{{input type='checkbox' checked=allAreVisible change=toggleAllVisibility}}
The problem is your checkbox is connected to a computed property, the computation should derive the value (aka you shouldn't be setting it), which is what would be happening when someone tries to check.
_allAreVisible:false,
allAreVisible: function(param) {
if(this.filterBy('visible', false).get('length') === 0){
// set to true;
// else set to false
}.observes('#each.visible'),
http://emberjs.jsbin.com/abODIKoj/1/edit

Can we access the respective controller of the view inside the init hook of the view

I have a case were I need to choose the template of the view based on the initial property value of the controller. Thus I need to access the controller while I am inside the init hook of the view but when i access the controller it returns "null".
MyApp.ApplicationController = Em.Controller.extend({
templateVersion: 'small'
});
MyApp.ApplicationView = Em.View.extend({
init: function(){
this._super();
console.log('Controller is: ',this.get('controller'));
if(this.get('controller').get('templateVersion') == 'small')
{
this.set('templateName', 'application-small');
} else {
this.set('templateName', 'application-bigger');
}
}
});
This is not the real case but an example for the real scenario.
For an example I have setup a jsbin here
I guess a more appropriate way of doing this would be by determine dynamically the templateName, something like the following:
MyApp.ApplicationView = Ember.View.extend({
templateName: function() {
if (this.get("controller.templateVersion") == "small") {
return "application-small";
} else {
return "application-bigger";
}
}.property('controller.templateVersion')
});
Doing it this way you dont need to hook into the init function and thus not having your controller properties available.
Here your updated jsbin.
Update
After your last comment I realized that the delay is the important part to make your use case work, here is an improved version which indeed changes even if the templateVersion is initially not defined and get's setted with some delay, this time we observe the templateName property of the view and invoke a rerender.
MyApp.ApplicationView = Ember.View.extend({
templateName: function() {
if (this.get("controller.templateVersion") == "small") {
return "application-small";
} else {
return "application-bigger";
}
}.property('controller.templateVersion'),
templateChanged: function() {
this.rerender();
}.observes('templateName')
});
And here another jsbin with the new version with a simulated delay of 2 seconds, but it could be whatever value.
Hope it helps.

Categories