Build a Lightweight DOM Element Wrapper
Web development often involves frequent interaction with the Document Object Model (DOM). Directly manipulating the native DOM API can sometimes be verbose and repetitive, especially when performing common tasks like selecting elements, modifying content, or handling events. This challenge asks you to create a simple wrapper around a native DOM element, providing a more concise and chainable API for common operations.
Problem Description
Your task is to implement a DomWrapper class or object that encapsulates a single native DOM element. This wrapper should expose a set of methods that simplify common DOM operations, allowing for a more fluent and readable coding style, similar to popular libraries but on a much smaller scale.
Key requirements:
- Constructor: The
DomWrappershould be initialized with either:- A CSS selector string (e.g.,
'#my-id','.my-class','div'). If multiple elements match, it should wrap the first one found. If no element is found, it should still create aDomWrapperinstance that gracefully handles subsequent method calls (e.g., by doing nothing or returningnull/undefinedwhere appropriate, but not throwing errors). - A direct reference to a native DOM element object.
- A CSS selector string (e.g.,
- Chainable Methods: Most methods that modify the element's state (e.g.,
addClass,removeClass,css,append,prepend,attr[setter]) should return theDomWrapperinstance itself (this) to allow for method chaining. - Query/Getter Methods: Methods that retrieve information or return new wrapper instances (e.g.,
text,html,attr[getter],find,children,parent) should return the relevant data or a newDomWrapperinstance (or an array ofDomWrapperinstances forfind/children). - Core Methods to Implement:
text(content): Get or set the text content of the element. Ifcontentis provided, sets it; otherwise, returns the current text.html(content): Get or set the inner HTML of the element. Ifcontentis provided, sets it; otherwise, returns the current HTML.addClass(...classNames): Adds one or more CSS classes to the element.removeClass(...classNames): Removes one or more CSS classes from the element.toggleClass(className, force): Toggles a CSS class.force(optional boolean) can be used to explicitly add or remove.append(content): Appends content (string HTML or anotherDomWrapperinstance) to the element's children.prepend(content): Prepends content (string HTML or anotherDomWrapperinstance) to the element's children.on(eventName, handler): Attaches an event listener to the element. Thehandlershould ideally havethisbound to theDomWrapperinstance.off(eventName, handler): Removes an event listener from the element.attr(attributeName, value): Get or set an attribute. Ifvalueis provided, sets it; otherwise, returns the attribute's value.css(property, value): Get or set a CSS property. Ifvalueis provided, sets it; otherwise, returns the computed style forproperty.find(selector): Finds the first descendant element matching the selector and returns a newDomWrapperinstance for it. If no element is found, returns aDomWrapperinstance that handles this gracefully.children(): Returns an array of newDomWrapperinstances for all direct children of the element.parent(): Returns a newDomWrapperinstance for the element's direct parent.remove(): Removes the element from the DOM. This method should make the currentDomWrapperinstance's underlying element referencenulland returnnullorundefined.
Edge Cases:
- Constructor with a non-existent selector: Methods called on the resulting
DomWrapperinstance should not throw errors but gracefully handle the absence of the underlying DOM element (e.g.,text()returns empty string,addClass()does nothing,find()returns a wrapper fornull). attrorcssfor a non-existent property/attribute.on/offwith invalid event names or handlers.- Chaining methods on a
DomWrapperthat no longer holds a real DOM element (e.g., afterremove()has been called).
Examples
Example 1: Basic Element Creation and Manipulation
Input (Conceptual HTML setup):
<div id="app">
<p id="greeting">Hello</p>
</div>
Pseudocode:
// Imagine DomWrapper is a class you instantiate
let myWrapper = new DomWrapper('#greeting');
myWrapper.text('Welcome to Hone!');
myWrapper.addClass('highlight');
Output (Conceptual HTML result):
<div id="app">
<p id="greeting" class="highlight">Welcome to Hone!</p>
</div>
Explanation: A DomWrapper is created for the paragraph element. Its text content is updated, and a CSS class 'highlight' is added to it.
Example 2: Chaining and Event Handling
Input (Conceptual HTML setup):
<div id="container">
<button id="actionBtn">Click Me</button>
</div>
Pseudocode:
let btnWrapper = new DomWrapper('#actionBtn');
btnWrapper
.attr('data-count', '0') // Set a data attribute
.css('background-color', 'blue') // Set a CSS property
.on('click', function(event) {
// 'this' inside the handler refers to the DomWrapper instance if bound correctly
let currentCount = parseInt(this.attr('data-count'));
this.attr('data-count', currentCount + 1);
this.text('Clicked ' + (currentCount + 1) + ' times');
console.log('Button was clicked!');
});
Output (Conceptual behavior after clicking the button once):
- The button's 'data-count' attribute will be '1'.
- The button's text will change to 'Clicked 1 times'.
- A message will be logged to the console.
Explanation: The DomWrapper allows chaining `attr` and `css` methods. An event listener is attached which updates the element's attribute and text upon click, demonstrating how `this` can refer to the wrapper itself within the handler.
Example 3: Querying and Edge Cases
Input (Conceptual HTML setup):
<div id="parent">
<span class="child">Span 1</span>
<p class="child">Paragraph</p>
<button id="removeMe">Remove</button>
</div>
Pseudocode:
let parentWrapper = new DomWrapper('#parent');
let firstSpan = parentWrapper.find('span'); // Finds the first span within #parent
console.log(firstSpan.text()); // Should output "Span 1"
let nonExistent = new DomWrapper('.non-existent-class');
console.log(nonExistent.text()); // Should output "" (empty string)
nonExistent.addClass('error'); // Should do nothing, no error thrown.
let allChildren = parentWrapper.children();
// allChildren will be an array of DomWrapper instances for 'span.child' and 'p.child'
allChildren[0].css('color', 'red'); // Sets color of "Span 1" to red
let removeBtn = new DomWrapper('#removeMe');
removeBtn.remove(); // Removes the button from the DOM.
console.log(removeBtn.parent()); // Should return a wrapper for null/undefined gracefully
Output (Conceptual):
- Console: "Span 1"
- Console: "" (empty string)
- "Span 1" element will have its text color changed to red.
- The button with id "removeMe" will be gone from the DOM.
- The last console log should output a wrapper instance whose underlying element is null/undefined.
Explanation: `find` retrieves a new wrapper for a descendant. The constructor gracefully handles non-existent elements, preventing errors. `children` returns multiple wrappers for direct children. The `remove` method deletes the element and invalidates the wrapper for further DOM operations.
Constraints
- The implementation should not use any external DOM manipulation libraries (e.g., jQuery, Lodash). Rely solely on native browser DOM APIs.
- The wrapper should primarily manage a single underlying native DOM element. Methods like
findandchildrenshould return newDomWrapperinstances (or arrays of them) rather than attempting to manage multiple elements within a single wrapper. - Assume a modern browser environment (e.g., support for
document.querySelector,document.querySelectorAll,element.addEventListener,element.classList,element.style,element.remove). - The solution should be reasonably performant for typical web page interactions (handling hundreds of DOM manipulations, not millions).
- Input selectors will be valid CSS selectors. Input HTML strings will be valid fragments.
Notes
- Consider how to store the underlying native DOM element reference within your
DomWrapperinstance. This reference will benullorundefinedif the wrapper was initialized with a non-existent selector or if the element wasremove()d. - Pay close attention to the return values of each method to ensure chainability and correct data retrieval, especially for methods that can act as both getters and setters.
- For methods like
onandoff, remember thatthisinside the native event handler function usually refers to the native DOM element. You might need to usebindor an arrow function to ensurethisrefers to yourDomWrapperinstance within the handler. - When a
DomWrapperinstance is created for a non-existent element, ensure that its methods fail gracefully without throwing runtime errors. This might involve checking for the existence of the underlying DOM element at the beginning of each method call. - Think about how to return
DomWrapperinstances from methods likefind,children, andparent. You'll likely need to instantiate newDomWrapperobjects within these methods.