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}}">
Related
I’m working on a left menu bar that expands on a button click.
I want to save the state of the menu, if it is expanded or not.
When it refreshes the class must still be added.
$('#menu-action').click(function() {
$('.sidebar').toggleClass('active');
$('.main').toggleClass('active');
$(this).toggleClass('active');
if ($('.sidebar').hasClass('active')) {
$(this).find('i').addClass('fa-close');
$(this).find('i').removeClass('fa-bars');
} else {
$(this).find('i').addClass('fa-bars');
$(this).find('i').removeClass('fa-close');
}
});
// Add hover feedback on menu
$('#menu-action').hover(function() {
$('.sidebar').toggleClass('hovered');
});
Try Local Storage:
$(document).ready(function() {
if(localStorage.getItem("active")) {
$('.sidebar').addClass("active")
}
});
$(window).unload(function() {
localStorage.setItem("active", $('.sidebar').hasClass("active"));
});
Local storage is not supported by all browsers. See the link above. You can use extensions like store.js to support old browsers.
Another option is to use cookie plugin as mentioned here.
Since you have not yet made it clear on how you want to read or write cookies, I'd recommend using js-cookie to make handling a little easier. Handling cookies with plain JS is possible, but a rather cumbersome task.
A solution using the mentioned library would work like this (Expecting you have added js.cookie.js before your code to your HTML)
// Store references to reusable selectors
var $menuAction = $('#menu-action');
var $menuActionI = $menuAction.find('i'); // the <i> inside #menu-action
var $sidebar = $('.sidebar');
var activeClass = 'active';
// Docs: https://github.com/js-cookie/js-cookie/tree/v2.1.0#basic-usage
var isActive = Cookies.get('site-menu-active') || false;
function toggleMenu() {
$sidebar.toggleClass('active', isActive);
$('.main').toggleClass('active', isActive);
$menuAction.toggleClass('active', isActive);
$menuActionI.toggleClass('fa-close', isActive);
$menuActionI.toggleClass('fa-bars', isActive);
isActive = !isActive;
Cookies.set('site-menu-active', isActive, { expires: 7 });
}
// Calling immediately to set to state read from cookie
toggleMenu();
// Add click interaction
$menuAction.click(toggleMenu);
// Add hover feedback on menu
$menuAction.hover(function() {
$sidebar.toggleClass('hovered');
});
The Html5 storage is the best option for these scenario. Here you can change the localStorage to sessionStorage based on the requirement:
1)localStorage - even close the browser the data is alive
2)sessionStorage - while close the browser the data is removed
We can also remove the stored data
$('#menu-action').click(function() {
$('.sidebar').toggleClass('active');
$('.main').toggleClass('active');
$(this).toggleClass('active');
localStorage.setItem("active", $('.sidebar').hasClass('active'));
if ($('.sidebar').hasClass('active')) {
$(this).find('i').addClass('fa-close');
$(this).find('i').removeClass('fa-bars');
} else {
$(this).find('i').addClass('fa-bars');
$(this).find('i').removeClass('fa-close');
}
});
$(document).ready(function(){
if(localStorage.getItem("active")){
$('.sidebar').addClass('active');
$('.main').addClass('active');
$('#menu-action').find('i').addClass('fa-close');
$('#menu-action').find('i').removeClass('fa-bars');
}
});
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.
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".
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.
I think what I want to do is pretty simple I just don't know how to do it. I would like to fire my own event when one of my models attributes changes for the purpose of passing some data to the event handler (whether the change was an increase or decrease in value).
Basically I want my handler to do this in the view
handler: function(increased) {
if(increased) {
alert("the value increased")
}
else {
alert("the value decreased")
}
}
// ...
this.model.on("change:attr", this.handler, this);
Here you go: You basically listen for change:myvar. When a change occurs you use your model's previous() to get the old value. Depending on whether it increased or decreased you fire the appropriate event. You can listen to these events as shown in the initialize().
(function($){
window.MyModel = Backbone.Model.extend({
initialize: function () {
this.on('change:myvar', this.onMyVarChange);
this.on('increased:myvar', function () {
console.log('Increased');
});
this.on('decreased:myvar', function () {
console.log('Decreased');
});
},
onMyVarChange: function () {
if (this.get('myvar') > this.previous('myvar')) {
this.trigger('increased:myvar');
} else {
this.trigger('decreased:myvar');
}
}
});
window.mymodel = new MyModel({myvar: 1});
mymodel.set({myvar: 2});
mymodel.set({myvar: 3});
mymodel.set({myvar: 1});
})(jQuery);
Running the above will print "Increased", "Increased", "Decreased" to your console.
Just look at previousAttributes()
You can then compare:
If(this.get(attr) > this.previousAttributes()[attr]){
console.log('bigger');
} else {
console.log('smaller');
}
If you use that in your change event handler you're all set. No need for a custom trigger or a ton of code.
EDIT
This is from my Backbone.Validators project and how I obtain the list of all attributes which have changed during the validation step:
var get_changed_attributes = function(previous, current){
var changedAttributes = [];
_(current).each(function(val, key){
if(!_(previous).has(key)){
changedAttributes.push(key);
} else if (!_.isEqual(val, previous[key])){
changedAttributes.push(key);
}
});
return changedAttributes;
};
This requires Underscore 1.3.1 because it's using _.has. If you can't upgrade that's an easy thing to replace though. In your case you'd passing this.previousAttributes() and this.attributes
What if you fire your own custom event after listening to the change event?
handler: function(increased) {
this.model.trigger('my-custom-event', stuff, you, want);
},
myHandler: function(stuff, you, want){
// Do it...
}
// ...
this.model.on("change:attr", this.handler, this);
this.model.on('my-custom-event, this.myHandler, this);