Mutation Observer is triggered even when attribute value do not change - javascript

When I change an attribute value with the same value, looking at the inspector console, the DOM tree does not change, yet Mutation Observer triggers since I modified the attribute value, but for the actual same value.
Can someone explains how this works under the hood? I inserted a snippet to demonstrate my point.
/* OBSERVER */
var divToUpdate = document.querySelector('#update');
var config = {attributeFilter: ['data-update']};
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.target.dataset.update == 'true') {
console.log('Attribute value updated, but not really!', mutation);
}
});
});
observer.observe(divToUpdate, config);
/* BUTTON UPDATER */
document.querySelector('button').addEventListener('click', function() {
divToUpdate.setAttribute('data-update', true);
});
<div id="update" data-update="true">DIV WITH ATTRIBUTE</div>
<button type="button">UPDATE DIV ATTRIBUTE</button>

According to this history, MutationObserver was designed to work that way. Any call to setAttribute triggers a mutation, regardless of whether the value is being changed or set to the current value. https://github.com/whatwg/dom/issues/520#issuecomment-336574796

Related

Fire an event when a liste get new item

i have an empty list :
<ul id="select2-choices"></ul>
this list gets elements added to it using a ui ,so i get this :
<ul id="select2-choices">
<li>item1</li>
<li>item2</li>
</ul>
i want to fire an event , in order to call a function when that list get a new item :
$("#select2-choices").on("Thevent", function (e)){
self.SetDefaultTeam(e);
});
how to do that ?
You can use mutation observers as shown below. The code is commented. I created a button to mimic the addition of new items, but the mutation observer is the function that recognises that change in the DOM tree.
N.B. If you have access to the code that is adding the new li then it would be better to trigger your function from there.
Let me know if you were hoping for something else.
// Create a button to add options to mimic your functionality
$("#add-li").click(function() {
$("ul#select2-choices").append("<li>New</li>");
});
// Create mutation observer
var observer = new MutationObserver(function(mutations) {
// Something has changed in the #select2-choices
console.log("Change noticed in childList");
});
// Just look out for childList changes
var config = {
attributes: false,
childList: true,
characterData: false
};
// Select target
var target = document.querySelector('#select2-choices');
// Launch observer with above configuration
observer.observe(target, config);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul id="select2-choices">
</ul>
<button id="add-li">Add option</button>

Javascript force recalculate value of object property

I would like to re-use an "object", however, one of the object's properties values should be recalculated every time the object is accessed.
In my code I have a library which can basically make a list of card views from a data url. This list of card views is added to a page. There are two types of lists: Active Buildings list and Archived Buildings list. Switching between these two lists is done by pressing a button, which triggers the "rerender" function of the repeater shown below.
Archived Buildings should not be clickable. I pass along some configuration options to my library where I handle the relevant parts. However, because of the way I invoke the card view library, the value of the enableClick configuration option is always set to what the state was like at the load of the page.
Example of how the code looks:
$(function () {
var buildingsContainer = $('#buildings');
buildingsContainer.repeater({
url: function () {
var activeFilter = buildingFilter.find('.btn-primary').data('status');
return '/Building/All?status=' + activeFilter;
},
renderItem: cardTemplates(buildingsContainer).building({
activateBuildingUrl: '#(Url.Action("ActivateBuilding", "Building"))/{Id}',
editUrl: '#(Url.Action("Edit", "Building"))/{Id}',
deleteBuildingUrl: '#(Url.Action("SoftDeleteBuilding", "Building"))/{Id}',
enableClick: getActiveFilter() === 'Active'
})
})
});
function getActiveFilter() {
var buildingFilter = $('#buildingFilter');
return buildingFilter.find('.btn-primary').data('status');
}
No matter what the currently pressed button is, enableClick is always set to what it was when the page opened.
To better demonstrate my problem, I have created a JSFiddle:
https://jsfiddle.net/e3xnbxov/
In this JSFiddle, you see I have a options object with a value property. In the button's click listeners I print this value. However, it always remains on Active, even though I switch between Active and Archived. How can I make it so the value of the property is recalculated?
I think you have 2 options here.
1) Set the property as a function, and evaluate it:
$(function() {
var options = {
value: ()=>$('#container').find('.btn-primary').data('status')
};
var container = $('#container');
container.find('.btn').click(function() {
container.find('.btn').removeClass('btn-primary').addClass('btn-default');
$(this).addClass('btn-primary');
console.log(options.value());
});
});
jsfiddle: https://jsfiddle.net/mw8kuq6L/
2) Just use "this" to directly access the data value you want to check:
$(function() {
var container = $('#container');
container.find('.btn').click(function() {
container.find('.btn').removeClass('btn-primary').addClass('btn-default');
$(this).addClass('btn-primary');
console.log($(this).data('status'));
});
});
The problem is that the object (options) is created once, and the property is set once.
At the moment that the creation (and property setting) occurs, the 'active' button matches the jQuery selector ($('#container').find('.btn-primary')).
Javascript, like many languages, uses references. When you set the object's property, it received a reference to the result of the jQuery selector, not the selector (as a method) itself.
You could change it to behave more as you're expecting by creating a method on your object:
$(function() {
var options = {
value: function () {
return $('#container').find('.btn-primary').data('status')
}
};
var container = $('#container');
container.find('.btn').click(function() {
container.find('.btn').removeClass('btn-primary').addClass('btn-default');
$(this).addClass('btn-primary');
console.log(options.value());
});
});
Thus your options object now has a callable method which dynamically returns what you were expecting.
Otherwise I'd update the property when the selected button changes:
$(function() {
var options = {
value: $('#container').find('.btn-primary').data('status')
};
var container = $('#container');
container.find('.btn').click(function() {
container.find('.btn').removeClass('btn-primary').addClass('btn-default');
$(this).addClass('btn-primary');
options.value = $('#container').find('.btn-primary').data('status');
console.log(options.value);
});
});
This is just meant to be an addition to lpg's answer.
Another way would be to use a getter function which behaves like lpg's value function but can be used like a normal property:
$(function() {
var options = {
// define a getter for the property 'value'
get value () {
return $('#container').find('.btn-primary').data('status');
}
};
var container = $('#container');
container.find('.btn').click(function() {
container.find('.btn').removeClass('btn-primary').addClass('btn-default');
$(this).addClass('btn-primary');
console.log(options.value); // use the property for the property 'value'
});
});
<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet"/>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="http://netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<div id="container">
<button class="btn btn-sm btn-primary" data-status="Active">Active</button>
<button class="btn btn-sm btn-default" data-status="Archived">Archived</button>
</div>

