I have a directive which should allow me to change a specific mode, updating the scope as well.
This works fine.
The issue I'm facing now is that I need to be able to hide the element for the current mode.
I'll explain it better:
I have modes array:
[1,2,3,4,5]
and some links inside an ng-repeater:
<a href=""
ng-repeat="m in zoneModes"
ng-click="changeZoneMode({{m}})"
id="{{m}}"
class="menu-item">
<i class="icon-mode-{{m}}"></i>
</a>
what I have to do is hide an element if selectedZone is not equal to m:
ng-if="selectedMode!=m"
It seams like everything is fine doing this, but it is not.
You can clearly see what's the problem from this live example
Basically, everytime the condition is meet, it removes the element and never put it back.
I also tried with ng-show, but the result is not good either as it leaves an empty spot (you can check this other example)
Any suggestions? Thanks
EDIT
Trying to use the filter as suggested, but with no luck:
.directive('mySelector', function() {
var changeMode, linkFunction;
changeMode = function(newmode) {
var $scope;
$scope = this.$parent;
$scope.selectedMode = newmode;
$('.menu-open').attr('checked', false);
};
linkFunction = function(scope, element, attrs) {
scope.changeMode = changeMode;
scope.changeZoneMode = changeMode;
scope.zoneModes = [1, 2, 3, 4, 5];
return scope.$watch((function() {
return scope.selectedMode;
}), function(newMode) {
return scope.selectedMode = newMode;
});
};
return {
restrict: 'E',
scope: {
selectedMode: '=currentmode',
id: '#id'
},
replace: true,
templateUrl: 'templates/template.html',
link: linkFunction,
controller: 'abc'
};
}).controller('abc', ['$scope', function($scope) {
$scope.checkM = function (mode, m) {
return mode == m;
};
}])
template.html
<a href=""
ng-repeat="m in zoneModes|filter:checkM(selectedMode,m)"
ng-click="changeZoneMode({{m}})"
id="{{m}}"
class="menu-item">
<i class="icon-mode-{{m}}"></i>
</a>
Have you tried using a filter on your ng-repeat? If you use a filter your element is not lost, changing the value of m will show it again. Try something like this:
ng-repeat="m in zoneModes|filter:checkM(selectedMode,m)"
Then in your controller you need to make a filter:
$scope.checkM = function (mode, m) {
return mode == m;
};
I might not quite understand your question but is ng-if="selectedMode!='m'" giving you the result you want?
At the end , I managed to solve this using a filter, and here's the final code:
angular.module('App', ['ionic'])
.controller('ctrl', ['$scope', function($scope) {
$scope.current = {
'id': 0,
'mode': 1
}
}])
.directive('mySelector', function() {
var changeMode, linkFunction;
changeMode = function(newmode) {
var $scope;
$scope = this.$parent;
$scope.selectedMode = newmode;
$('.menu-open').attr('checked', false);
};
linkFunction = function(scope, element, attrs) {
scope.changeMode = changeMode;
scope.changeZoneMode = changeMode;
scope.zoneModes = [1, 2, 3, 4, 5];
return scope.$watch((function() {
return scope.selectedMode;
}), function(newMode) {
return scope.selectedMode = newMode;
});
};
return {
restrict: 'E',
scope: {
selectedMode: '=currentmode',
id: '#id'
},
replace: true,
templateUrl: 'templates/template.html',
link: linkFunction,
};
})
.filter('abc', function() {
return function( m, sel ) {
console.log('abc',arguments);
var filteredModes = [];
angular.forEach(m, function(mode) {
if( sel != mode ) {
filteredModes.push(mode);
}
});
return filteredModes;
}
});
and it is now working as expected.
Related
I'm learning vuejs and trying to do all without jquery
I need to get a value of a css style line-height.
In jquery i would do:
let x = $(this).css("line-height");
How can I get this value using vuejs 2.5?
I was exploring this.$el in this structure, but can't find solution to get this value:
data: function () {
return {
lineHeight: null
}
},
mounted(){
this.lineHeight = ?
}
tl;dr
// with jQuery: $(this).css("line-height");
// with Vue:
mounted() {
this.lineHeight = window.getComputedStyle(this.$el).getPropertyValue('line-height');
}
If the component (this.$el) may be inside an iframe or popup, or if you want to be extra careful, read on.
JSFiddle demo here.
new Vue({
el: '#app',
data: {
lineHeightTLDR: '',
lineHeightFull: '',
},
mounted(){
this.lineHeightTLDR = window.getComputedStyle(this.$el).getPropertyValue('line-height');
this.lineHeightFull = this.css('line-height');
},
methods: {
css(propertyName) {
let view = this.$el.ownerDocument.defaultView;
if ( !view || !view.opener ) {
view = window;
}
let computed = view.getComputedStyle(this.$el);
return computed.getPropertyValue(propertyName) || computed[propertyName];
}
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<pre>lineHeight tl;dr..: {{ lineHeightTLDR }}</pre>
<pre>lineHeight full...: {{ lineHeightFull }}</pre>
</div>
Background
Simplest way to mimic jQuery is just to take a look at its source. The returned value from .css() is, roughly:
ret = computed.getPropertyValue( name ) || computed[ name ];
Which uses CSSStyleDeclaration.getPropertyValue on computed. And computed is:
return function( elem ) {
var view = elem.ownerDocument.defaultView;
if ( !view || !view.opener ) {
view = window;
}
return view.getComputedStyle( elem );
}
Which uses Window.getComputedStyle() As you can see, the returned value is something around:
ret = view.getComputedStyle(elem).getPropertyValue( name ) || view.getComputedStyle(elem)[name];
Where view is most probably window but could be something else (elem.ownerDocument.defaultView).
In the end of the day, if you want to be extra certain and do very close to jQuery.css(), use:
// with jQuery: $(this).css("line-height");
// with Vue:
mounted(){
this.lineHeight = this.css('line-height');
},
methods: {
css(propertyName) {
let view = elem.ownerDocument.defaultView;
if ( !view || !view.opener ) {
view = window;
}
let computed = view.getComputedStyle(this.$el);
ret = computed.getPropertyValue(propertyName) || computed[propertyName];
}
}
But if you know your usage does not rely on iframes or popups (as it is very unusual for a Vue instance JavaScript code to run at a window and have the $el it is attached to on another), go with the tl;dr version.
Context: I have a list of posts with tags, categories from wordpress api. I display these posts with Vue and using computed with a search box to filter the result based on titre, description, tags, and categories
Problem: I am trying to update a computed list when user click on a list of tag available. I add the get and set for computed data like this:
var vm = new Vue({
el: '#blogs',
data: {
search: '',
posts: [],
filterPosts: []
},
beforeMount: function() {
// It should call the data and update
callData();
},
computed: {
filterPosts: {
get: function() {
var self = this;
return self.posts.filter(function(post){
var query = self.search.toLowerCase();
var title = post.title.toLowerCase();
var content = post.content.toLowerCase();
var date = post.date.toLowerCase();
var categories = '';
post.categories.forEach(function(category) {
categories += category.name.toLowerCase();
});
var tags = '';
post.tags.forEach(function(tag){
tags += tag.name.toLowerCase();
});
return title.indexOf(query) !== -1 ||content.indexOf(query) !== -1 || date.indexOf(query) !== -1 || categories.indexOf(query) !== -1 || tags.indexOf(query) !== -1;
});
},
set: function (newValue) {
console.log(newValue);
this.filterPosts = Object.assign({}, newValue);
}
}
},
methods: {
filterByTag: function(tag, event) {
event.preventDefault();
var self = this;
self.filterPosts = self.posts.filter(function(post){
var tags = '';
post.tags.forEach(function(tag){
tags += tag.name.toLowerCase();
});
return tags.indexOf(tag.toLowerCase()) !== -1;
});
}
}
}); // Vue instance
The console.log always output new data based on the function I wrote on methods but Vue didn't re-render the view. I think I didn't do the right way or thought like Vue. Could you please give some insight?
Edit 1
Add full code.
I tried to add filterPosts in data but I received this error from Vue: The computed property "filterPosts" is already defined in data.
Your setter is actually not setting anything, it only logs the new value. You need to store it somewhere.
For example you can store it in the component's data:
data: {
value: 'foo',
},
computed: {
otherValue: {
get() { /*...*/ },
set(newVal) { this.value = newVal },
},
},
But this is definitely not the only possibility, if you use Vuex, the setter can dispatch an action that will then make the computed value get updated. The component will eventually catch the update and show the new value.
computed: {
value: {
get() {
return this.$store.getters.externalData;
},
set(newVal) {
return this.$store.dispatch('modifyingAction', newVal);
},
},
},
The bottomline is you have to trigger a data change in the setter, otherwise your component will not be updated nor will it trigger any rerender.
EDIT (The original answer was updated with full code):
The answer is that unless you want to manually change the list filteredPosts without altering posts, you don't need a get and set function for your computed variable. The behaviour you want can be acheived with this:
const vm = new Vue({
data() {
return {
search: '',
posts: [],
// these should probably be props, or you won't be able to edit the list easily. The result is the same anyway.
};
},
computed: {
filteredPosts() {
return this.posts.filter(function(post) {
... // do the filtering
});
},
},
template: "<ul><li v-for='post in filteredPosts'>{{ post.content }}</li></ul>",
});
This way, if you change the posts or the search variable in data, filteredPosts will get recomputed, and a re-render will be triggered.
After going around and around, I found a solution, I think it may be the right way with Vue now: Update the computed data through its dependencies properties or data.
The set method didn't work for this case so I add an activeTag in data, when I click on a tag, it will change the activeTag and notify the computed filterPost recheck and re-render. Please tell me if we have another way to update the computed data.
var vm = new Vue({
el: '#blogs',
data: {
search: '',
posts: [],
tags: [],
activeTag: ''
},
beforeMount: function() {
// It should call the data and update
callData();
},
computed: {
filterPosts: {
get: function() {
var self = this;
return self.posts.filter(function(post){
var query = self.search.toLowerCase();
var title = post.title.toLowerCase();
var content = post.content.toLowerCase();
var date = post.date.toLowerCase();
var categories = '';
post.categories.forEach(function(category) {
categories += category.name.toLowerCase();
});
var tags = '';
post.tags.forEach(function(tag){
tags += tag.name.toLowerCase();
});
var activeTag = self.activeTag;
if (activeTag !== '') {
return tags.indexOf(activeTag.toLowerCase()) !== -1;
}else{
return title.indexOf(query) !== -1 ||content.indexOf(query) !== -1 || date.indexOf(query) !== -1 || categories.indexOf(query) !== -1 || tags.indexOf(query) !== -1;
}
});
},
set: function (newValue) {
console.log(newValue);
}
}
},
methods: {
filterByTag: function(tag, event) {
event.preventDefault();
var self = this;
self.activeTag = tag;
}
}
}); // Vue instance
Try something like:
data: {
myValue: 'OK'
},
computed: {
filterPosts: {
get: function () {
return this.myValue + ' is OK'
}
set: function (newValue) {
this.myValue = newValue
}
}
}
More:
https://v2.vuejs.org/v2/guide/computed.html#Computed-Setter
I have a side menu and when it's open, the body can be partially seen. My side menu might be long so you could scroll on it. But when the menu is at the bottom you then scroll on the body, and I don't want this behaviour.
Similar to Scrolling only content div, others should be fixed but I'm using React. Other content should be scrollable when my side menu is closed. Think of the content as side menu in the example in the link. So far I'm using the same technique provided by that answer but it's ugly (kinda jQuery):
preventOverflow = (menuOpen) => { // this is called when side menu is toggled
const body = document.getElementsByTagName('body')[0]; // this should be fixed when side menu is open
if (menuOpen) {
body.className += ' overflow-hidden';
} else {
body.className = body.className.replace(' overflow-hidden', '');
}
}
// css
.overflow-hidden {
overflow-y: hidden;
}
What should I do with Reactjs?
You should make a meta component in react to change things on the body as well as changing things like document title and things like that. I made one a while ago to do that for me. I'll add it here.
Usage
render() {
return (
<div>
<DocumentMeta bodyClasses={[isMenuOpen ? 'no-scroll' : '']} />
... rest of your normal code
</div>
)
}
DocumentMeta.jsx
import React from 'react';
import _ from 'lodash';
import withSideEffect from 'react-side-effect';
var HEADER_ATTRIBUTE = "data-react-header";
var TAG_NAMES = {
META: "meta",
LINK: "link",
};
var TAG_PROPERTIES = {
NAME: "name",
CHARSET: "charset",
HTTPEQUIV: "http-equiv",
REL: "rel",
HREF: "href",
PROPERTY: "property",
CONTENT: "content"
};
var getInnermostProperty = (propsList, property) => {
return _.result(_.find(propsList.reverse(), property), property);
};
var getTitleFromPropsList = (propsList) => {
var innermostTitle = getInnermostProperty(propsList, "title");
var innermostTemplate = getInnermostProperty(propsList, "titleTemplate");
if (innermostTemplate && innermostTitle) {
return innermostTemplate.replace(/\%s/g, innermostTitle);
}
return innermostTitle || "";
};
var getBodyIdFromPropsList = (propsList) => {
var bodyId = getInnermostProperty(propsList, "bodyId");
return bodyId;
};
var getBodyClassesFromPropsList = (propsList) => {
return propsList
.filter(props => props.bodyClasses && Array.isArray(props.bodyClasses))
.map(props => props.bodyClasses)
.reduce((classes, list) => classes.concat(list), []);
};
var getTagsFromPropsList = (tagName, uniqueTagIds, propsList) => {
// Calculate list of tags, giving priority innermost component (end of the propslist)
var approvedSeenTags = {};
var validTags = _.keys(TAG_PROPERTIES).map(key => TAG_PROPERTIES[key]);
var tagList = propsList
.filter(props => props[tagName] !== undefined)
.map(props => props[tagName])
.reverse()
.reduce((approvedTags, instanceTags) => {
var instanceSeenTags = {};
instanceTags.filter(tag => {
for(var attributeKey in tag) {
var value = tag[attributeKey].toLowerCase();
var attributeKey = attributeKey.toLowerCase();
if (validTags.indexOf(attributeKey) == -1) {
return false;
}
if (!approvedSeenTags[attributeKey]) {
approvedSeenTags[attributeKey] = [];
}
if (!instanceSeenTags[attributeKey]) {
instanceSeenTags[attributeKey] = [];
}
if (!_.has(approvedSeenTags[attributeKey], value)) {
instanceSeenTags[attributeKey].push(value);
return true;
}
return false;
}
})
.reverse()
.forEach(tag => approvedTags.push(tag));
// Update seen tags with tags from this instance
_.keys(instanceSeenTags).forEach((attr) => {
approvedSeenTags[attr] = _.union(approvedSeenTags[attr], instanceSeenTags[attr])
});
instanceSeenTags = {};
return approvedTags;
}, []);
return tagList;
};
var updateTitle = title => {
document.title = title || document.title;
};
var updateBodyId = (id) => {
document.body.setAttribute("id", id);
};
var updateBodyClasses = classes => {
document.body.className = "";
classes.forEach(cl => {
if(!cl || cl == "") return;
document.body.classList.add(cl);
});
};
var updateTags = (type, tags) => {
var headElement = document.head || document.querySelector("head");
var existingTags = headElement.querySelectorAll(`${type}[${HEADER_ATTRIBUTE}]`);
existingTags = Array.prototype.slice.call(existingTags);
// Remove any duplicate tags
existingTags.forEach(tag => tag.parentNode.removeChild(tag));
if (tags && tags.length) {
tags.forEach(tag => {
var newElement = document.createElement(type);
for (var attribute in tag) {
if (tag.hasOwnProperty(attribute)) {
newElement.setAttribute(attribute, tag[attribute]);
}
}
newElement.setAttribute(HEADER_ATTRIBUTE, "true");
headElement.insertBefore(newElement, headElement.firstChild);
});
}
};
var generateTagsAsString = (type, tags) => {
var html = tags.map(tag => {
var attributeHtml = Object.keys(tag)
.map((attribute) => {
const encodedValue = HTMLEntities.encode(tag[attribute], {
useNamedReferences: true
});
return `${attribute}="${encodedValue}"`;
})
.join(" ");
return `<${type} ${attributeHtml} ${HEADER_ATTRIBUTE}="true" />`;
});
return html.join("\n");
};
var reducePropsToState = (propsList) => ({
title: getTitleFromPropsList(propsList),
metaTags: getTagsFromPropsList(TAG_NAMES.META, [TAG_PROPERTIES.NAME, TAG_PROPERTIES.CHARSET, TAG_PROPERTIES.HTTPEQUIV, TAG_PROPERTIES.CONTENT], propsList),
linkTags: getTagsFromPropsList(TAG_NAMES.LINK, [TAG_PROPERTIES.REL, TAG_PROPERTIES.HREF], propsList),
bodyId: getBodyIdFromPropsList(propsList),
bodyClasses: getBodyClassesFromPropsList(propsList),
});
var handleClientStateChange = ({title, metaTags, linkTags, bodyId, bodyClasses}) => {
updateTitle(title);
updateTags(TAG_NAMES.LINK, linkTags);
updateTags(TAG_NAMES.META, metaTags);
updateBodyId(bodyId);
updateBodyClasses(bodyClasses)
};
var mapStateOnServer = ({title, metaTags, linkTags}) => ({
title: HTMLEntities.encode(title),
meta: generateTagsAsString(TAG_NAMES.META, metaTags),
link: generateTagsAsString(TAG_NAMES.LINK, linkTags)
});
var DocumentMeta = React.createClass({
propTypes: {
title: React.PropTypes.string,
titleTemplate: React.PropTypes.string,
meta: React.PropTypes.arrayOf(React.PropTypes.object),
link: React.PropTypes.arrayOf(React.PropTypes.object),
children: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
]),
bodyClasses: React.PropTypes.array,
},
render() {
if (Object.is(React.Children.count(this.props.children), 1)) {
return React.Children.only(this.props.children);
} else if (React.Children.count(this.props.children) > 1) {
return (
<span>
{this.props.children}
</span>
);
}
return null;
},
});
DocumentMeta = withSideEffect(reducePropsToState, handleClientStateChange, mapStateOnServer)(DocumentMeta);
module.exports = DocumentMeta;
This component could probably be changed a little for your case (withSideEffect is used for both client and server side rendering... if you arent using server side rendering then its probably not completely necessary) but the component will work on client side rendering if you would like to use it there as well.
ReactJS doesn't have direct access to the <body> element, and that's the element that needs to have its overflow-y style changed. So while what you're doing isn't perhaps the prettiest code, it's not entirely wrong either.
The only real suggestion I'd give is (shudder) using inline styles on the body instead of a classname so as to avoid having to introduce the CSS declaration. As long as your menu is the only thing responsible for updating the overflow-y attribute, there's no reason you can't use an inline style on it. Mashing that down with the ?: operator results in fairly simple code:
body.style.overflowY = menuOpen ? "hidden" : "";
And then you can just delete the .overflow-hidden class in its entirety.
If for some reason multiple things are managing the overflow state of the body, you might want to stick with classnames and assign a unique one for each thing managing it, something like this:
if (menuOpen) {
body.className += ' menu-open';
}
else {
// Use some tricks from jQuery to remove the "menu-open" class more elegantly.
var className = " " + body.className + " ";
className = className.replace(" overflow-hidden ", " ").replace(/\s+/, " ");
className = className.substr(1, className.length - 2);
}
CSS:
body.menu-open {
overflow-y: hidden;
}
I'm creating a chat app. I hope i can add a new "hello" message if i check the messages count of current chat is equal to 0 (Problem #1). Also i have a dictionary as a collection for translation. But t() returns EN variant (Problem #2)
t = function(text) {
var res = Dictionary.findOne({o:text});
return res && res.t || text;
}
Meteor.startup(function () {
Deps.autorun(function () {
Meteor.subscribe('dictionary', Session.get('lang'), function(){
Session.set('dictionaryReady', true);
});
Meteor.subscribe('chats', Session.get('domain'), function(){
if (chatCurrent(Meteor.userId(), Session.get('domain')).count()===0 //true, even is not actually [problem_#1]
&& Session.get('dictionaryReady') //true, but next function t() doesn't work properly [problem #2]
) {
var mudata = Session.get('my_manager') ? udata(Session.get('my_manager'), Session.get('domain')) : null,
hello = mudata && mudata.hello || t('Hello! How I can help you?'),
name = mudata && mudata.name || t('Anna');
Meteor.call('create_message', {chat: Meteor.userId(), to: Meteor.userId(), text: hello, name: name, from: Session.get('my_manager'), domain: Session.get('domain'), last_manager: Session.get('my_manager')});
});
});
});
Problem #1 and Problem #2 everytime when page just loaded. So when i refresh the page i get another "hello message" on default EN locale.
Here is how you can render your template only once your subscriptions are ready. This is a solution taken from meteor kitchen generated code.
first you create a "loading" template
<template name="loading">
<div class="loading">
<i class="fa fa-circle-o-notch fa-4x fa-spin"></i>
</div>
</template>
Second, attach to your template a route controller. Here is a simplified version of it (but it should work):
this.myTemplateController = RouteController.extend({
template: "myTemplate",
onBeforeAction: function() {
this.next();
},
action: function() {
if(this.isReady()) { this.render(); } else { this.render("loading"); }
},
isReady: function() {
var subs = [
Meteor.subscribe("sub1", this.params.yourParam),
Meteor.subscribe("sub2", this.params.yourParam),
Meteor.subscribe("sub3", this.params.yourParam)
];
var ready = true;
_.each(subs, function(sub) {
if(!sub.ready())
ready = false;
});
return ready;
},
data: function() {
return {
params: this.params || {},
yourParamWhatever: Chat.findOne({_id:this.params.yourParam}, {})
};
},
});
Now you should have all your subscriptions ready when your template is loaded.
Concerning the translation, you could have a look at TAPi18n package that I highly recommend. It is quite easy to implement.
I have a collection of dates that I will use to instanciate my jquery datepicker widget. I have used beforeShowDay method to add a highlight css class to show on what days events are. The issue I encounter is that the css class is reset when I click on a date. Am I doing something wrong ?
Thanks
$("#datepicker").datepicker({
inline: true,
showOtherMonths: true,
showButtonPanel: false,
beforeShowDay: function(date) {
var result = [true, '', null];
var matching = $.grep(events, function(event) {
return event.date.valueOf() === date.valueOf();
});
if (matching.length) {
result = [true, 'highlight', null];
}
return result;
},
onSelect: function(dateText) {
}
});
Try this way, maybe you are not returning "true".
beforeShowDay: function(dates) {
for (i = 0, vetorLen = freedays.length; i < vetorLen; i++) {
if ($.inArray(dates,freedays) != -1) {
return [true, 'css-class-to-highlight', ''];
} else {
return [false, '', ''];
}
}
return [true];
},
hope this help you.