-1

I want to create and use universal javascript function with which it should be easy to create new children for parent nodes in easy, fast and flexible way.

Look at my code:

<!DOCTYPE html>
<html>
<body>

<style>
div {
    border: 2px solid #eeeeee;
    background-color: #dff0d8;
}
ol {
    background-color: #dff0d8;
}
li {
    background-color: #eff0c8;
}
</style>

<script>
function addNewElement(newElementType,parentId) {
    var newElement = document.createElement(newElementType);
    newElement.innerHTML = 'new element';
    parentId.appendChild(newElement);
    // actually I want to use just this simple code, what makes this function universal, but it doesn't work..
    // while next commented lines work as it should
    /**
    if (parentId == "someThing"){
         someThing.appendChild(newElement);
    }
    if (parentId == "list"){
         list.appendChild(newElement);
    }
    **/
}
</script>

<p>In next example we can add new child element to this list:</p>
<ol id="list">
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ol>
<button onclick="addNewElement('li','list')">Add new li-element to this list</button>
<p>In next example we can add new child element to this div:</p>
<div id="someThing">Something here</div>
<button onclick="addNewElement('div','someThing')">Add new div-element to this div</button>
</body>
</html>

With parentId.appendChild(newElement) you doesn't get expected result, while it works as it should with specified calls that are shown in /** commented lines **/:

if (parentId == "someThing"){
     someThing.appendChild(newElement);
}
if (parentId == "list"){
     list.appendChild(newElement);
}

I'm a newbie in JS, so I don't fully understand why I can't use it parentId.appendChild(newElement) to get same results.

I guess it should be simple to make it work even without any jQuery or other libraries.

So I ask you how can I achieve this?

WebSurfer
  • 1
  • 5
  • I don't understand the question. There's already a function to spend children, and you need to have an element to append it too. It doesn't get any simpler than that. How you get the parent element isn't really relevant, and can be wrapped up in a function. I don't understand what you're trying to achieve. – Dave Newton Jul 30 '16 at 12:04
  • @DaveNewton maybe some moderator who edited my post and replaced few words could allowed it be harder to undestand for you, Please, check other edits. I hope you can understand that my provided code wasn't working except the commented parts what I wanted to improve and let it be more universal without many "if else" checks.. I wanted to let it be accessed simply with button onclick and addNewElement function that reads the passed parameters. The main reason is to simplify the process of appending new childs and get smarter and shorter code. Is it fine now? – WebSurfer Jul 30 '16 at 12:46
  • All you needed to add was to get the element by ID (or whatever mechanism you want to use to get an actual element). I don't understand how it could be any simpler than that. – Dave Newton Jul 30 '16 at 12:57
  • @DaveNewton yeah, it's very simple situation! Especially for such an experienced person as you. But I'm a newbie in JS coding and there are lots of unknown parts for me. Though, I try to make it smart from the beginning and check/compare all solutions. I didn't wanted to write getElementById every time when I wanted to find specified element and I decided to create a function that will process it for me, so it will be easy to manipulate with elements on page via onclick event. I'm going to expand the possibilities of this function, so it's not the end :) – WebSurfer Jul 30 '16 at 13:11
  • @DaveNewton and I forgot to mention that for me actually the first main question was why I can not access elements with `parentId.appendChild(newElement)` from my example, while `parentId` was properly set to real element IDs like `someThing` or `list`. Look at commented `/** **/` lines - it's a working solution from the thread start. So I needed to know why you can't access the elements like this and only use methods/functions provided for this purpose. I still do not fully understand why.. But since I know that I'd found the proper solution, it's fine. I believe I will understand it later. – WebSurfer Jul 30 '16 at 13:29
  • 1
    Because the ID of an element is not an element. Just like my driver's license number isn't me, it's one of my IDs. – Dave Newton Jul 30 '16 at 15:45
  • @DaveNewton just like I thought - it's not an object, what my function gets in parameters, it's a string. – WebSurfer Aug 01 '16 at 17:35