Custom control - data binding not working

I am currently trying to extend a sap.m.Input field to be able to style and extend the label placement.
The rendering works fine, but somehow the data-binding gets lost in the process and i am unsure why that is. This is my control:
sap.ui.define([
'sap/m/Input',
], function(Input) {
'use strict';
return Input.extend('one.sj.control.BhTextInput', {
metadata: {
properties: {
label: {
type: 'string',
},
},
aggregations: {
icon: {
type: 'sap.ui.core.Icon',
multiple: false,
visibility: 'public',
},
},
},
renderer: function(oRM, oControl) {
oRM.write('<div class="formControl">');
oRM.write('<input placeholder="'+oControl.getPlaceholder()+'"');
oRM.write('type="'+oControl.getType()+'"');
oRM.write('value="'+oControl.getValue()+'"');
oRM.writeClasses();
oRM.writeControlData(oControl);
oRM.write('/>');
oRM.write('<label class="inputLabel" for="'+oControl.getId()+'"');
oRM.write('>');
oRM.renderControl(oControl.getIcon());
oRM.write('<span class="inputLabelContent">');
oRM.write(oControl.getLabel());
oRM.write('</span>');
oRM.write('</label>');
oRM.write('</div>');
},
});
});
As you can see it is quite simple.
This is how i use it:
<sj:BhTextInput
id="username" class="input textInput"
placeholder="{i18n>HINT_USERNAME}" value="{creds>/username}"
type="Text">
<sj:icon>
<core:Icon src="sap-icon://email" class="inputIcon" />
</sj:icon>
</sj:BhTextInput>
I confirmed that is not a problem of my model, as it works fine when i replace the manual <input/> construction in the renderer method above with:
sap.m.InputRenderer.render(oRM, oControl);
Can you spot anything wrong? Thanks!
EDIT: To clarify a bit on what i mean by "data-binding gets lost". I am only getting an empty string when accessing the value bound to the Input field inside my controller like this: getModel('creds').getProperty('/username');. This does work when replacing the manual construction as written above.
I'm not sure if this is what causing your problem but I believe oRM.write doesn't add spaces to your rendered HTML. It is better to use oRM.writeAttribute for writing attributes. Also class should be added using oRM.addClass.
Ok. There are couple of changes that are required to get it working.
Note 1: The InputBase API ( parent of sap.m.Input) needs your <input> tag to have an id containing "inner" to fetch its value properly. This is from INputBase API:
/**
* Returns the DOM value respect to maxLength
* When parameter is set chops the given parameter
*
* TODO: write two different functions for two different behaviour
*/
InputBase.prototype._getInputValue = function(sValue) {
sValue = (sValue === undefined) ? this.$("inner").val() || "" : sValue.toString();
if (this.getMaxLength && this.getMaxLength() > 0) {
sValue = sValue.substring(0, this.getMaxLength());
}
return sValue;
};
So, on every change, it reads the DOM value and then updates the control metadata.
/**
* Handles the change event.
*
* #protected
* #param {object} oEvent
* #returns {true|undefined} true when change event is fired
*/
InputBase.prototype.onChange = function(oEvent) {
// check the control is editable or not
if (!this.getEditable() || !this.getEnabled()) {
return;
}
// get the dom value respect to max length
var sValue = this._getInputValue();
// compare with the old known value
if (sValue !== this._lastValue) {
// save the value on change
this.setValue(sValue);
if (oEvent) {
//IE10+ fires Input event when Non-ASCII characters are used. As this is a real change
// event shouldn't be ignored.
this._bIgnoreNextInputEventNonASCII = false;
}
// get the value back maybe formatted
sValue = this.getValue();
// remember the last value on change
this._lastValue = sValue;
// fire change event
this.fireChangeEvent(sValue);
// inform change detection
return true;
} else {
// same value as before --> ignore Dom update
this._bCheckDomValue = false;
}
};
So, I changed your renderer method of the control like this:
renderer: function(oRM, oControl) {
oRM.write('<div class=formControl');
oRM.writeClasses();
oRM.writeControlData(oControl); // let div handle control metadata such as id.
oRM.write(">")
oRM.write('<input placeholder="'+oControl.getPlaceholder()+'"');
oRM.write('id="'+oControl.getId()+'-inner"'); // set id with 'inner'
// oRM.write('type="'+oControl.getType()+'"'); dont know why type is throwing error s=, so had to comment it.
oRM.write('value="'+oControl.getMyValue()+'"');
// oRM.writeClasses();
// oRM.writeControlData(oControl);
oRM.write('/>');
oRM.write('<label class="inputLabel" for="'+oControl.getId()+'"');
oRM.write('>');
oRM.renderControl(oControl.getIcon());
oRM.write('<span class="inputLabelContent">');
oRM.write(oControl.getLabel());
oRM.write('</span>');
oRM.write('</label>');
oRM.write('</div>');
}
Let me know if this works for you. :)

