Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Feat: ngRef directive to publish controllers, or elements into current scope #16511

Merged
merged 3 commits into from
Jun 5, 2018

Conversation

Narretz
Copy link
Contributor

@Narretz Narretz commented Mar 26, 2018

What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
feat

What is the new behavior (if this is a feature change)?
publish a component's or element-directive's, or jqlite wrapped DOM element into the containing scope

Does this PR introduce a breaking change?
No

Please check if the PR fulfills these requirements

  • The commit message follows our guidelines
  • Fix/Feature: Docs have been added/updated
  • Fix/Feature: Tests have been added; existing tests pass

Other information:

This is a rebased and updated version of #14080 with the following changes:

  • publish the jqlite-wrapped DOM element instead of the raw DOM element. In AngularJS, we don't deal with raw DOM elements, so this would create an inconsistency
  • introduce a ngRefElement attribute that indicates that you always want to publish the jqlite-wrapped DOM element, even if a controller exists

@googlebot
Copy link

So there's good news and bad news.

👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there.

😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that here in the pull request.

Note to project maintainer: This is a terminal state, meaning the cla/google commit status will not change from this state. It's up to you to confirm consent of the commit author(s) and merge this pull request when appropriate.

1 similar comment
@googlebot
Copy link

So there's good news and bad news.

👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there.

😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that here in the pull request.

Note to project maintainer: This is a terminal state, meaning the cla/google commit status will not change from this state. It's up to you to confirm consent of the commit author(s) and merge this pull request when appropriate.