4 Answers4

1

First of all, you shouldn't use the same element ID more than once.

According to W3C:

The id attribute specifies a unique id for an HTML element (the value must be unique within the HTML document).

So I changed your HTML, i.e. removed IDs from buttons and passed required IDs into addNewElement function:

<p>In next example we can add new child element to this list:</p>
<ol id="list">
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ol>
<button onclick="addNewElement('li', 'list')">Add new li-element to this list</button>
<p>In next example we can add new child element to this div:</p>
<div id="someThing">Something here</div>
<button onclick="addNewElement('div', 'someThing')">Add new div-element to this div</button>

Then I updated addNewElement function:

function addNewElement(elementType, parentId) {
    let parentElement = document.getElementById(parentId);
    let newElement = document.createElement(elementType);

    newElement.innerHTML = 'new element';
    parentElement.appendChild(newElement);
}

And it works.

Please look at the jsFiddle for more details.

Alex M
  • 2,756
  • 7
  • 29
  • 35
  • Haha, you were faster :) Thanks for the answer! At first I was calling addNewElement function without "this.id" parameter, I was just experimenting with it and forgot to remove it, when I firstly post the question. I agree, same element ID should not be used in such cases to avoid problems. So, do you think, there is no any other way except "getElementById"? I was looking for something even more simplier, so I forgot about this.. ))) – WebSurfer Jul 30 '16 at 10:52
  • @WebSurfer Yeah :) I believe there's no other way in this case, because you should definitely know an element where a child should be appended to. – Alex M Jul 30 '16 at 11:02
  • I've found! There's at least one more way how to do it, though it's a little more advanced one - [link](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector) document.querySelector or [link](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll) document.querySelectorAll. Though, in some cases they could be more useful. ^_^ – WebSurfer Jul 30 '16 at 11:21
  • @WebSurfer Yeah, I know about those functions. Anyway, thanks! But usage of them in your case requires some extra changes of HTML. More over, they less performant than `getElementById`. – Alex M Jul 30 '16 at 11:31
  • I see now.. Since they are relatively new, people say that they could be buggy and more resource-intensive. Yeah, I will stick with getElementById now or in some cases will use getElementsByClass, however I will also try to use this new features in other elaborate situations, if they will be suitable. Thanks for help! – WebSurfer Jul 30 '16 at 11:40
1

While you've already accepted an answer, I felt that it might be worth offering a more extensible approach, which allows you to use unobtrusive JavaScript (rather than relying upon in-line event-handlers such as onclick) for easier maintenance.

It's also a little more extensible and customisable:

// a simple function to help derive the correct element
// from the supplied argument, 'needle':
function derive(needle) {

  // if the needle has a nodeType and if that nodeType is
  // exactly equal to 1:
  if (needle.nodeType && needle.nodeType === 1) {

    // the needle is then an element-node, and here
    // we convert that node into an Array of one:
    needle = [needle];

  // otherwise, if the needle is a string, and
  // document.getElementById() finds an element
  // with that id:
  } else if ('string' === typeof needle && document.getElementById(needle)) {
    // we find that element-node again, using the string
    // and again convert it to an Array of one:
    needle = [document.getElementById(needle)];

  // otherwise, if the needle is - again - a string, and
  // document.querySelectorAll() can find a collection
  // (of one or more) elements matching the selector that
  // the needle is implied to be then we retrieve those
  // elements and, using Array.from(), we convert the
  // collection into an Array:
  } else if ('string' === typeof needle && document.querySelectorAll(needle)) {
    needle = Array.from(document.querySelectorAll(needle));
  }

  // here we return the results to the calling context:
  return needle;
}