Mutation Observer or DOMNodeInserted

I have a script where I´ve use on the first slide of Adobe Captivate, to automate the task ok creating, courses, the script create the UX, navigation elements, intro/end motions, a game, insert spritesheets with characters, etc...
I´ve used DOMNodeInserted until know to check the modifications on the slide, when the user go to the next slide, the elements are added to the DOM and the page content is changed I´ve used this timer until now to call the function:
function detectChange(){
var slideName = document.getElementById('div_Slide')
slideName.addEventListener("DOMNodeInserted", detectChange, false);
updateSlideElements();
setTimeout(updateSlideElements, 100);
}
So I´m trying to use mutation Observer now:
var observer = new MutationObserver(function(mutations, observer) {
updateSlideElements();
});
observer.observe(document.getElementById('div_Slide').firstChild, {
attributes: true,
childList:true
});
But this is what´s happening, before with setTimeout I could reach the following element:
var motionText2 = document.querySelectorAll('div[id*=motion][class=cp-accessibility]');
This element is the firstChild of:
And the element can be found:
But now with mutationObserver the console returns empty:
I´ve just use a setTimeout inside the observer and watch the parent container not the firstChild:
var observer = new MutationObserver(function(mutations, observer) {
setTimeout(updateSlideElements, 100);
});
observer.observe(document.getElementById('div_Slide'), {
attributes: true,
childList:true
//subtree:true
});

Update value of input type time (rerender) and focus on element again with React

In the spec for my app it says (developerified translation): When tabbing to a time element, it should update with the current time before you can change it.
So I have:
<input type="time" ref="myTimeEl" onFocus={this.handleTimeFocus.bind(null, 'myTimeEl')} name="myTimeEl" value={this.model.myTimeEl} id="myTimeEl" onChange={this.changes} />
Also relevant
changes(evt) {
let ch = {};
ch[evt.target.name] = evt.target.value;
this.model.set(ch);
},
handleTimeFocus(elName, event)
{
if (this.model[elName].length === 0) {
let set = {};
set[elName] = moment().format('HH:mm');
this.model.set(set);
}
},
The component will update when the model changes. This works well, except that the input loses focus when tabbing to it (because it gets rerendered).
Please note, if I would use an input type="text" this works out of the box. However I MUST use type="time".
So far I have tried a number of tricks trying to focus back on the element after the re-render but nothing seems to work.
I'm on react 0.14.6
Please help.
For this to work, you would need to:
Add a focusedElement parameter to the components state
In getInitialState(): set this parameter to null
In handleTimeFocus(): set focusElement to 'timeElem` or similar
Add a componentDidUpdate() lifecycle method, where you check if state has focusedElement set, and if so, focus the element - by applying a standard javascript focus() command.
That way, whenever your component updates (this is not needed in initial render), react checks if the element needs focus (by checking state), and if so, gives the element focus.
A solution for savages, but I would rather not
handleTimeFocus(elName, event)
{
if (this.model[elName].length === 0) {
let set = {};
set[elName] = moment().format('HH:mm');
this.model.set(set);
this.forceUpdate(function(){
event.target.select();
});
}
},
try using autoFocus attrribute.
follow the first 3 steps mention by wintvelt.
then in render function check if the element was focused, based on that set the autoFocus attribute to true or false.
example:
render(){
var isTimeFocused = this.state.focusedElement === 'timeElem' ? true : false;
return(
<input type="time" ref="myTimeEl" onFocus={this.handleTimeFocus.bind(null, 'myTimeEl')} name="myTimeEl" value={this.model.myTimeEl} id="myTimeEl" onChange={this.changes} autoFocus={isTimeFocused} />
);
}

Categories