@Narretz Narretz changed the title Feat ng ref Feat: ngRef directive Mar 26, 2018
@Narretz Narretz changed the title Feat: ngRef directive Feat: ngRef directive to publish component controllers into current scope Mar 26, 2018
@Narretz Narretz changed the title Feat: ngRef directive to publish component controllers into current scope Feat: ngRef directive to publish controllers, or elements into current scope Mar 26, 2018
// only remove it if value has not changed,
// carefully because animations (and other procedures) may duplicate elements
if (getter(scope) === refValue) {
setter(scope, null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra space

var setter = getter.assign;

return function(scope, element, attrs) {
var refValue = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the null value is necessary


// get the expression for value binding
var getter = $parse(tAttrs.ngRef);
var setter = getter.assign;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we throw a nonassign if the expression can not be assigned to?

* to the given property in the current scope. If no controller exists, the jqlite-wrapped DOM
* element will be added to the scope.
*
* If the element with ngRef is destroyed `null` is assigned to the property.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ngRef --> `ngRef`

* </div>
* </file>
* <file name="index.js">
* angular.module('myApp', []);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this might be better for a real app, having multiple, tiny files in examples is harder to follow. I would put everything in one JS file (and avoid unnecessary redirection, e.g.:

angular.
  module('myApp', []).
  component('myToggle', {
    controller: function MyToggleController(...) { ... }
  });

* var toggle = element(by.buttonText('Toggle'));
* expect(toggle.evaluate('myToggle.isOpen()')).toEqual(false);
* toggle.click();
* expect(toggle.evaluate('myToggle.isOpen()')).toEqual(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, I didn't know about the evaluate method. Nice 😃

*
* @example
* ### ngRef inside scopes
* This example shows how new scopes limits
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Limits?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this sentence at all :-)

* <div>outerToggle.isOpen(): {{outerToggle.isOpen() | json}}</div>
* </li>
* </ul>
* <div>ngRepeat.isOpen(): {{ngRepeatToggle.isOpen() | json}}</div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ngRepeat --> ngRepeatToggle

* <div>ngIfToggle.isOpen(): {{ngIfToggle.isOpen() | json}}</div>
* <div>outerToggle.isOpen(): {{outerToggle.isOpen() | json}}</div>
* </div>
* <div>ngIf.isOpen(): {{ngIf.isOpen() | json}}</div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ngIf --> ngIfToggle

* </example>
*
*/
var ngRefDirective = ['$parse',function($parse) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space after ,.

* });
* });
* </file>
* </example>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the examples, it looks like we suggest putting these stuff on the scope (I hope we don't).
It would be better if showcased how to assign the values into controllers, instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I didn't write the examples, but can fix them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that in the simple case where you just want to use the value inside the HTML only it makes sense to assign the ref to the scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed the 2nd example to use $ctrl, but not the first. I think we can keep it like this - the whole docs are riddled with "old style" examples

* @param {string} ngRef property name - A valid AngularJS expression identifier to which the
* controller or jqlite-wrapped DOM element will be bound.
* @param {string=} ngRefElement void - If specified, ngRef will always bind the jqlite-wrapped
* DOM-element even if a controller is available.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does the void - bit mean in the description here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is supposed to signify that the attribute takes no value. :)

return function(scope, element, attrs) {
var refValue = null;

if ('ngRefElement' in attrs) {
Copy link
Contributor

@dcherman dcherman Apr 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, would this preclude the use of both ngRef and ngRefElement used in conjunction to get both the controller and element?

Never mind, it's late. Thought this was a separate directive, then realized it was an option.

@Narretz
Copy link
Contributor Author

Narretz commented Apr 6, 2018

The API design has changed after discussing it with @petebacondarwin - there is now a ngRefRead attribute that can be used to assign a specific directive's controller or the element.

The default behavior is still that the component /element directive's controller, or if there's no component / directive, the element is assigned.

@Narretz Narretz modified the milestones: 1.6.x, 1.7.x Apr 12, 2018
@petebacondarwin petebacondarwin self-assigned this May 14, 2018
@petebacondarwin
Copy link
Member

@Narretz is this now ready for a full review?

@Narretz
Copy link
Contributor Author

Narretz commented May 22, 2018

@petebacondarwin not quite. I'll try to finish it tonight.

@Narretz Narretz force-pushed the feat-ngRef branch 3 times, most recently from 1e610f1 to 935bb71 Compare May 24, 2018 09:52
Copy link
Member

@petebacondarwin petebacondarwin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few minor comments


```

To resolve this error, use a path expression that are assignable:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is assignable

expect($rootScope.mySpan[0].textContent).toBe('my text');
}));

it('should nullify the dom element value if it is destroyed', inject(function($compile, $rootScope) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this test description accurate? It implies, to me at least, that the value of the DOM element is being set to null.
I think we are actually setting the value of the expression to null, right?


expect($rootScope.myDirective).toBe(myDirectiveController);
});
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth having a test that shows the element directive / component is chosen over any additional attribute directives if no ngRefRead attribute is available?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will add

});

inject(function($compile, $rootScope) {
var template = '<my-component ng-ref="myComponent">{{myComponent.text}}</my-component>';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woah! This makes you think :-)

* @restrict A
*
* @description
* The `ngRef` attribute tells AngularJS to assign the controller of a component (or an element-directive)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also assign attribute directives if we use ngRefRead so this sentence is not entirely accurate.

* });
* });
* </file>
* </example>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that in the simple case where you just want to use the value inside the HTML only it makes sense to assign the ref to the scope.

@Narretz
Copy link
Contributor Author

Narretz commented May 26, 2018

I've updated the PR with your suggestions @petebacondarwin

@@ -313,6 +313,7 @@ beforeEach(function() {

function generateCompare(isNot) {
return function(actual, namespace, code, content) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... 🤔


var ngRefDirective = ['$parse', function($parse) {
return {
priority: -1,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the -1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed for transclusion compatibility

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it because you are doing work in the compile function?
(Does this work have to be done there in the first place?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't look like it's because of the compile fn. Without the priority of -1, the link function receives the transclusion comment instead of the actual element if the element has "element transclusion" on it. I haven't looked into why it happens like this, though.

var refValue;
var controller;

if ('ngRefRead' in attrs) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use .hasOwnProperty() instead (to avoid traversing the prototype chain)?

if (attrs.ngRefRead === '$element') {
refValue = element;
} else {
controller = element.data('$' + attrs.ngRefRead + 'Controller');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have two different variables (refValue, controller) and not assign to refValue directly?

// when the element is removed, remove it (nullify it)
element.on('$destroy', function() {
// only remove it if value has not changed,
// carefully because animations (and other procedures) may duplicate elements
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you remove it carefully? 😁

@googlebot
Copy link

CLAs look good, thanks!

1 similar comment
@googlebot
Copy link

CLAs look good, thanks!

@googlebot googlebot added cla: yes and removed cla: no labels Jun 4, 2018
@petebacondarwin
Copy link
Member

The Travis failures look pretty legitimate to me.

@Narretz Narretz merged commit aa6adc7 into angular:master Jun 5, 2018
Narretz added a commit that referenced this pull request Jun 6, 2018
@alirezamirian
Copy link
Contributor

👍
Once, I had written an article about such feature :)
https://medium.com/@alireza.mirian/mimicking-angular-template-references-in-angularjs-with-a-10-line-directive-c0687a3ddaa6

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants