Isolated directive with two-way binding in Angular, but the property is not defined?

Confusion arises when trying to understand the concept of two-way data binding. Let's take a look at the code snippet below:

.directive('mupStageButtons', function() {
    return {
        transclude: true,
        template: '<span ng-transclude></span>',
        replace: true,
        scope: {
            property: "=",
            action: "="
        },
        controller: function($scope) {
            console.log($scope); //I can see the property of $scope defined in console
            console.log($scope.property); //undefined
            this.property = $scope.property;
            this.changeStage = $scope.action; //anyway this is ok
        },
    };
})
.directive('mupStageButton', function() {
    return {
        transclude: true,
        templateUrl: '/static/templates/directives/StageButton.html',
        require: '^^mupStageButtons',
        scope: {
            value: "=",
            btnClass: "@",
        },
        link: function(scope, element, attrs, mupStageButtonsCtrl, transclude) {
            scope.property = mupStageButtonsCtrl.property;
            scope.changeStage = mupStageButtonsCtrl.changeStage;
        }
    };
})

//html

<mup-stage-buttons property="company.stage" action="setStage">
    <mup-stage-button value="0" btn-class="btn-default-grey">
    </mup-stage-button>
</mup-stage-buttons>


//controller for that html ^^^

.controller('CompanyDetailController', function($scope, $stateParams, Company){
    Company.query ({
      id : $stateParams.companyId
    }, function (data) {
      $scope.company = new Company(data);
    });
}

//template for <mup-stage-button>

<label ng-class="property === value ? 'active' : 'btn-on-hover' " class="btn {{btnClass}}" ng-click="changeStage(value)">
    <div ng-transclude></div>
</label>

The question here is about whether "=" signifies that changes in the outer scope will be reflected through data binding. In this scenario, however, fetching a $resource leaves the "property" undefined even after it has been fetched. What could be causing this issue?

EDIT: The expected behavior is for the ng-class in the <mup-stage-button> template to function correctly.

EDIT: View the plunker for more context: https://plnkr.co/edit/drXxyMpd2IOhXMWFj8LP?p=preview

Answer №1

One key point to note about the transclude option is that the content wrapped within it is connected to the OUTER scope rather than the directive's scope.

After compilation, the scope bindings in your scenario will appear as follows:

<div ng-controller="CompanyDetailController">
    <mup-stage-buttons property="company.stage" action="setStage"> <-- although 'property' is correctly bound, it is not accessible below due to transclusion -->
        <span ng-transclude>
            {{company.stage}} <!-- Accessible here due to transclusion, but 'property' is not available! -->

            <mup-stage-button property="company.stage" value="0"> 
                <!-- Scope of the directive here can bind to outer scope's 'company.stage' -->
                {{property}} - {{value}} <!-- this will work -->
                <label ng-class="property === value ? 'active' : 'btn-on-hover'" class="btn {{btnClass}}" ng-click="changeStage(value)">
                    <div ng-transclude>
                        <!-- Content transcluded here, connected to CompanyDetailController $scope -->
                        not working ng-class 0
                    </div>
                </label>
            </mup-stage-button>
        </span>
    </mup-stage-buttons>
</div>

To resolve this issue and make your code function correctly (Plunk), you just need to map the property to company.stage on the child directive exclusively.

UPDATE

In order to eliminate repetitive binding of property="company.stage" on child directives and pass data through the controller and link function of parent and child directives respectively, it is recommended to utilize a wrapping object for your scope properties. This way, changes made to this object will be reflected in the child scopes as they share a reference to the object. This concept is known as the dot notation:

CompanyDetailController:

$scope.vars = {};
this.getCompany = function () {
  $scope.vars.company = $scope.company = {stage: 0}; 
};

Then bind the vars property to the parent directive's scope:

// ...
scope: {
    vars: '=',
},
controller: function($scope) {
    this.vars = $scope.vars;
}
// ...

Next, assign the reference of vars to the child directive's scope:

// ...
link: function(scope, element, attrs, mupStageButtonsCtrl, transclude) {
    scope.vars = mupStageButtonsCtrl.vars;
}
// ...

Finally, access it in the view of the child directive:

<label ng-class="vars.company.stage === value ? 'active' : 'btn-on-hover'">...</label>

This approach eliminates the need to duplicate bindings on instances of the child directive.

Plunk has been updated accordingly.

Answer №2

When working with JavaScript

Primitives are passed by value, Objects are passed by "copy of a reference".

  • For a more detailed explanation, check out this resource: stackoverflow.com/questions

One way to tackle this issue is by using the $watch method:

.directive('mupStageButtons', function() {
    return {
        transclude: true,
        template: '<span ng-transclude></span>',
        replace: true,
        scope: {
            property: "=",
            action: "="
        },
        controller: function($scope) {
            that = this;
            $scope.$watch('property', function(newValue){
                that.property = newValue;    
      /***Refreshing this.property (normal assignment would only copy value, 
     it would not behave as a reference to desired transcluded property)***/
            });
            this.changeStage = $scope.action;
        },
    };
})
.directive('mupStageButton', function() {
    return {
        transclude: true,
        templateUrl: '/static/templates/directives/StageButton.html',
        require: '^^mupStageButtons',
        scope: {
            value: "=",
            btnClass: "@",
        },
        link: function(scope, element, attrs, mupStageButtonsCtrl, transclude) {
            scope.btnCtrl = mupStageButtonsCtrl;
            scope.changeStage = mupStageButtonsCtrl.changeStage;
        }
    };
})

In addition to the $watch method, another crucial part is utilizing this in the link function:

scope.btnCtrl = mupStageButtonsCtrl;

Instead of simply doing:

scope.property = mupStageButtonsCtrl.property;

which would create a separate copy and not update dynamically, assigning the ctrl reference to scope.btnCtrl resolves the issue. Here is the template for the child directive:

<label ng-class="btnCtrl.property === value ? 'active' : 'btn-on-hover' " class="btn {{btnClass}}" ng-click="changeStage(value)">
    <div ng-transclude></div>
</label>

Now, these directives can be used generically by passing just the property like company.stage, eliminating the need for the directive to know the specific property name (stage).

<mup-stage-buttons property="company.stage" action="setStage">
    <mup-stage-button value="0" btn-class="btn-default-grey">
        Stage 0
    </mup-stage-button>
</mup-stage-buttons>

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

Why does the 401 error continue to persist while attempting to log in using Google Identity service on my Laravel application?

Trying to implement Google authentication services for user authentication. I've already integrated Laravel sanctum to allow users to log in and register successfully. This time, I want to add Google Identity services as an additional authentication ...

Whenever I attempt to start the server using npm run server, I encounter the following error message: "Error: Unable to locate module './config/db'"

This is the server.jsx file I'm working with now: Take a look at my server.jsx file Also, here is the bd.jsx file located in the config folder: Check out the db.jsx file Let me show you the structure of my folders as well: Explore my folder structur ...

Ways to conceal <td>s in angularjs on specific rows during ng-repeat

In the first <tr>, I have a table with many <td> elements that I am populating using ng-repeat. <tr ng-repeat="receipt in $data"> <td data-title="voucher number"> <span>{{receipt.voucher_no}}</span> </td ...

retrieve user photos from twitter using angular.js and firebase

As a newcomer to angular.js and Firebase, I am delving into editing code from an open-source script to expand my knowledge. I initially used a chat script with a Facebook login. However, I have decided to switch the Facebook login to a Twitter login, as F ...

AJAX call error: Invocation not permitted

I'm currently working on a web application using JQuery that requires communication with a server. I've reused this code multiple times, only changing the data and success function. However, every time I execute this function, I encounter an err ...

Generate a unique identifier for the HTML content

Is there a more efficient way to assign unique id tags to certain words using vanilla javascript? For example: <p>All the king's horses ran away.</p> For instance, "All" would have the id "term1", "King's" would have "term2", and ...

Encountering an "AJAX not a function" error while using the d3/flask interface

Hey there! I'm new to the world of JavaScript and AJAX. Take a look at this d3 function I've been working on: var node = g.selectAll(".node") .data(root.descendants()) .enter().append("g") .attr("class", function(d) { return "node" + ...

Having trouble getting the templateUrl to work properly with AngularUI-Router?

Can you please explain the flow of how a URL is processed when visited and provide insights on why Angular's templateUrl may not be working? When a user clicks on a URL in their browser, it first checks the cache to see if the URL is saved from the $ ...

Encountering issues with running NPM on my Ubuntu server hosted on Digital Ocean

After successfully installing node (nodejs), I encountered a persistent error when attempting to use NPM. Despite researching the issue extensively and trying different solutions, I have been unable to resolve it. Here is the error message displayed in th ...

Transferring information through Ajax to PHP

When using AJAX to send a string via the GET method, I've noticed that punctuation characters sometimes don't appear when echoed in PHP. For instance, sending "sub + 12" results in PHP echoing "sub 12", and sending "&()*" echoes an empty str ...

Explore the full range of events available on the Angular UI-Calendar, the innovative directive designed for Arshaw FullCalendar

Utilizing external events with Angular ui-calendar: HTML: <div id='external-events'> <ul> <li class='fc-event'>Event 1</li> <li class='fc-event'>Event 2< ...

Passing a PHP array to a JavaScript function using a button click event in an HTML form

It seems there was some confusion in my approach. I'm working with a datatable that has an edit button at the end. Clicking on this button opens a modal, and I want to pass the data from the table to the modal. While I can send it as a single variabl ...

Can I substitute custom tags for the traditional <p> tag?

My HTML with CSS has an issue where a custom HTML tag is not displaying the text while my CSS picks up another tag. Interestingly, replacing <title>Layout Controls</title> with <p>Layout Controls</p> resolves the problem and shows t ...

Obtain a specific item from an array using its id in HTML with the help of angular.js

Here is the desired output: Name : AAA, Type: Flower Name : BBB, Type: Bird The Controller Function function myCtrl($scope) { $scope.items = [ {id: 0, name: 'AAA', type: 12}, {id: 1, name: 'BBB', type: 33}]; $scope.type ...

Are there any negatives to turning off create-react-app's SKIP_PREFLIGHT_CHECK option?

After setting up my create-react-app project, I added eslint as a dev dependency. My reasons for doing this include: 1) Running eslint as a pre-commit check using husky and lint-staged 2) Extending CRA's eslint with airbnb and prettier lint configs ...

In need of clarification on the topic of promises and async/await

I have been utilizing Promises and async/await in my code, and it seems like they are quite similar. Typically, I would wrap my promise and return it as needed. function someFetchThatTakesTime(){ // Converting the request into a Promise. return new ...

Protractor - selecting a button within a table

Following the HTML code provided below, there is a table containing a list of test site links with a delete button next to each test site. /* Element locators in the table */ var testSiteLinks = element.all(by.css('a[ui-sref^="testpages"]')); ...

XMLHttpRequest Error: The elusive 404 code appears despite the existence of the file

This is the organization of my project files: The folders Voice, Text, and Template are included. https://i.stack.imgur.com/9un9X.png When I execute python app.py and navigate to localhost http://0.0.0.0:8080/, the index.html page is displayed with conte ...

Tips for effectively handling an angularjs HTTP error with a custom page error in MVC

• Handling page not found and server problem errors by redirecting to custom error pages has been successfully implemented as shown below: web.config <customErrors mode="On"> <error statusCode="400" redirect="~/Error/Error400"/> <e ...

Initiate the Material TextField onChange event from within a nested component

I am currently working on a component that looks like this: class IncrementField extends Component { inputRef; changeValue() { this.inputRef.value = parseInt(this.inputRef.value) + 1; } render() { const { ...other } = this.props; return ( ...