function addNewElement(opts) {

  // the default settings for the function:
  // append:  Boolean, true: the content will be
  //          inserted after the found sibling-
  //          node; false: the content will be
  //          inserted before the found sibling-
  //          node.
  // classes: String, a string of white-space
  //          separated class-names to add to
  //          the new contents,
  //          Array, an array of class-names to
  //          add to the new contents.
  // content: String, a string of HTML you wish
  //          to appear in the newly-added content.
  // count:   Number, the number of elements you
  //          wish to insert at once.
  // create:  String, the element-type to create
  //          null, if you want the function to
  //          'decide' for itself.
  // parent:  Node, the element to which you want
  //          to add new elements,
  //          String, the id of the element to
  //          which you want to add new elements,
  //          or a CSS selector by which you want
  //          find the element(s) in the document
  //          to add new elements to.
  // sibling: Node, the node beside which the new
  //          element(s) should be added.
  //          Null, the function will try to determine
  //          the desired element beside which the
  //          content should be added, based on
  //          the 'append' setting (above).
  var settings = {
      'append': true,
      'classes' : null,
      'content': 'Newly-added element.',
      'count': 1,
      'create': null,
      'parent': document.body,
      'sibling': null
    },

    // uninitialised variables for use later, primarily
    // to declare/instantiate variables in one place:
    parents,
    childType,
    created,
    sibling,
    clone,
    classes,
    count,

    // a documentFragment to enable the addition of multiple
    // elements at the same time without triggering (quite so)
    // many redraws of the document/page:
    fragment = document.createDocumentFragment();

  // using Object.keys to iterate over the opts Object, if
  // one is supplied or an empty object to avoid errors,
  // using the Array.prototype.forEach() method:
  Object.keys(opts || {}).forEach(function(key) {

    // here we update/overwrite the keys of the
    // settings object to the values held in those
    // properties of the opts Object:
    settings[key] = opts[key];
  });

  // we call the derive function to retrieve an array
  // of element(s):
  parents = derive(settings.parent);

  // checking, and then storing, the value of
  // settings.append; it it's equal to true the
  // assessment returns true, if it's equal to
  // false the assessment returns false (this
  // is a naive check, because it requires that
  // a Boolean is stored in that property):
  appendCheck = settings.append === true;

  // ensuring that the settings.count number
  // is a number by parsing the potential
  // String, other-based number, into base-10:
  count = parseInt(settings.count, 10);

  // iterating over each of the parents:
  parents.forEach(function(pater) {
    // 'pater' the first argument is a reference
    // to the current array-element of the array
    // over which we're iterating.

    // retrieving the element-type to be created,
    // if a value was supplied in settings.create
    // then we use that (we don't check it's a
    // valid element, or that it can be validly
    // contained in the nominated parent), otherwise
    // if the current element node has children
    // then we retrieve the localName of its
    // lastElementChild, if it has no children
    // the ternary returns null and we move to
    // the string of 'div':
    childType = settings.create || (pater.children.length > 0 ? pater.lastElementChild.localName : null) || 'div';

    // here we create the element:
    created = document.createElement(childType);

    // if the earlier assessment of settings.append
    // resulted in true:
    if (appendCheck === true) {

      // we find the sibling beside which to insert the
      // new content; if a node was supplied we use that,
      // otherwise we use the lastElementChild or lastChild:
      sibling = settings.sibling || pater.lastElementChild || pater.lastChild;
    } else if (appendCheck === false) {
      // otherwise, we use either the supplied value or
      // we use the firstElementChild or firstChild:
      sibling = settings.sibling || pater.firstElementChild || pater.firstChild
    }

    // assign the supplied - or default - content to the
    // created element:
    created.innerHTML = settings.content;

    // if any class-names have been supplied:
    if (settings.classes) {

      // we first check whether the settings.classes
      // variable is an Array (using Array.isArray),
      // which returns a Boolean (true or false); if
      // it returns true we simply use the Array otherwise
      // we assume it's a String and split that String
      // on its white-space characters (/\s+/):
      classes = Array.isArray(settings.classes) ? settings.classes : settings.classes.split(/\s+/);

      // iterating over the array of class-names:
      classes.forEach(function(cN) {
        // the first argument (cN) is a reference
        // to the current array-element of the
        // Array over which we're iterating.

        // here we use the Element.classList API to
        // add each of the class-names:
        created.classList.add(cN);
      });
    }

    // a simple for loop to add the desired
    // number of new elements (as supplied in
    // the settings.count, or opts.count
    // setting):
    for (var i = 0; i < count; i++) {

      // clone the created-element (and its
      // child elements):
      clone = created.cloneNode(true);

      // append the cloned node to the 
      // documentFragment we created
      // earlier:
      fragment.appendChild(clone);
    }

    // here we use parentNode.insertBefore() to insert
    // the new contents (held in fragment) either the
    // sibling.nextSibling (if appendCheck is true) or
    // before the sibling (if appendCheck is false):
    pater.insertBefore(fragment, (appendCheck ? sibling.nextSibling : sibling));
  });
}

// retrieving the <button> elements on the page, and converting
// to an Array, using Array.from():
var buttons = Array.from(document.querySelectorAll('button'));

// iterating over those <button> elements in the Array:
buttons.forEach(function(button) {

  // using the anonymous function of the addEventListener()
  // to call the addNewElement function, in which
  // we set the opts.parent setting to the
  // previousElementSibling of the button
  // firing the event:
  button.addEventListener('click', function() {
    addNewElement({
      'parent': button.previousElementSibling
    });
  });
});

function derive(needle) {
  if (needle.nodeType && needle.nodeType === 1) {
    needle = [needle];
  } else if ('string' === typeof needle && document.getElementById(needle)) {
    needle = [document.getElementById(needle)];
  } else if ('string' === typeof needle && document.querySelectorAll(needle)) {
    needle = Array.from(document.querySelectorAll(needle));
  }

  return needle;
}

function addNewElement(opts) {

  var settings = {
      'append': true,
      'classes': null,
      'create': null,
      'content': 'Newly-added element.',
      'count': 1,
      'parent': document.body,
      'sibling': null
    },
    parents,
    childType,
    created,
    sibling,
    clone,
    classes,
    fragment = document.createDocumentFragment();

  Object.keys(opts || {}).forEach(function(key) {
    settings[key] = opts[key];
  });

  parents = derive(settings.parent);
  appendCheck = settings.append === true;

  parents.forEach(function(pater) {
    childType = settings.create || (pater.children.length > 0 ? pater.lastElementChild.localName : null) || 'div';
    created = document.createElement(childType);
    if (appendCheck === true) {
      sibling = settings.sibling || pater.lastElementChild || pater.lastChild;
    } else if (appendCheck === false) {
      sibling = settings.sibling || pater.firstElementChild || pater.firstChild
    }

    created.innerHTML = settings.content;

    if (settings.classes) {
      classes = Array.isArray(settings.classes) ? settings.classes : settings.classes.split(/\s+/);
      classes.forEach(function(cN) {
        created.classList.add(cN);
      });
    }

    for (var i = 0; i < settings.count; i++) {
      clone = created.cloneNode(true);
      fragment.appendChild(clone);
    }

    pater.insertBefore(fragment, (appendCheck ? sibling.nextSibling : sibling));
  });
}

var buttons = Array.from(document.querySelectorAll('button'));

buttons.forEach(function(button) {
  button.addEventListener('click', function() {
    addNewElement({
      'parent': button.previousElementSibling
    });
  });
});
div {
  border: 2px solid #eeeeee;
  background-color: #dff0d8;
}
ol {
  background-color: #dff0d8;
}
li {
  background-color: #eff0c8;
}
<p>In next example we can add new child element to this list:</p>
<ol id="list">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ol>
<button>Add new li-element to this list</button>
<p>In next example we can add new child element to this div:</p>
<div id="someThing">Something here</div>
<button>Add new div-element to this div</button>

JS Fiddle demo.

References:

David Thomas
  • 249,100
  • 51
  • 377
  • 410
  • Big thanks! That looks like an advanced and more universal way to process the elements with their parent nodes and children! Just one question - can you, please, edit your fiddle post, so it will be possible to see how it works? I'm currently looking deeper inside your `addNewElement`.. – WebSurfer Aug 01 '16 at 17:44
  • In the way, so when you will click the buttons -> then it will work. – WebSurfer Aug 01 '16 at 18:39
  • It does work for me, on both Windows 10 and Android. Does it not work for you? – David Thomas Aug 01 '16 at 19:19
  • https://jsfiddle.net/davidThomas/sh6ju6e9/ - I checked various browsers and different computers and it doesn't run.. Sorry.. Maybe we need to add onclick function to that buttons? – WebSurfer Aug 01 '16 at 20:38
  • Just so that we're on the same page: does it work in the Snippet here on Stack Overflow? If not, could I ask: are you seeing any errors reported in your browser's developer tools? And, either way, what do you mean by "*not working*" in context? Because - for me - clicking the button adds a new element to the ` – David Thomas Aug 01 '16 at 22:20
0

Well, I've found simple way how to fix it, but I was looking for something even more basic:

document.getElementById(parentId).appendChild(newElement);

EDIT: Another way how to do it:

<!DOCTYPE html>
<html>
<body>
<style>
div {
    border: 2px solid #eeeeee;
    background-color: #dff0d8;
}
ol {
    background-color: #dff0d8;
}
li {
    background-color: #eff0c8;
}
</style>
<script>
function addNewElement(newElementType,parentId,parentElementType) {
    //document.getElementById(clickedId).appendChild(newElement);
    var el = parentElementType + "[id=" + parentId + "]";
    el = document.querySelector(el);
    var newElement = document.createElement(newElementType);
    newElement.innerHTML = 'new element';
    el.appendChild(newElement);
}
</script>
<p>In next example we can add new child element to this list:</p>
<ol id="list">
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ol>
<button onclick="addNewElement('li','list','ol')">Add new li-element to this list</button>
<p>In next example we can add new child element to this div:</p>
<div id="someThing">Something here</div>
<button onclick="addNewElement('div','someThing','div')">Add new div-element to this div</button>
</body>
</html>

But now we need to pass parent node type in addNewElement function in this new example. Or we can also define classes for ul and div elements and use them instead of ids. It's more advanced way, but it may be more useful in some cases. Here's the documentary about document.querySelector and document.querySelectorAll.

Also read this querySelector and querySelectorAll vs getElementsByClassName and getElementById in JavaScript, if you want to get some additional info.

Community
  • 1
  • 1
WebSurfer
  • 1
  • 5
  • `parentElementType + "[id=" + parentId + "]"` is a very inefficient selector. Because of the way selector engines work, it will first find all elements of `parentElementType` and then filter by checking the `id` attribute, even though the id should be unique it will check all of the elements. Attribute selectors are relatively slow too. Instead you should use the [id selector](https://www.w3.org/TR/css3-selectors/#id-selectors) without the tag name, it is very efficient at finding elements by id. Or as @Alex mentioned use `getElementById`, which is even more efficient. – Useless Code Jul 30 '16 at 12:45
  • @UselessCode thank you for reply! Can you show me how you recommend to use suggested id selector? For me, for e.g., code from my situation with "document.querySelector(div[id='someThing']" means almost the same as "h1#chapter1" from the example from the link you suggested, isn't they are both id selectors? Please, explain deeper, if you can. And about "getElementById" - I totally agree. While it's basic method to find element by Id - it is preferable to use. And in most cases (JS perf tests) it works faster. But, since I'm kind a newbie in JS, I don't see much diff between these 3 ways. – WebSurfer Jul 30 '16 at 13:01
  • Simply put, the more complicated the selector the slower it is going to be. `#something` just has to look for an element with an id of something, generally the browsers have an internal list of elements with ids so doing that is fast. `div[id=something]` has to first find all of the divs and then see if they have an id of something. It seems that in recent years browsers have [gotten better](http://calendar.perfplanet.com/2011/css-selector-performance-has-changed-for-the-better/) at quickly matching selectors, but I'm not sure if that applies to the DOM API or just the CSS engine. – Useless Code Jul 30 '16 at 14:40
