Tips for achieving AngularJS 2-way binding of multiple objects within a contenteditable element, along with any surrounding text

I would like to create a collection similar to this structure:

var array = [{innerText: "I was with ", index:0},{obj: "Mary", id: 1, index:1}, {innerText: " and ", index:2},{obj: "John", id: 2, index:3}]; 

The goal is to have a content editable div that displays the elements from the array and stays synchronized with any changes made to the innerText or object inputs. When the user modifies the content in the div, the array should be automatically updated.

For instance, the div might look like this (without AngularJS):

<div contenteditable="true">
I was with <input type="text" value="Mary" data-index="1"/> and <input type="text" value="John" data-index="3"/>
</div>  

This setup should support backspacing within the div, inserting new inputs, and typing text, all while updating the array accordingly.

The current implementation involves directives for the collection as a whole, the innerText values, and the objects. However, the binding does not account for mutations in the internal DOM of the contenteditable element. There's also an issue with using {{innerText}} as a template for innerText, as it doesn't guarantee proper binding when typing text in the editable area.

Edit: Additionally, a collection structured like this could still be beneficial with the same contenteditable functionality:

var array = [{obj: "Mary", id: 1, index:1}, {obj: "John", id: 2, index:3}, {innerText: "I was with @ and @""]; 

Edit2: Reopening the question to address the lack of true two-way binding in the previously accepted solution. The updated code should generate a model like the following:

"modelValue": [
    {
      "innerText": "abc",
      "index": 0
    },
    {
      "obj": "abc",
      "index": 1
    },
    {
      "innerText": "abc",
      "index": 2
    }
  ]

The corresponding view should display:

"viewValue": "\n abc\n <input type=\"text\">\n abc\n "

The solution should include a service that generates a static model when a button is pressed, along with a controller function that sets the modelValue in the scope and converts it into the desired viewValue format.

Edit3: Demonstrating how true two-way binding can be achieved without using $watch by leveraging compile pre-link and post-link methods:

*Code examples and CSS styling included*

*JavaScript and AngularJS libraries included*

Answer №1

If you're looking for a solution using a custom directive, I have a method that might help. While it may not align perfectly with your specific data model, it should still achieve the desired outcome.

Here's an overview of the object model:

{
  "viewValue": "\n abc\n <input type=\"text\">\n abc\n ",
  "modelValue": [
    {
      "innerText": "abc",
      "index": 0
    },
    {
      "obj": "abc",
      "index": 1
    },
    {
      "innerText": "abc",
      "index": 2
    }
  ]
}

The viewValue represents the HTML contenteditable structure, while the described data is stored in modelValue.

We establish multiple event listeners (taking inspiration from this reference) and construct the model accordingly.

elm.bind('blur keyup paste input', function() {
    scope.$apply(function() {
        var new$viewValue = {
            viewValue: elm.html(),
            modelValue: []
        }
        var index = 0;
        angular.forEach(elm[0].childNodes, function(value, index) {
            if (value.nodeName === "INPUT") {
                if (value.value) {
                    new$viewValue.modelValue.push({
                        obj: value.value,
                        index: index
                    });
                } else {
                    value.parentNode.removeChild(value);
                }
            } else if (value.nodeName === "#text") {
                new$viewValue.modelValue.push({
                    innerText: value.textContent.trim(),
                    index: index
                });
            }
            index++;
        });
        ctrl.$setViewValue(new$viewValue);
    });
});

This script retrieves all child nodes within the contenteditable div, identifies their type (input or text), and populates the model accordingly. Additionally, we preserve the HTML state to facilitate view reconstruction.

A render function is utilized to display the view, updating the HTML content based on the stored model.

ctrl.$render = function() {
    elm.html(ctrl.$viewValue.viewValue);
    //Untested code that should add the text back into the fields if the model already exists
    angular.forEach(elm[0].childNodes, function (value, index) {
        if (value.nodeName === "INPUT") {
            if (ctrl.$viewValue.modelValue[index].obj) {
                 value.value = ctrl.$viewValue.modelValue[index].obj
            }
            else {
                 value.parentNode.removeChild(value);
            }
        }
    });
};

UPDATE: For bidirectional data binding, consider the following approach:

scope.getViewValue = function() {
    var tempDiv = document.createElement("div");
    angular.forEach(ctrl.$viewValue.modelValue, function(value, index) {
      if (value.innerText) {
        var newTextNode = document.createTextNode(value.innerText);
        tempDiv.appendChild(newTextNode);
      } else if (value.obj) {
        var newInput = document.createElement('input');
        newInput.type = "text";
        newInput.value = value.obj;
        tempDiv.appendChild(newInput);
      }
    })
    return tempDiv.innerHTML;
};

scope.$watch(function() { return ctrl.$modelValue; }, function(newVal, oldVal) {
    var newViewValue = scope.getViewValue();
    ctrl.$setViewValue({
      "viewValue": newViewValue,
      "modelValue": ctrl.$viewValue.modelValue
    });
   ctrl.$render();
}, true);

This feature entails monitoring changes to the object specified by ng-model. Upon modification, it recalculates the innerHTML of the view. A known issue involves losing focus upon field redrawing, which can be resolved by saving and restoring the focused element during redraws.

To delve further into the code and observe its functionality, refer to the embedded snippet below. An extra button has been included to demonstrate the capability of adding more text fields.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Guide to sending an array to Jade for rendering using NodeJS and ExpressJS

Coming from a background in "functional" programming languages like PHP, Lua, and C, I am currently facing a challenging learning curve as I transition to the MVC model used in NodeJS. I've been struggling to display a simple MySQL array in Jade and ...

Error with JavaScript slideshow The slideshow using JavaScript seems to

I'm currently utilizing a script from the WOW Slider (free version) that looks like this: var slideIndex = 0; function showSlides() { var i; slides = document.getElementsByClassName("mySlides"); dots = document.getEle ...

Unable to abort AWS Amplify's REST (POST) request in a React application

Here is a code snippet that creates a POST request using the aws amplify api. I've stored the API.post promise in a variable called promiseToCancel, and when the cancel button is clicked, the cancelRequest() function is called. The promiseToCancel va ...

Extracting precise information from a JSON file using Angular's $http.get

I am struggling with extracting a specific user from a JSON file containing a user list and displaying it on an Angular index page. Despite extensive research, I have been unable to find a satisfactory solution. The user list must remain in a JSON file ins ...

Obtaining template attributes in CKEditor: A guide

I have been working with the template plugin in CKEditor to load predefined templates. Each template is defined as follows: templates: [ { title: "Quickclick 1", image: "template1.png", description: "Quickclick 1 template", html_et: "& ...

What methods are available for implementing hover effects within attributes using a JavaScript object?

const customPanelStyle = { 'background-color': 'red', 'border': '1px solid green', ':hover'{ 'background': 'blue' } } class some extends React.Component{ rende ...

How to assign a specific class to the previous item in an ng-repeat loop within a

Below is a prime example of a reusable progress bar that utilizes ng-repeat and directives. Check out the Plunker In my current setup, I use the class .progressBar__isActive for the active page or index. What I now want to achieve is adding the progressB ...

Utilizing Angular 5 routerLink for linking to absolute paths with hash symbols

I am facing an issue with a URL that needs to be opened in a new tab. Unfortunately, Angular generates this URL without the # symbol. Currently, we have implemented the following: <!-- HTML --> <a title="Edit" [routerLink] = "['/object/objec ...

Navigating through nested data in React TypeScript can be accomplished by accessing the nested data

How can data in a nested interface like the one shown below be accessed in React TypeScript? export interface School { prices: Array<{ state: { response_header?: { school_type_opportunities?: Array<{ benefit_type_opportunity?: st ...

The suspense fallback feature in next.js 14 seems to be malfunctioning and not displaying properly

I'm currently experimenting with using Suspense in my next.js application, but I am facing an issue where the fallback does not display even after implementing a hardcoded delay. It seems that the results always show up immediately, even on page refre ...

Is it possible to dynamically add the URL to an iframe based on a condition being true, and then iterate through a list of URLs before

I'm trying to figure out how to change the URL in an iframe based on the presence of a class="show". The first time the element has the class "show," it should load about.html. The second time the same element has the class "show," it should open wor ...

Designing a toggle button interface with Material UI

Looking to add a toggle button to my page. Here is an image of what I have in mind: https://i.sstatic.net/tiQAP.png Unable to find any material-ui component or API for this specific toggle button design. Considering building a custom toggle button with CS ...

Unable to removeClass and addClass as expected

Each time I click on a div, it gets encased in a black line. However, my goal is for the previously selected div to lose the black outline whenever I click on a different div. Only the current div should have the border. Below is the code snippet that I a ...

How can I maintain the default function for links that have been clicked using window.on('click') event listener?

I am currently working on a project to visualize the spatial positions of 4673 of the closest galaxies. To enhance the user experience, I have implemented customized click events that allow users to focus on individual galaxies and even change their colors ...

Displaying Errors from Controllers in React Hook Forms

Currently, I am attempting to generate required errors for my input element that is enclosed within a Controller component from react-hook-form version 7. The Input consists of a Material-UI TextField structured like this; <Controller ...

Can you uncover the secrets of static generator functions through espionage?

My current project involves an utility function that exposes a generator: export class Utility { // This utility function provides a generator that streams 2^n binary combinations for n variables public static *binaryCombinationGenerator(numVars: ...

Usage of Firebase data

Looking for assistance on how to retrieve data from Firebase Cloud Store. My code includes the Firebase configuration and app.js but I'm facing an issue where the page appears blank on localhost:3000 without any errors. Below is the firebase.js confi ...

Analyzing the object for interface compatibility

When I receive a query string in one of my REST endpoints using koa-router, each value of the query string object parameter is always a string: { count: "120", result: "true", text: "ok" } Within my codebase, I have an Interface that represents the ...

Link property can be added to a bindpopup polygon in Leaflet to have it open in a new tab when clicked

Is it possible to include a hyperlink in popup content on Leaflet, similar to this example? function onEachFeature(feature, layer) { idLoDat = feature.properties.IDLo; layer.bindPopup("Company name: " + feature.properties.TenCty + " ...

Is there a way to prevent a page from rendering until the necessary data is retrieved?

I am facing an issue where my page is attempting to render before the data is available. I have used async/await in my code, but I keep getting an error saying that the data is undefined. Interestingly, when I comment out the page elements and check the Re ...