Create or update script of given HTML - javascript

The code is getting an HTML file and configObject and according to this config object need to modify this htmlFile content(the code is working)
This is the input:
please notice that that in the firstobj of the array (there is scenario which the next attribute is not provided which say to put the new script at the bottom) the next said put the script tag after script that have ID "ui-boot"
var extendedHtmlObject = [{
type: 'script',
action: 'new',
content: 'console.log(‘hello world’);',
next: "ui-boot"
}, {
type: 'script',
id: "ui-boot",
action: 'upd',
innerElem: [{
type: 'attr',
id: 'data--ui-comersion',
content: '1.17'
}, {
type: 'attr',
id: 'src',
content: '/test/test2/-co.js'
}]
}
This is the main function:
getExtend: function(htmlContent, extendedHtmlObject) {
var self = this;
if (extendedHtmlObject) {
extendedHtmlObject.forEach(function(configs) {
switch (configs.type) {
case 'script':
htmlContent = self._handleScriptElement(htmlContent, configs);
break;
case 'head':
break;
}
});
}
return htmlContent;
},
This method determines if I need to create a new script or update existing script attributes according to the input object
_handleScriptElement: function(htmlFilecontent, configEntry) {
var oExtendedHTML = htmlFilecontent;
switch (configEntry.action) {
case 'new':
oExtendedHTML = this._createNewScript(htmlFilecontent, configEntry);
break;
case 'upd':
var sParsedHtml = this._htmlParser(oExtendedHTML);
oExtendedHTML = this._updateScript(oExtendedHTML, configEntry, sParsedHtml);
oExtendedHTML = this._convertHtmlBack(oExtendedHTML);
break;
}
return oExtendedHTML;
},
This is the method for creating new script with two option
1. the first fork need to parse the html
2. the second for doesn't.
_createNewScript: function(htmlFilecontent, configEn) {
var sScriptContent = this._createScript(configEntry.content);
if (configEn.next != null) {
var sParsedHtml = this._htmlParser(htmlFilecon);
$(sScriptContent).insertAfter(sParsedHtml.find('#' + configEn.next));
htmlFilecontent = this._convertHtmlBack(sParsedHtml);
} else {
//when the script is at the end of file
var iHeadEndTagPos = htmlFilecon.search("(| )* )*head(|*>");
htmlFilecon = htmlFilecon.substr(0, iHeadEndTagPos) + sNewScript + htmlFilecon.substr(iHeadEndTagPos);
}
return htmlFilecon;
},
This code is redundant and not efficient(I'm fairly new to JS), could I maybe improve it with JS prototype?
I want to do the parse just once in the start and the parseBack at the end(of looping the input object) but the problem is that in the createNewScript the second fork doesn't need to use the parser...
The code inside module of requireJS
update
To make it more clear, The external API have two input and one output
HTML file content
config object which determine how to update the HTML, for example to create new script (as the first object in the array
extendedHtmlObject ) or update existing script content such as
attributes values)
the output should be the extended HTML with all the modification
**update 2 **
If I can provide additional data to make it more clear please let me know what.

Maybe have a look at some of the multitude of templating systems out there.
Alternatively, simplify your html creation. If you use innerHTML to create DOM nodes, have the HTML file content just be a long string containing the full markup with placeholders for all values/classnames/etc.
eg: <script src="{{src}" id="{{id}}"></script> and then just replace all the placeholders with the values. Then you can just update the innerHTML of the target with this string.
If you use DOM node creation, document.createElement(), create all those template nodes in advance (documentFragments are really handy here). Then have one function that will check the 'extendedHtmlObject' settings and select either the node already on the page in case of update, or select your premade nodeList. Once you have the html structure as nodes, hand it off to a different function that will update all the nodes inside the structure with the new values in the 'extendedHtmlObject'. You can then append the nodes to their target.
In both cases you can juist throw the back and forth html parsing and branching away.
pseudocode:
(function myModule() {
var templates = {},
createTemplates = function() {
// Create all templates you'll use and save them.
// If you create nodes, also include a mapping from each node to its respective value
// eg: children[0] = 'label', children[1] = 'value'
},
updateTemplate = function( template, values ) {
// INNERHTML: for each placeholder 'valueName' in string template, replace placeholder by values[valueName]
// NODE CREATION: use the node mapping in the template to select each node in the template and replace its current value with values[valueName]
return template;
},
getTemplate = function( type, reference ) {
return (type === 'new') ? templates[reference] : $('#' + reference);
},
update = function( config ) {
var html = getTemplate(config.action, (config.next || config.id)),
updated = updateTemplate(html, config);
if (config.next) $('#' + config.next).insertAfter(html);
else $('#' + config.id).innerHTML = html /* OR */ $('#' + config.id).appendChild(html);
},
init = function() {
createTemplates();
};
}());
TLDR: Create new or grab existing HTML first, then update either the string or the nodes, then determine where it has to go. Make everything as general as possible.
The html creation function should be able to create any html element/update any template, not just script nodes.
The update function should not care if the html its updating existed before or is newly generated.
The function that puts the html back into the website should not be inside the creation functions.
This way you'll probably at first end up with just one big switch statement that will call the same functions with different parameters, which can easily be replaced by adding another config setting to the 'extendedHtmlObject'.
UPDATE:
I've created an example of a usable structure. It uses the DOM methods (createElement, etc) to create a new document containing the updated input string. You can change it to better match your specific input/output as much as you want. It's more of an example of how you can create anything you want with just a few general functions and alot of config settings.
This doesn't use the templetes I spoke before of though, since that would take longer to make. Just have a look at handlebars.js or mustache.js if you want templating.
But it better matches the structure you had yourself, so I hope it's easier for you then to pick the parts you like.
var originalHTML = '<script id="ui-boot" src="path/to/script/scriptname.js"></script>',
imports = {},
// IMPORTANT NOTE:
// Using a string as the body of a script element might use eval(), which should be avoided at all costs.
// Better would be to replace the 'content' config setting by a 'src' attribute and ahve the content in a seperate file you can link to.
configAry = [
{ // script-new
type: 'script',
action: 'new',
content: 'console.log(‘hello world’);',
next: "ui-boot"
},
{ // script-upd
type: 'script',
id: "ui-boot",
action: 'upd',
innerElem: [
{
type: 'attr',
id: 'data--ui-comersion',
content: '1.17'
},
{
type: 'attr',
id: 'src',
content: '/test/test2/-co.js'
}
]
}
],
// Extra helpers to make the code smaller. Replace by JQuery functions if needed.
DOM = {
'create' : function create( name ) {
return document.createElement(name);
},
// Create a new document and use the string as the body of this new document.
// This can be replaced by other functions of document.implementation if the string contains more than just the content of the document body.
'createDoc' : function createDoc( str ) {
var doc = document.implementation.createHTMLDocument('myTitle');
doc.body.innerHTML = str;
return doc;
},
'insertAfter' : function insertAfter(node, target, content ) {
target = content.querySelector(target);
if (target.nextSibling) target.nextSibling.insertBefore(node);
else target.parentNode.appendChild(node);
return content;
},
'replace' : function replace( node, target, content ) {
target = content.querySelector(target);
target.parentNode.replaceChild(node, target);
return content;
},
'update' : function update( node, textContent, attributes ) {
if (textContent) node.textContent = textContent;
return (attributes) ?
Object.keys(attributes).reduce(function( node, attr ) {
node.setAttribute(attr, attributes[attr]);
return node;
}, node) :
node;
}
},
// The actual module
moduleHTMLExtender = (function( imports ) {
var createExtension = function createExtension( extension, content ) {
switch (extension.action) {
case 'new' :
return {
'method' : 'insertAfter',
'node' : DOM.update(DOM.create(extension.type), extension.content),
'target' : '#' + extension.next
};
break;
case 'upd' :
return {
'method' : 'replace',
'node' : DOM.update(content.querySelector('#' + extension.id).cloneNode(), null, extension.innerElem.reduce(function( map, config ) {
if (config.type === 'attr') map[config.id] = config.content;
return map;
}, {})),
'target' : '#' + extension.id
}
break;
default:
return false;
break;
}
},
addExtensions = function addExtensions( content, extensions ) {
return extensions.reduce(function( content, extension ) {
return DOM[extension.method](extension.node, extension.target, content);
}, content);
},
// Returns new document as an [object Document]
extendContent = function extendContent( content, extensions ) {
var doc = DOM.createDoc(content),
toExtend = (extensions) ?
extensions.map(function( extension ) {
return createExtension(extension, doc);
}) :
null;
var res = null;
if (toExtend) return addExtensions(doc, toExtend);
else return doc;
};
// Export public interface. Replace this by your require.js code or similar.
return {
'extendContent' : extendContent
};
}( imports )),
extendedHTMLDoc = moduleHTMLExtender.extendContent(originalHTML, configAry);
console.log(extendedHTMLDoc.documentElement.outerHTML);
// output = '<html><head><title>myTitle</title></head><body><script id="ui-boot" src="/test/test2/-co.js" data--ui-comersion="1.17"></script><script>console.log(hello world);</script></body></html>';

Related

How to render links generated dynamically from JSON?

To display links in my rendered JSON, I want them as value in the name/value pair.
The function below takes a valid JSON as a parameter and should return JSON with their values converted to anchor tags.
The input JSON is
{"#context":"/api/contexts/EntryPoint.jsonld","#id":"/api","#type":"EntryPoint","AnomalyCollection":"/api/AnomalyCollection","CommandCollection":"/api/CommandCollection","ControllerLogCollection":"/api/ControllerLogCollection","DroneCollection":"/api/DroneCollection","DroneLogCollection":"/api/DroneLogCollection","HttpApiLogCollection":"/api/HttpApiLogCollection","Location":"/api/Location","MessageCollection":"/api/MessageCollection","dsCollection":"/api/dsCollection"}
{"k": "v"} should get changed to {"k": v}
I understand that the changed value would not be a valid JSON, then what is the way out?
function makeEditable(data) {
for (var property in data) {
if (data.hasOwnProperty(property)) {
var tag = $('<a href=#>' + data[property] + '</a>');
data[property] = tag;
}
}
return data;
}
The function above instead of giving links, gives the below render.
{
#context : {
0 : "http://localhost:5000/#"
},
#id : {
0 : "http://localhost:5000/#"
},
#type : {
0 : "http://localhost:5000/#"
}
}
What I want is a render similar to this
I would highly suggest to not manipulate your data (JSON) and do the DOM implementation on your HTML or template that you're using.
In case you insist on following your approach: don't wrapper your anchor tag by jQuery just write it as a string. something like <a href=#>${data[property]}</a>
You're meant to make it a string, not a anchor, otherwise (because jQuery) it'll make data[property] equal to the URL you get directed to when you click on the anchor, because this:
<a href=#>v</a>
Was turned into this:
http://localhost:5000/#
Which is where you'd get directed to if you'd click on the link. Try using a simple template literal instead (it's also smaller and simpler than concatenating strings):
function makeEditable(data) {
for (var property in data) {
if (data.hasOwnProperty(property)) {
var tag = `${data]property]}`;
data[property] = tag;
}
}
return data;
}
I realised that modifying JSON would not work and instead decided to append to the div directly.
This was the function after modification:
function makeEditable(data) {
var fdata={};
for (var property in data) {
if (data.hasOwnProperty(property)) {
$("#vocab-json").append('<b>'+property+'</b>'+''+data[property]+'<br>');
}
}
return fdata;
}

What's a good way to dynamically render DOM elements with plain old JS?

The challenge I'm facing is to build a single-page app with plain old Javascript, no libraries or frameworks allowed. While creating dynamic DOM elements in React and Angular is fairly straightforward, the vanilla JS solution I've come up with seems clunky. I'm wondering if there's a particularly more concise or efficient way of building dynamically rendered DOM elements?
The function below takes in an array received from a GET request and renders a div for each item, passing in the value (much like you would map over results in React and render child elements).
function loadResults(array) {
array.forEach(videoObject => {
let videoData = videoObject.snippet;
let video = {
title : videoData.title,
img : videoData.thumbnails.default.url,
description : videoData.description
};
let div = document.createElement("DIV");
let img = document.createElement("IMG");
img.src = video.img;
let h4 = document.createElement("h4");
let title = document.createTextNode(video.title);
h4.appendChild(title);
let p = document.createElement("p");
let desc = document.createTextNode(video.description);
p.appendChild(desc);
div.appendChild(img);
div.appendChild(h4);
div.appendChild(p);
document.getElementById('results')
.appendChild(div);
});
}
This feels unnecessarily clunky but I haven't yet found an easier way to do this.
Thanks in advance!
Note: Everything I say here is on the proof of concept level and nothing more. It does not handle errors or exceptional cases, nor was
it tested in production. Use at your own discretion.
A good way to go about would be to create a function that creates elements for you. Something like this:
const crEl = (tagName, attributes = {}, text) => {
const el = document.createElement(tagName);
Object.assign(el, attributes);
if (text) { el.appendChild(document.createTextNode(text)); }
return el;
};
Then you can use it like so:
results
.map(item => crEl(div, whateverAttributes, item.text))
.forEach(el => someParentElement.appendChild(el));
Another cool proof of concept I've seen is the use of ES6 Proxies as a sort of templating engine.
const t = new Proxy({}, {
get(target, property, receiver) {
return (children, attrs) => {
const el = document.createElement(property);
for (let attr in attrs) {
el.setAttribute(attr, attrs[attr]);
}
for (let child of(Array.isArray(children) ? children : [children])) {
el.appendChild(typeof child === "string" ? document.createTextNode(child) : child);
}
return el;
}
}
})
const el = t.div([
t.span(
["Hello ", t.b("world!")], {
style: "background: red;"
}
)
])
document.body.appendChild(el);
The Proxy traps the get on the target object (which is empty), and renders an element with the name of the called method. This leads to the really cool syntax you see as of const el =.
If you can use ES6, template strings is another idea ->
var vids = [
{
snippet: {
description: 'hello',
title: 'test',
img: '#',
thumbnails: { default: {url: 'http://placehold.it/64x64'} }
}
}
];
function loadResults(array) {
array.forEach(videoObject => {
let videoData = videoObject.snippet;
let video = {
title : videoData.title,
img : videoData.thumbnails.default.url,
description : videoData.description
};
document.getElementById('results').innerHTML = `
<div>
<img src="${video.img}"/>
<h4>${video.title}</h4>
<p>${video.description}</p>
</div>
`;
});
}
loadResults(vids);
<div id="results"></div>
To my mind, if you are not using any templating engine, you want to keep as much control over how you put together your elements as possible. So sound approach would be to abstract common tasks and allow chaining the calls to avoid extra variables. So I'd go with something like this (not very fancy):
function CE(el, target){
let ne = document.createElement(el);
if( target )
target.appendChild(ne);
return ne;
}
function CT(content, target){
let ne = document.createTextNode(content);
target.appendChild(ne);
return ne;
}
function loadResults(array) {
var results = document.getElementById('results');
array.forEach(videoObject => {
let videoData = videoObject.snippet;
let video = {
title : videoData.title,
img : videoData.thumbnails.default.url,
description : videoData.description
};
let div = CE('div');
let img = CE("IMG", div);
img.src = video.img;
CT(video.title, CE("H4", div));
CT(video.description, CE("p", div););
results.appendChild(div);
});
}
What you gain is that you still have a fine control on how your elements are assembled, what is linked to what. But your code is easier to follow.
I created a library for the same purpose, you can find it there https://www.npmjs.com/package/object-to-html-renderer
I understand you cannot use any lib in your project, but as this one is very short (and has no dependencies), you could just copy and adapt the code which is just one small file:
(see https://gitlab.com/kuadrado-software/object-to-html-renderer/-/blob/master/index.js )
// object-to-html-renderer/index.js
module.exports = {
register_key: "objectToHtmlRender",
/**
* Register "this" as a window scope accessible variable named by the given key, or default.
* #param {String} key
*/
register(key) {
const register_key = key || this.register_key;
window[register_key] = this;
},
/**
* This must be called before any other method in order to initialize the lib.
* It provides the root of the rendering cycle as a Javascript object.
* #param {Object} renderCycleRoot A JS component with a render method.
*/
setRenderCycleRoot(renderCycleRoot) {
this.renderCycleRoot = renderCycleRoot;
},
event_name: "objtohtml-render-cycle",
/**
* Set a custom event name for the event that is trigger on render cycle.
* Default is "objtohtml-render-cycle".
* #param {String} evt_name
*/
setEventName(evt_name) {
this.event_name = evt_name;
},
/**
* This is the core agorithm that read an javascript Object and convert it into an HTML element.
* #param {Object} obj The object representing the html element must be formatted like:
* {
* tag: String // The name of the html tag, Any valid html tag should work. div, section, br, ul, li...
* xmlns: String // This can replace the tag key if the element is an element with a namespace URI, for example an <svg> tag.
* See https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS for more information
* style_rules: Object // a object providing css attributes. The attributes names must be in JS syntax,
* like maxHeight: "500px", backgrouncColor: "#ff2d56", margin: 0, etc.
* contents: Array or String // This reprensents the contents that will be nested in the created html element.
* <div>{contents}</div>
* The contents can be an array of other objects reprenting elements (with tag, contents, etc)
* or it can be a simple string.
* // All other attributes will be parsed as html attributes. They can be anything like onclick, href, onchange, title...
* // or they can also define custom html5 attributes, like data, my_custom_attr or anything.
* }
* #returns {HTMLElement} The output html node.
*/
objectToHtml(obj) {
if (!obj) return document.createElement("span"); // in case of invalid input, don't block the whole process.
const objectToHtml = this.objectToHtml.bind(this);
const { tag, xmlns } = obj;
const node = xmlns !== undefined ? document.createElementNS(xmlns, tag) : document.createElement(tag);
const excludeKeys = ["tag", "contents", "style_rules", "state", "xmlns"];
Object.keys(obj)
.filter(attr => !excludeKeys.includes(attr))
.forEach(attr => {
switch (attr) {
case "class":
node.classList.add(...obj[attr].split(" ").filter(s => s !== ""));
break;
case "on_render":
if (!obj.id) {
node.id = `${btoa(JSON.stringify(obj).slice(0, 127)).replace(/\=/g, '')}${window.performance.now()}`;
}
if (typeof obj.on_render !== "function") {
console.error("The on_render attribute must be a function")
} else {
this.attach_on_render_callback(node, obj.on_render);
}
break;
default:
if (xmlns !== undefined) {
node.setAttributeNS(null, attr, obj[attr])
} else {
node[attr] = obj[attr];
}
}
});
if (obj.contents && typeof obj.contents === "string") {
node.innerHTML = obj.contents;
} else {
obj.contents &&
obj.contents.length > 0 &&
obj.contents.forEach(el => {
switch (typeof el) {
case "string":
node.innerHTML = el;
break;
case "object":
if (xmlns !== undefined) {
el = Object.assign(el, { xmlns })
}
node.appendChild(objectToHtml(el));
break;
}
});
}
if (obj.style_rules) {
Object.keys(obj.style_rules).forEach(rule => {
node.style[rule] = obj.style_rules[rule];
});
}
return node;
},
on_render_callbacks: [],
/**
* This is called if the on_render attribute of a component is set.
* #param {HTMLElement} node The created html element
* #param {Function} callback The callback defined in the js component to render
*/
attach_on_render_callback(node, callback) {
const callback_handler = {
callback: e => {
if (e.detail.outputNode === node || e.detail.outputNode.querySelector(`#${node.id}`)) {
callback(node);
const handler_index = this.on_render_callbacks.indexOf((this.on_render_callbacks.find(cb => cb.node === node)));
if (handler_index === -1) {
console.warn("A callback was registered for node with id " + node.id + " but callback handler is undefined.")
} else {
window.removeEventListener(this.event_name, this.on_render_callbacks[handler_index].callback)
this.on_render_callbacks.splice(handler_index, 1);
}
}
},
node,
};
const len = this.on_render_callbacks.push(callback_handler);
window.addEventListener(this.event_name, this.on_render_callbacks[len - 1].callback);
},
/**
* If a main element exists in the html document, it will be used as rendering root.
* If not, it will be created and inserted.
*/
renderCycle: function () {
const main_elmt = document.getElementsByTagName("main")[0] || (function () {
const created_main = document.createElement("main");
document.body.appendChild(created_main);
return created_main;
})();
this.subRender(this.renderCycleRoot.render(), main_elmt, { mode: "replace" });
},
/**
* This method behaves like the renderCycle() method, but rather that starting the rendering cycle from the root component,
* it can start from any component of the tree. The root component must be given as the first argument, the second argument be
* be a valid html element in the dom and will be used as the insertion target.
* #param {Object} object An object providing a render method returning an object representation of the html to insert
* #param {HTMLElement} htmlNode The htlm element to update
* #param {Object} options can be used the define the insertion mode, default is set to "append" and can be set to "override",
* "insert-before" (must be defined along with an insertIndex key (integer)),
* "adjacent" (must be defined along with an insertLocation key (String)), "replace" or "remove".
* In case of "remove", the first argument "object" is not used and can be set to null, undefined or {}...
*/
subRender(object, htmlNode, options = { mode: "append" }) {
let outputNode = null;
const get_insert = () => {
outputNode = this.objectToHtml(object);
return outputNode;
};
switch (options.mode) {
case "append":
htmlNode.appendChild(get_insert());
break;
case "override":
htmlNode.innerHTML = "";
htmlNode.appendChild(get_insert());
break;
case "insert-before":
htmlNode.insertBefore(get_insert(), htmlNode.childNodes[options.insertIndex]);
break;
case "adjacent":
/**
* options.insertLocation must be one of:
*
* afterbegin
* afterend
* beforebegin
* beforeend
*/
htmlNode.insertAdjacentElement(options.insertLocation, get_insert());
break;
case "replace":
htmlNode.parentNode.replaceChild(get_insert(), htmlNode);
break;
case "remove":
htmlNode.remove();
break;
}
const evt_name = this.event_name;
const event = new CustomEvent(evt_name, {
detail: {
inputObject: object,
outputNode,
insertOptions: options,
targetNode: htmlNode,
}
});
window.dispatchEvent(event);
},
};
This is the entire lib, and it can be used like this (there is more features than this but at least for the basic usage) :
// EXAMPLE - refresh a list after fetch data
const renderer = require("object-to-html-renderer");
class DataListComponent {
constructor() {
this.render_data = [];
this.list_id = "my-data-list";
}
async fetchData() {
const fetchData = await (await fetch(`some/json/data/url`)).json();
return fetchData;
}
renderDataItem(item) {
return {
tag: "div",
contents: [
// Whatever you want to do to render your data item...
],
};
}
renderDataList() {
return {
tag: "ul",
id: this.list_id,
contents: this.render_data.map(data_item => {
return {
tag: "li",
contents: [this.renderDataItem(data_item)],
};
}),
};
}
render() {
return {
tag: "div",
contents: [
{
tag: "button",
contents: "fetch some data !",
onclick: async () => {
const data = await this.fetchData();
this.render_data = data;
renderer.subRender(
this.renderDataList(),
document.getElementById(this.list_id),
{ mode: "replace" },
);
},
},
this.renderDataList(),
],
};
}
}
class RootComponent {
render() {
return {
tag: "main", // the tag for the root component must be <main>
contents: [new DataListComponent().render()],
};
}
}
renderer.setRenderCycleRoot(new RootComponent());
renderer.renderCycle();
I built entire web applications just with this and it works pretty fine. I find it a good alternative to React Vue etc. (Of course it's much simpler and does not all that React does..)
Maybe it could be useful to you or someone else.
Two observations about this code review question:
Refactoring by
adding a helper function to create an element with optional text content, and
removing the video object abstraction since it copies two out of three properties under the same name,
produces readable but vanilla type Javascript:
function loadResults(array) {
function create (type,text) {
let element = document.createElement(type);
if( text)
element.appendChild( document.createTextNode( text));
return element;
}
array.forEach(videoObject => {
let vid = videoObject.snippet;
let div = create("DIV");
let img = create("IMG");
img.src = vid.thumbnails.default.url;
let h4 = create("H4", vid.title);
let p = create("P", vid.description);
div.appendChild(img);
div.appendChild(h4);
div.appendChild(p);
document.getElementById('results').appendChild(div);
});
}

jquery to json problems

I would really appreciate any help on this problem that I have spent many hours trying to solve. I have a jquery object that I created from elements on a web page:
var get_article = $('title, p, img');
When I console.log this, I get a nice, clean list of the elements and their index values. What I would like to do is add each element to a json object along with their index value and contents. I have tried this:
article = new Array();
$.each(get_article, function(){
$('title').each(function() {
var title_items = {
node: $(this).index(''),
html_element: "title",
content: $(this).text()
};
article.push(JSON.stringify(title_items));
});
$('p').each(function() {
var p_items = {
node: $(this).index(''),
html_element: "p",
content: $(this).text()
};
article.push(JSON.stringify(p_items));
});
$('img').each(function() {
var img_items = {
node: $(this).index(''),
html_element: "img",
content: $(this).attr("src")
};
article.push(JSON.stringify(img_items));
});
});
When I console.log the json object (article), all of the data is correct except the node values in the json object (article) are all over the place and they don't show the same as the index value in the original jquery object (get_article). Also, the data begins to repeat itself, so I have more nodes than was in the original jquery object (get_article). Please help!
Thanks all for your help. I was able to solve the problem using this code. Looking at your responses made me see that I needed to try another way. This is what worked for me:
for (i = 0; i < get_article.length; i++) {
if ($(get_article[i]).is('title')) {
var title_items = {
node: i,
html_element: "title",
content: $(get_article[i]).text()
};
article.push(JSON.stringify(title_items));
}
if ($(get_article[i]).is('p')) {
var p_items = {
node: i,
html_element: "p",
content: $(get_article[i]).text()
};
article.push(JSON.stringify(p_items));
}
if ($(get_article[i]).is('img')) {
var img_items = {
node: i,
html_element: "img",
content: $(get_article[i]).attr("src")
};
article.push(JSON.stringify(img_items));
}
};
You're not actually using get_article anywhere in your code.
$.each(get_article, function(){
$('title').each(function() {
var title_items = {
// ...
};
article.push(JSON.stringify(title_items));
});
// ....
});
is exactly the same as
$('title').each(function() {
var title_items = {
// ...
};
article.push(JSON.stringify(title_items));
});
It's hard to say for sure since you haven't provided enough details, but perhaps what you want is something like:
$.each(get_article, function(){
$(this).find('title').each(function() {
var title_items = {
// ...
};
article.push(JSON.stringify(title_items));
});
// ....
});
That's still not going to have the exact same effect as simply logging to the console, though. The order of the elements will be different because in the code you're first getting all the titles, then all the paragraphs, and then all the images. When you console.log the jQuery output, it's not segregating the elements by their type but simply showing them in their DOM order.
For example, if the DOM has
title 1
paragraph 1
title 2
the console.log will show
title 1
paragraph 1
title 2
while the (fixed) code will show
title 1
title 2
paragraph 1

Ember many to many relationship - add a new object if one does not already exist

I have two models with a many to many relationship to each other - the first is 'recipe', the second is 'tag':
App.ApplicationAdapter = DS.FixtureAdapter.extend({});
App.Recipe = DS.Model.extend({
title: attr('string'),
source: attr('string'),
notes: attr('string'),
isFavourite: attr('boolean', {defaultValue: false}),
tags: hasMany('tag', { async: true })
});
App.Tag = DS.Model.extend({
name: attr('string'),
recipes: hasMany('recipe', { async: true })
});
In my template, I have an input for adding a tag to a recipe.
User can add either one tag, or multiple by separating each tag with a comma.
{{input type="text" enter="AddTags" placeholder="add a tag (or two!)"}}
This calls 'AddTags' within my RecipeController:
App.RecipeController = Ember.ObjectController.extend({
actions: {
AddTags: function(tags){
var store = this.store;
var thisRecipe = this.get('model');
var thisRecipeTags = thisRecipe.get('tags');
var addedTags = tags.split(',');
for (i = 0; i < addedTags.length; i++) {
tag = store.createRecord('tag', {
name: addedTags[i]
});
thisRecipeTags.addObject(tag);
}
}
}
});
This works wonderfully - however - I would like to refactor the function so that a new tag is only created if one with the same name doesn't already exist:
AddTags: function(tags){
var store = this.store;
var thisRecipe = this.get('model');
var thisRecipeTags = thisRecipe.get('tags');
var addedTags = tags.split(',');
for (i = 0; i < addedTags.length; i++) {
// If exisiting tag where name == addedTags[i]
tag = existing tag object here
// Else create a new tag
tag = store.createRecord('tag', {
name: addedTags[i]
});
thisRecipeTags.addObject(tag);
}
}
How can I do this?
Update: I have set up a JS bin with the relevant code here: http://jsbin.com/kixexe/1/edit?js,output
Working demo here
Now for some explanation...
You are using DS.FixtureAdapter in your jsbin, which requires you to implement a queryFixtures method if you are going to query fixtures (see here) like you are doing when querying by name. A quick implementation of this could be as follows:
MyApp.TagAdapter = DS.FixtureAdapter.extend({
queryFixtures: function(fixtures, query){
var result = [];
var tag = fixtures.findBy('name', query.name);
if(tag){
result.push(tag);
}
return result;
}
});
When you query the store, you get back a promise which you can go into by using then()
If you didn't find anything - you can just move on with what you had before...
If you did find an existing tag - you can add it to other tags and then save the tags and the recipe records, since this is a many-to-many relationship.
UPDATE
Updated solution here
The for loop you had was getting confused especially when using promises, this is usually NOT what you want. Instead, Ember has a more typical/functional approach of forEach() I also trimmed the whitespace on the ends of the tags, since this is probably what you want using map(). See here to learn more about map() and forEach()
// trim whitespace
var addedTags = tags.split(',').map(function(str){
return str.replace(/^\s+|\s+$/gm,'');
});
// loop through tags
addedTags.forEach(function(tagName){
store.find('tag', { name: tagName }).then(function(results){
if(results.content.length){
thisRecipeTags.addObject(results.content[0]);
thisRecipeTags.save();
thisRecipe.save();
}
else {
var tag = store.createRecord('tag', {
name: tagName
});
thisRecipeTags.addObject(tag);
thisRecipeTags.save();
thisRecipe.save();
}
});
});
UPDATE #2
The tag was not being properly removed. Because this is a 2-way mapping, apparently you need to remove tag from the recipe (which you were doing) and ALSO recipe from the tag, which you were NOT doing. The updated code looks as follows:
removeTag: function(tag) {
var recipe = this.get('model');
recipe.get('tags').removeObject(tag);
tag.get('recipes').removeObject(recipe.get('id'));
tag.save();
recipe.save();
},
Working (hopefully for real this time ;) solution here
You can search for an existing tag as follows:
var existing = store.find('tag', { name: addedTags[i] });
So, you can have logic along the lines of:
AddTags: function(tags){
var store = this.store;
var thisRecipe = this.get('model');
var thisRecipeTags = thisRecipe.get('tags');
var addedTags = tags.split(',');
for (i = 0; i < addedTags.length; i++) {
var existing = store.find('tag', { name: addedTags[i] });
if(existing){
tag = existing
} else {
tag = store.createRecord('tag', {
name: addedTags[i]
});
}
thisRecipeTags.addObject(tag);
}
}
I can't test this at the moment so it might need some changes to account for promises, etc -- but that should get you more or less what you need.

jQuery Plugin Overwriting Parameters

This may be a very mundane question, but this is the first jQuery plugin that I have written and I'm a bit fuzzy on understanding the scope rules in JavaScript.
I'm trying to write an simple jQuery plugin that wraps around the Stack Overflow API. I'm starting off by trying to work with the Flair API.
I wanted to make the plugin as configurable as possible so that you can easily pass it the domain and user id, and generate multiple Flairs.
var superUser = $.jStackOverflow.flair({domain:"superuser.com", id: 30162, parentId:'#su-flair'});
var stackOverflow = $.jStackOverflow.flair({domain:"stackoverflow.com", id: 55954, parentId:'#so-flair'});
The problem is, when it makes the second call, it's somehow using the correct domain and id parameters, but the parentId field that it's using in the callback function to create the HTML is using the first parameter.
You can see the plugin here and the HTML here
UPDATED
DEMO: http://jsbin.com/epeti3/5
/* 16/02/2012 02.04.38 */
(function($) {
$.fn.jStackOverflow = function(options) {
var opts = $.extend({},
$.fn.jStackOverflow.defaults, options);
return this.each(function() {
$this = $(this);
var opt = $.meta ? $.extend({},
opts, $this.data()) : opts;
var result;
var id = this.id;
var flair = $.fn.jStackOverflow.flair(opt, id);
$this.html(flair);
});
};
$.fn.jStackOverflow.setApis = function(options) {
var apis = options.protocol + options.domain + options.gTLD + "/users/flair/" + options.id + "." + options.format;
if (options.makeCallbacks) {
apis += "?callback=?";
}
return apis;
};
$.fn.jStackOverflow.flair = function(options, id) {
var api = $.fn.jStackOverflow.setApis(options);
if (options.makeCallbacks) {
result = $.getJSON(api,
function(data) {
$.fn.jStackOverflow.flairCallback(data, options, id);
});
}
return result;
};
$.fn.jStackOverflow.flairCallback = function(data, options, id) {
for (var key in data) {
if (data.hasOwnProperty(key)) {
$('<div class="' + key + '"></div>').html(key + ' : ' +data[key]).appendTo('#' + id);
}
}
};
$.fn.jStackOverflow.defaults = {
protocol: 'http://',
domain: 'stackoverflow',
gTLD: '.com',
format: 'json',
makeCallbacks: true
};
})(jQuery);
use:
<div id="so-flair"></div>
$(function() {
$('#so-flair').jStackOverflow({domain:"stackoverflow", id: 91130 });
});
The problem is that you only have a single instance of your plugin. This means that the two calls to $.jStackOverflow.flair() interfere with each other as both manipulate interal data of a single object.
Check for a demo what happens if there is some delay between the two calls (click the two buttons at the bottom)
http://jsbin.com/esovu (to edit http://jsbin.com/esovu/edit
Suddenly it starts working. So you need to investigate how to write a plugin which supports multiple instances on a single page.
You can pick any "good" jQuery plugin which multiple instances support to check how to do it.
e.g. jQuery Carousel.
Check how the lines interact to allow creating multiple Carousel instances on one page (code taken from jQuery Carousel source)
$.fn.jcarousel = function(o) { //this would match your `jStackOverflow`
return this.each(function() { //for each matched element return a new carousel
new $jc(this, o);
});
};
...
var defaults = {
...
};
...
$.jcarousel = function(e, o) { //the acutal constructor
...
}
...
$jc.fn.extend({
...
});

Categories

Resources