0

I know you already have an answer that works for you, but I just wanted to add one that shows a more flexible way of doing this using a configuration object instead of just passing in a tag name. To make it more flexible you can pass in a reference to a parent object instead of an id. Also, it returns a reference to the newly created element in case you want to do something with it after it is added to the DOM.

'use strict';

var addNewElement = function (configItems, elParent) {
    var newElements = [];

    if (!Array.isArray(configItems)) {
      // if configItems is not an array, and therefore a
      // single config object or string, turn it into
      // a single element array
      configItems = [configItems];
    }

    // If elParent is a string assume it is
    // the id of an element in the page and select it
    if (typeof elParent === 'string') {
      elParent = document.getElementById(elParent);
    }

    configItems.forEach(function (config) {
      var option,
        elChild;
      // if a string is passed in, assume it is
      // the tagName and create a default config object
      if (typeof config === 'string') {
        config = {tag: config};
      }


      elChild = document.createElement(config.tag);

      for (option in config) {
        if (config.hasOwnProperty(option)) {
          switch (option) {
            case 'tag':
              // do nothing, already used tag to create new element
              break;
            case 'html':
              // just a shortcut so we don't have to use
              // innerHTML in our config object
              elChild.innerHTML = config.html;
              break;
            case 'text':
              // another shortcut
              elChild.textContent = config.text;
              break;
            case 'class':
              // if we are passed an array convert it to a space delimited string
              elChild.className = Array.isArray(config.class) ?
                config.class.join(' ') : config.class;
              break;
            default:
              // if we haven't already handled it, assume it is
              // an attribute to add to the element
              elChild.setAttribute(option, config[option]);
          }
        }
      }

      // default text if none was specified
      if (elChild.innerHTML === '') {
        elChild.innerHTML = 'new element';
      }

      newElements.push(elChild);
      elParent.appendChild(elChild);
    });

    // return a reference to the new element(s)
    // in case you want to do something else with it
    // after it was inserted into the document
    // returns a single item or an array depending on how many
    // items you passed it in configItems
    return newElements.length === 1 ? newElements[0] : newElements;
};

Usage would look like this:

// just add a new element with the default text by id
addNewElement('li', 'list');

var list = document.getElementById('list');

// a little fancier, this time using an element reference
addNewElement({
  tag: 'li',
  html: 'Custom List Item!',
  class: 'fancy'
}, list);

addNewElement({
  tag: 'input',
  placeholder: 'Type here',
  value: 'Delete me'
}, document.body); // attach to the body

// do something with the element
// after we create it
var houdini = addNewElement({
  tag: 'li',
  text: 'Now you see me.',
  class: ['houdini', 'show'],
}, list);

setTimeout(function () {
  houdini.textContent = "Now you don't";
  houdini.classList.remove('show');
}, 2000);

var checkElements = addNewElement([
  {
    tag: 'input',
    id: 'check',
    type: 'checkbox',
    checked: 'checked',
  },
  {
    tag: 'label',
    for: 'check',
    html: 'Uncheck me!'
  }
], document.body);

jsFiddle showing it in action.

Using hasOwnProperty is necessary since we are using for in.

The 'class' case is there because in ES3 you could not use reserved words as property names with dot notation, so when the DOM API was designed they instead used className to represent the class property. Ever since ES5 we can use reserved words as properties without quoting them. This allows us to add a 'class' shortcut property.

Useless Code
  • 12,123
  • 5
  • 35
  • 40
  • Many thanks to you too! I'll dig deeper into your code to get fully understanding. I can not see jsFiddle example in action and can't understand why.. Looks like need to learn your code and/or javascript better ^_^ – WebSurfer Aug 01 '16 at 17:51