MacBook, defective by design banner

title:
Put the knife down and take a green herb, dude.


descrip:

One feller's views on the state of everyday computer science & its application (and now, OTHER STUFF) who isn't rich enough to shell out for www.myfreakinfirst-andlast-name.com

Using 89% of the same design the blog had in 2001.

FOR ENTERTAINMENT PURPOSES ONLY!!!
Back-up your data and, when you bike, always wear white.

As an Amazon Associate, I earn from qualifying purchases. Affiliate links in green.

x

MarkUpDown is the best Markdown editor for professionals on Windows 10.

It includes two-pane live preview, in-app uploads to imgur for image hosting, and MultiMarkdown table support.

Features you won't find anywhere else include...

You've wasted more than $15 of your time looking for a great Markdown editor.

Stop looking. MarkUpDown is the app you're looking for.

Learn more or head over to the 'Store now!

Thursday, October 27, 2022

A note to self I might have already noted... myself.

angular.module('MyApp')['_invokeQueue'].forEach(function(value){ 
    console.log(value[1] + ": " + value[2][0]);
});

Sauce.

So look, I'm at a company where much of the app is still in AngularJS. The downside is that this reminds me of a post I'd shared before that said:

If your list of things to develop is really a list of things that you wonโ€™t have to do in a more functional environment, none of which will make you more employable elsewhereโ€ฆ itโ€™s time to walk away.

That's not wrong. More experience in AngularJS isn't making me more employable at this point unless I want to work in the small subset of positions that plan to continue using it well past its dead-on date. Which, again, was January of this year.

That admission aside, as I've mentioned before, there is, however, a practical advantage: Most of the questions I have when I'm developing have nearly canonical answers. I mean, have you seen StackOverflow's blog on the Next.JS conference? The blog's title is "Goodbye Webpack, hello Turbopack!". With just a little bit of exaggeration, webpack, we barely knew ye.

I was looking to get us to a React stack, and I'd initially liked this "minimal" build tool suite over at 2ality. Okay, okay, it's been over three years, but it's now kaput. Gone. Snowpack is dead. I'd heard a bit about ViteJS already a bit from my RSS sub to Shawn Wildermuth's blog, and sure enough that's what Snowpack says to use now. If I'd learned Snowpack inside and out (not that it was tough to learn, and I've been playing with it on and off for a few years), that'd be nearly wasted time now.

Look, it's fine to need to keep up a little, it's not like .NET 6 doesn't have a decent amount of new stuff (does it though?), and maybe it's just that I've been around long enough to see things change several times, but it doesn't really have to, does it? There are database admins who haven't needed to learn anything exceptional for their whole career. I mean, look, it's not like our buddy Mr. Dave hasn't kept things moving, but it ain't client-side JavaScript. He's had a chance to get good at what he does, to really drill-down and understand rdbms engines.

Okay, yes. I'm a little jealous. ๐Ÿ˜

And if I haven't mentioned it before, I'm pretty sure it's because of conferences, YouTube, and Twitter. Everybody who wants to be a bleeding edge type can be, and they all race each other to be the new expert of something nobody else can be yet because it was just released. God made humans, but constantly evolving tech stacks made humans equal[ly clueless].

That said, I do need to write my follow-up to my intro to why we're using transpilation-free Preact. Preact really is a wonderful bridge from a hopelessly out of date client-side app to, well, at least a TypeScript-powered React refactor. Need to develop something new now but can't, for whatever reason, create all new tooling?

If you can limit support to IE 9+ or so, Preact (with hooks) is your stack. We've used Preact with Enzyme to create a tested, cradle-to-grave page with a reasonably complex UI and have it running well right smack in the middle of an AngularJS app. Set down new work in Preact now, start eating from the top with React, and when they meet in the middle, it's a very quick port.

So maybe I won't need to remember that cheat to get all directives, constants, etc out of an AngularJS site for too long. But when I forget again, at least I'll know I've got it sitting here.

Labels: , , , , ,


posted by ruffin at 10/27/2022 04:44:00 PM
Friday, May 20, 2022

Step 1: How I chose React

I'm working for a company that has a lot of legacy code, where "legacy" means "nothing on the client newer than AngularJS". Recently, I essentially volunteered to figure out how to stop building more technical debt by proposing a "modern" JavaScript templating engine (and associated tools) to use going forward for new feature work.

But since there's very little pure greenfield work for us in the near future -- and plenty of new features on the backlog -- my solution needed to be able to operate in a lot of different environments without causing ramp-up headaches for each new team of developers. Finding a solution to that is the real challenge I'm going to describe here.

I should note that, even in a working museum of obsolete code, the idea isn't and shouldn't be to toss the legacy code and start over, no matter how tempting that might feel. As Spolsky said years ago...

The sheer volume of bugs [in Netscape 6 on release], it seems, proves that rewriting code from scratch does not make for a better code base, it makes it worse. Old code doesnโ€™t rust, it gets better, as bugs are fixed.

Lou Montulli ['one of the 5 programming superstars who did the original version of Navigator' -Spolsky] again: โ€œI laughed heartily as I got questions from one of my former employees about FTP code the he was rewriting. It had taken 3 years of tuning to get code that could read the 60 different types of FTP servers, those 5000 lines of code may have looked ugly, but at least they worked.โ€ [emphasis mine -mfn]

In brief, you want to keep as much as your legacy code running as is realistically possible, refactoring it as your resources and priorities allow.

I've split my response into two posts.

  1. The sort of research and thinking that went into picking a solution.
  2. Code examples where I'm exhibiting the solution I've selected Is Not Wrong ยฉ 1842.

What follows is part 1...

Read more ยป

Labels: , , , , ,


posted by ruffin at 5/20/2022 03:20:00 PM
Thursday, May 20, 2021

I finally found some videos that talk directly to making an AngularJS site [sic] play nicely with TypeScript (Pluralsight pay-to-play. You can get a free trial, but make sure you select Premium level for your trial or you'll be out of luck). Looks like there was a nice little period where Angular2 had not offiially been released, but TypeScript was already reaonsably mature.
 
So I’m running through the tutorial, and hit the section on Providers. I admittedly hadn’t used them before, and very quickly learned why: Usually you can use a specialized version of a Provider to do what you need, like a Service or Factory. Providers are conventionally used for things that are reused in multiple apps and might need a domain specific tweak as it’s spun up in each specific app's context. 
 
You know, for instance...

Let's say that our unicornLauncher service is so awesome that many apps use it. By default the launcher shoots unicorns into space without any protective shielding. But on some planets the atmosphere is so thick that we must wrap every unicorn in tinfoil before sending it on its intergalactic trip, otherwise they would burn while passing through the atmosphere. It would then be great if we could configure the launcher to use the tinfoil shielding for each launch in apps that need it. We can make it configurable like so...
 

I mean, I'm all for a little humor in a tutorial, but can we get something closer to a real world example for the docs? I mean, I'm not a big fan of PhoneCat app either, since it's just a (now hilariously dated) commercial for Android phones, which is better (?) but so self-serving as to make me wonder. Angular.io's Heroes app is actually pretty close to the right speed.

Honestly, the AngularJS docs in general have left me a little disappointed. Guess in a perfect world, that wouldn't be a problem. Sometimes you gotta know the bugs to ship and which JavaScript frameworks to let die.

Labels: ,


posted by Jalindrine at 5/20/2021 01:54:00 PM
Wednesday, April 14, 2021

Any time I forget something more than once, I should probably blog about it. Saves me time looking for a comment on SO I put on an answer I particularly appreciated.

Because I've forgotten this particular something more than once, let's return to our time machine, travel back to AngularJS land again, and leave some notes.


What's an "ampersand binding" in AngularJS?

Essentially, an ampersand binding allows you to mark that you're passing an event handler function to a custom component.

Here's one mention in the AngularJS docs:

We saw earlier how to useย =attrย in theย scopeย option, but in the above example, we're usingย &attrย instead. Theย &ย binding allows a directive to trigger evaluation of an expression in the context of the original scope, at a specific time. Any legal expression is allowed, including an expression which contains a function call. Because of this,ย &ย bindings are ideal for binding callback functions to directive behaviors.

(There may be a better reference in the docs, but man it's hard to google an ampersand. They could have also ensured something that made it easier to find, like if it had a clear name similar to "ampersand component binding", but the AngularJS folk weren't nice enough to do that either afacit.)

Here's some example code [of mine :shudder:] that uses it:

angular.module('app').component('qbTristateCheckbox', {
    template:
        '<input ' +
        'ng-class="tsc.classes" ' +
        'ng-disabled="tsc.isDisabled()" ' +
        'ng-click="tsc.wasChecked($event)" ' +
        'ng-attr-id="{{tsc.checkboxId}}" ' +
        'type="checkbox" />',
        
    controller: QbTristateCheckboxController,
    controllerAs: 'tsc',
    
    bindings: {
        labelDisplay: '@',

        checkState: '<',
        checkboxClasses: '<',
        ngDisabled: '<',
        checkboxId: '<',

        clickHandler: '&', // <<< THIS ONE IS WHAT WE'RE TALKING ABOUT
    },
});

In this case, we've got a custom checkbox component that supports tristate. Specifically, that means it's a wrapper that enables easy access to checked, unchecked, and indeterminate states (three states, not the usual two. Am I over explaining this? ;^D) in an html form checkbox input.

Indeterminate status checkboxes are kinda complicated in vanilla HTML, and this is trying to abstract some of that in a package AngularJS can consume easily.

As a freakin' huge proponent of props down, events up, the tristate checkbox is a very black box.

  • You tell it the state it should be in by passing down the value carried by the prop checkState (here, for simplicity's sake, 0, 1, or 3 [sic]).
  • When it's clicked, it tells you that's happened by calling the event handler contained in clickHandler.

checkState down. clickHandler up.


Where the ampersand binding bites you

The problem is that the ampersand binding doesn't give a function for your component's controller that you can call in a conventional sense.

If clickHandler's signature on the tristate checkbox component's parent looks like this...

vm.checkHandler = function (permission, permissionType, newCheckState) {

... you'd never guess how you pass newCheckState from the child custom component to the function.

Here's an example (again, this is a real-world example. It's a little more use case specific than I'd like for a perfect demo):

<qb-tristate-checkbox
    ng-disabled="!qpc.canAssign(permission, 'Write')"
    checkbox-id="'write-' + index"
    check-state="permission.Write"

    label-display="''"

    click-handler="qpc.checkHandler(permission, 'Write', newCheckState)"
></qb-tristate-checkbox> 

permission is from an ng-repeat parent tag:

<li
    class="instructorPermissions text-center list-group-item row no-gutters justify-content-between"
    ng-if="qpc.questionIds && qpc.questionIds.length"
    
    ng-repeat="(index, permission) in qpc.processedPermissions | orderBy: ['-Owner','UserName']"
>
<!--                   ^^^^^^^^^^ that permission right there -->

'Write' is obviously just a static string.

But newCheckState comes from the component. It's the only value coming from the tristate checkbox that we care about.

How do I pass it?

Like this:

vm.clickHandler({
    newCheckState: value,
});

Inside of an object literal? Wait, WTFOMGBBQNSFW!!!!!


At first, I would've expected the event handler registration in the template to look like this:

<qb-tristate-checkbox
    click-handler="qpc.checkHandler" <<< Purely passing a reference
></qb-tristate-checkbox> 

... and the call from within the component to look like this...

// again, this would be within the QbTristateCheckboxController
vm.clickHandler(permission, 'Write', value)

But it's not.

I would have also accepted something in the component that lets you know you're calling a weirdly wrapped event handler prop, something like...

vm.callAmpersandBinding('clickHandler', permission, 'Write', value);

That's funky, but it has the benefit of letting you know something funky is going on. I mean, if clickHandler is a prop, and it has a function assigned to it, calling that reference should call the literal function, right?


Um, no. No. Not at all. Instead, we have to pass that value by embedding in an object literal. In fact, if we wanted to pass all three from the component, the code would look like...

<!-- template -->
<qb-tristate-checkbox
    click-handler="qpc.checkHandler(permission, staticString, newCheckState)"
></qb-tristate-checkbox> 

and

vm.clickHandler({
    newCheckState: value,
    permission: vm.processedPermisions[someMagicIndex], // <<< How do I find this?
    staticString: 'Write' // Outside of the template, neither makes sense
});

Instead of letting us call the handler directly, AngularJS provides some weird monkeypatching that makes clickHandler in our controller call a wrapped version of the event handler.


The advantages of the AngularJS ampersand binding convention

It's not all bad. What's neat about this is that it does allow us to combine jive from the template scope, like our permission from the ng-repeat, into an event handler call with variables sent from the component.

a visual representation of the relationship of the template, component parent, and component scopes to the event handler call

That is, by not limiting the call to the event handler from within the component to the literal event handler signature, we've got a lot more flexibility. We can combine two scopes, custom component controller and template contexts.

At the same time, though passing an object literal that maps to parameters named only in a prop definition in a template file might be more flexible, it's an extremely unintuitive, indiscoverable convention.

That we were establishing a new convention and why really didn't click for me until I understood how the new convention was useful -- precisely so that we could mix parent template and component controller scopes like this.


Put another way...

If that didn't click, here's a good explanation from superluminary on SO of the [unintuitive] way an ampersand binding in AngularJS works, and is what finally made how things worked click in my head. Once I had this convention straight and was able to combine it with the advantages of in-template parameter mixing, I was finally an ampersand binding expert. (Until I forgot.)

Anyhow, from that answer:

TL;DR;ย - You are assuming that the bound function is being passed to the child component. This is incorrect. In fact, AngularJS is parsing the string template and creating a new function, which then calls the parent function.

This function needs to receive an object with keys and values, rather than a plain variable. [emphasis mine -mfn]

...

Say I create a component like this:

const MyComponent = {
  bindings: {
    onSearch: '&'
  },
  controller: controller
};

This function (in the parent) looks like this:

onSearch(value) {
  // do search
}

In my parent template, I can now do this:

<my-component on-search="onSearch(value)"></my-component>
...

In myComponent controller, I need to do something like:

handleOnSearch(value) {
  if (this.onSearch) {
    this.onSearch({value: value})
  }
}

It's almost like AngularJS said, "I see your event handler, and I'm going to raise you this monkeypatch..."

componentController.clickHandler = function(paramsAsObjectLiteral) {
    parentScope.clickHandler({
        paramsAsObjectLiteral['newCheckState'] || templateContext.newCheckState,
        paramsAsObjectLiteral['staticString'] || templateContext.staticString,
        paramsAsObjectLiteral['permission'] || templateContext.permission,
    });
};

(Actually, that's functionally speaking exactly what AngularJS does, though there's another round of mapping after you compiled the template to code before you can have templateContext and I'm not sure which, the component or template's version of variables, really takes precedence (which goes on the left side of the ||s). The template would make more sense, I guess.)


What people usually end up doing... and why you shouldn't

The weird part is that you can skip this new convention entirely if you pass the event handler by reference. It's cheating, I'm pretty sure the AngularJS wishes you wouldn't, and you lose a lot of flexibility, but I can see why people do it. The ampersand binding convention simply isn't easily intuitive.

Here's one such example of that "my mind is blown; gimme simple" mentality from krawaller.se:

Becauseย &ย is so convoluted, many useย =ย to do output [...but...] A nicer solution than all of the above is toย useย <ย to create outputย byย passing in a callback!

We create the callback in the outer controller...

$scope.callback = function(amount){
$scope.count += amount;
}

...and pass it to the component:

<output out="callback"></output>

The component now simply calls it accordingly:

app.component("output",{
bindings: { out: '<' },
template: `
<button ng-click="$ctrl.out(1)">buy one</button>
<button ng-click="$ctrl.out(5)">buy many</button>`

});

Very similar toย &, but without the convoluted magic!

Then, in your controller, you'd simply call vm.out(myAmount) somewhere, where myAmount is a variable inside of your custom child component's scope.

But recall that you now can't do anything clever with your template's scope! As long as the template is something trivial like the <output out="callback"></output> we have here, fine, sure, you're golden. But as soon as you're one ng-repeat in, you're going to want to brush off that &.


Anyhow, I've known the ampersand binding convention long enough to use it and forget it once already, so there's a refresher. Hope that's helpful if you too have been sucked INTO AN ANACHRONISTIC ANGULARJS TIME BOB OMB!!!!1!!!

Labels: ,


posted by ruffin at 4/14/2021 01:42:00 PM
Monday, February 22, 2021

Last week I talked a little about using transclusion in AngularJS from a starting primer on the topic to where I still wasn't quite sure how to use it to pass DOM into a uibModal.

Here are a few more transclude-related links I rediscovered as I was closing out browser tabs this morning. With any luck, I'll come back and process this jive into something that answers the question I punted on for the time being last week.


From What exactly do you do with the transclude function and the clone linking function?:

As @Jonah pointed out below,ย here is a really good article on the compile option of directives and using the transclusion function

The basic idea is the compile function should return a linking function. You can use the transclusion function provided inside the linking function to take a clone of the transcluded DOM element, compile it, and insert it wherever it needs to be inserted.

Here is a better example I've [pull outta the air] on Plunker

The idea of the compile function is it gives you a chance to programmatically alter the DOM elements based on attributes passed BEFORE the linking function is created and called.

// a silly directive to repeat the items of a dictionary object.
app.directive('keyValueRepeat', function ($compile){
  return {
    transclude: true,
    scope: {
      data: '=',
      showDebug: '@'
    },
    compile: function(elem, attrs, transclude) {

      if(attrs.showDebug) {                
        elem.append('<div class="debug">DEBUG ENABLED {{showDebug}}</div>');
      }

      return function(scope, lElem, lAttrs) {
        var items = [];
        console.log(lElem);
        scope.$watch('data', function(data) {

          // remove old values from the tracking array
          // (see below)
          for(var i = items.length; i-- > 0;) {
            items[i].element.remove();
            items[i].scope.$destroy();
            items.splice(i,1);
          }

          //add new ones
          for(var key in data) {

            var val = data[key],
                childScope = scope.$new(),
                childElement = angular.element('<div></div>');

            // for each item in our repeater, we're going to create it's
            // own scope and set the key and value properties on it.
            childScope.key = key;
            childScope.value = val;

            // do the transclusion.
            transclude(childScope, function(clone, innerScope) {
              //clone is a copy of the transcluded DOM element content.
              console.log(clone);

              // Because we're still inside the compile function of the directive,
              // we can alter the contents of each output item
              // based on an attribute passed.
              if(attrs.showDebug) {                
                clone.prepend('<span class="debug">{{key}}: {{value}}</span>');
              }

              //append the transcluded element.
              childElement.append($compile(clone)(innerScope));
            });

            // add the objects made to a tracking array.
            // so we can remove them later when we need to update.
            items.push({
              element: childElement,
              scope: childScope
            });

            lElem.append(childElement);
          }
        });
      };
    }
  };
});

So that's at least a detailed example of using the transclude function in a compile.


You should probably also know the difference between transclude: true and transclude: 'element'.


This one I need to read again to understand what's going on precisely, but seems interesting.

How to make transcluded template to bind to component scope:

You can programmatically transclude using the $transclude service and append the elements to the form like this:

$transclude($scope, function(clone) {
  $element.find('form').append(clone);
})

Then you add the elements to your form controller like this:

$scope.testForm.$addControl($element);

And then to follow back up on Transclusion in Angular UI Modal not working, here's a bit of the answer given:

Taking the above points into account, theย openModalย method in the link function of your custom directiveย theModalย should look like the following:

scope.control.openModal = function () {
    scope.instance = $uibModal.open({
        animation: false,
        scope: scope,
        template: '<div>in the template</div><div class="content"></div>',
        appendTo: element  
    });

    
    /**
    * Give Angular some time to render your DOM
    */
    $timeout(function (){
        /**
        * Since you have jQuery loaded already
        */
        var content = element.find('.content');
        
        /**
        * Finally, append the transcluded element to the correct position, 
        * while also making sure that the cloned DOM is bound to the parent scope (i.e. ctl) 
        */
        transclude(scope.$parent, function(clonedContent){
            content.append(clonedContent);
        });
    });
};

Note how theย transcludeย function gives you control over how you want to bind some transcluded DOM toย a custom scopeย and NOT the default directive's scope. The plainย transclude()ย call will take the current available scope object into account - i.e. the directive's scope - for binding the transcluded DOM.


So, again, I punted and created a reusable modal that expects a specific payload in scope that gets mapped to the body. Here's just the body of the modal to demonstrate (no comments in the live version, natch):

<div id="multiMessageModal" class="modal-body">
    <span ng-repeat="message in messages">
    
        <!-- If you got text, put it here -->
        <p ng-if="message.text">{{message.text}}</p>
    
        <!-- If you got list items that may be related to text (but can be their own thing), put them here -->
        <ul ng-if="isArray(message.listItems) && message.listItems.length">
            <li ng-repeat="listitem in message.listItems">{{listitem}}</li>
        </ul>
        
        <!-- And then note that we're repeating as often as you want
            so you can have as many texts or lists in a row by leaving
            the other part falsy with each `message` -->    
    </span>
</div>

Makes sense, right? So if you need text, list items, then text, you have two messages, one with text and listItems and the next with just text. You could do three different unordered lists in a row with text before and after, or just three paragraphs of text with [{ text: 'p1' }, { text: 'p2' }, { text: 'p3' }], whatever.

Again, not nearly as good as being able to pass any template at all in a call to the modal, but it's doing the job for now. I still feel a little dirty from the hack, though.

Labels: ,


posted by ruffin at 2/22/2021 10:44:00 AM
Thursday, February 18, 2021

I think Iโ€™ve mentioned Iโ€™m stuck in an AngularJS (Angular 1) codebase for most of my work right now, which isnโ€™t great. The portion of the codebase where I do most of my work is pretty solid, but there are lots of places where it doesnโ€™t fully exploit what AngularJS had to offer.
 
I donโ€™t love learning all this stuff about AngularJS in 2021, but thatโ€™s the job. ๐Ÿ˜ One solution that I keep suggesting is transclusion, which is where you embed html over into a custom component just like you would any other html element's tags.
<my-custom-tag>
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
  </ul>
</my-custom-tag>
And each time Iโ€™ve promptly forgotten how to use it.
 
So as a note to self, hereโ€™s a [*coughcough*five-year old*coughcough*] video Iโ€™ve found that explains it fairly well:
 
 
Youโ€™re welcome for cuing the video up past the annoyingly loud โ€œWHAAAA!!!!โ€ at the beginning where heโ€™s trying to solidify his self-branding as The Net Ninja.

Well, it turns out there's a lot more to transclusion than I'd hoped.

Straight transclusion, where you want to drop html inside of your new custom tags just like they're standard html, and what I was describing before, is pretty much just as advertised.

But then there's the situation when you want to have more than one transclusion. Say your custom component has a header, footer, and body. Do you have to parse stuff or can you set up three separate transclusions in your markup?

Turns out it's the latter. (This is not covered in the video, above.)

So if I have a wacky component like this one:

<div>
    <div>Header: <span ng-transclude="header"></span></div>
    <div>
        <h2>Body</h2>
        <div ng-transclude="body"></div>
    </div>
    <div ng-transclude="footer"></div>
</div>

... and I map it up in my code like this...

.component('mySelect', {
    bind: {
        collection: '<',
    },
    transclude: {
        header: '?header', // note the `?` to keep each one optional
        body: '?body', // also note that you can change the names
        footer: '?footer', // between parent element and component template.
    },
    templateUrl: '/path/to/your/template.html',
});

... I can drop html into each by doing this...

<my-select>
    <header>head</header>
    <body>body</body>
    <footer>my foot</footer>
</my-select>

... and have them pop into the right places.

That's neat, I guess.


Transclusion and uibModal

Which brings us to my actual use case. What I wanted to do was create One Modal to Rule Them All based on the AngularJS version of bootstrap UI, the $uibModal. And I wanted a component where you could pass in via transclusion the contents for the modal.

So, you know, what I want is to take this conventional uibModal template:

<script type="text/ng-template" id="myModalContent.html">
    <div class="modal-header">
        <h3 class="modal-title" id="modal-title">I'm a modal!</h3>
    </div>
    <div class="modal-body" id="modal-body">
        <ul>
            <li ng-repeat="item in $ctrl.items">
                <a href="#" ng-click="$event.preventDefault(); $ctrl.selected.item = item">{{ item }}</a>
            </li>
        </ul>
        Selected: <b>{{ $ctrl.selected.item }}</b>
    </div>
    <div class="modal-footer">
        <button class="btn btn-primary" type="button" ng-click="$ctrl.ok()">OK</button>
        <button class="btn btn-warning" type="button" ng-click="$ctrl.cancel()">Cancel</button>
    </div>
</script>

... and replace it with something like this...

<script type="text/ng-template" id="myModalContent.html">
    <div class="modal-header" ng-transclude="header">
        <h3 class="modal-title" id="modal-title">I'm a modal!</h3>
    </div>
    <div class="modal-body" id="modal-body"  ng-transclude="body"></div>
    <div class="modal-footer" ng-transclude="footer">
        <button class="btn btn-primary" type="button" ng-click="$ctrl.ok()">OK</button>
        <button class="btn btn-warning" type="button" ng-click="$ctrl.cancel()">Cancel</button>
    </div>
</script>

One problem: Where do I put the html for each transclusion spot? That is, how do I insert the equivalent of this for my modal? I don't really want to put it into a new template each time. That defeats the purpose.

<my-select>
    <header>head</header>
    <body>body</body>
    <footer>my foot</footer>
</my-select>

You can't really insert into a template without the template. That is, uibModals aren't in their own component. You're already passing in the entire template, I believe (I'm not an AngularJS expert by any stretch, and I'm not really super psyched to be one where I don't have to, tbh).

There are at least two questions on StackOverflow that seem related.

The first one has some code with a method passed to link that I've seen mentioned elsewhere, but never explained well, including in the docs. And those are the only examples I've stumbled into so far, though I'm admittedly specifically looking for explanations of transclusion with uibModal, not link and transclude more generally.

app.directive("theModal", function($uibModal) {
  return {
    restrict: "AE",        
    scope: {              
      control: "="
    },
    transclude: true,
    link: function (scope, element, attrs, ctrl, transclude) {// <<<<<< This and...
      scope.control = scope.control || {}
      scope.control.openModal = function () {
        scope.instance = $uibModal.open({
          animation: false,
          scope: scope,
          template: '<div>in the template</div><div class="content"></div>'
        });
        element.find('.content').append(transclude()); // <<<<<< ... this look promising. (-mfn)
      };
    }
  }
});

So I'm still not real sure how to send over transcluded content, but that looks reasonably promising.

For now, I've wimped out and created a new modal template type that takes a payload with messages and arrays of list items that the specialized modal turns into a display.

For instance, here's the body that takes in messages payloads like [{ text: "Stuff before the list", listItems: [1,2,3] }, { text: "Jive after the list" }]...

<div class="modal-body">
    <span ng-repeat="message in messages">
        <p ng-if="message.text">{{message.text}}</p>
        <ul ng-if="isArray(message.listItems) && message.listItems.length">
            <li ng-repeat="listitem in message.listItems">{{listitem}}</li>
        </ul>
    </span>
</div>

Feels dumb though.

Labels: ,


posted by Jalindrine at 2/18/2021 09:42:00 AM
Tuesday, February 16, 2021

This is a note people still stuck in AngularJS-land should probably understand. I see lots of the former in the codebase I've most recently inherited.

From codelord.net:

You donโ€™t need defer to create a simple-valued promise

Letโ€™s take a look at this case:

var defer;
defer = $q.defer();
defer.resolve(['detail', 'simple']);
return defer.promise;

Itโ€™s easy to see the author just wanted to create a promise with some hard-coded value. Those 4 lines? They can just be rewritten as:

return $q.when(['detail', 'simple']);

Labels: ,


posted by ruffin at 2/16/2021 09:41:00 AM
Wednesday, January 15, 2020

Here's Today Adventure in Enjoyable AngularJS...

I ran into some code from a project that looked give or take like this...

namespace.directive('caNavigation', function () {
    "use strict";

    return {
        restrict: 'E',
        scope: {
            inLinks: "@",
            collapseNav: "="
        },
        // ...
       
        controller:
            ["$scope", /* more jive */],
            function ($scope /* more jive */)
        {
            $scope.$watch('inLinks', function (value) {
                // omgwtfbbq?
                // ...


Well, that's @ and = stuff is pretty cryptic, especially since I hadn't used a directive in AngularJS before (Angular 2+, sure, but still learning ye olde AngularJS -- going backwards, I know. Don't forget I had my first run-in with Perl in 2014 [sic]).

Let's see what ye olde docs say:

[Having to have a separate controller to change direct scopes] is clearly not a great solution.
What we want to be able to do is separate the scope inside a directive from the scope outside, and then map the outer scope to a directive's inner scope. We can do this by creating what we call an isolate scope. To do this, we can use a directive's scope option:
...

Let's take a closer look at the scope option:
//...
scope: {
customerInfo: '=info'
},
//...
The scope option is an object that contains a property for each isolate scope binding. In this case it has just one property:
  • Its name (customerInfo) corresponds to the directive's isolate scope property, customerInfo.
  • Its value (=info) tells $compile to bind to the info attribute.

Good heavens, AngularJS. Not the most discoverable. Seems like in AngularJS in general there's lots of magic stringing. Perhaps I shouldn't be surprised, but that doesn't mean I like it. (Here I mean largely inline array notation, which is too hipster by half. I mean, I get that you don't what to lose information when you minimize, but then how about pass real references, not refer to objects by their original names? Sheesh.* )

But let's go a little deeper so that we know what the @ symbol means too.
The 'isolate' scope object hash defines a set of local scope properties derived from attributes on the directive's element. These local properties are useful for aliasing values for templates. The keys in the object hash map to the name of the property on the isolate scope; the values define how the property is bound to the parent scope, via matching attributes on the directive's element:
  • @ or @attr - bind a local scope property to the value of DOM attribute. The result is always a string since DOM attributes are strings. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given <my-component my-attr="hello {{name}}"> and the isolate scope definition scope: { localName:'@myAttr' }, the directive's scope property localName will reflect the interpolated value of hello {{name}}. As the name attribute changes so will the localName property on the directive's scope. The name is read from the parent scope (not the directive's scope).
  • = or =attr - set up a bidirectional binding between a local scope property and an expression passed via the attribute attr. The expression is evaluated in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given <my-component my-attr="parentModel"> and the isolate scope definition scope: { localModel: '=myAttr' }, the property localModel on the directive's scope will reflect the value of parentModel on the parent scope. Changes to parentModel will be reflected in localModel and vice versa. If the binding expression is non-assignable, or if the attribute isn't optional and doesn't exist, an exception ($compile:nonassign) will be thrown upon discovering changes to the local value, since it will be impossible to sync them back to the parent scope.
    By default, the $watch method is used for tracking changes, and the equality check is based on object identity. However, if an object literal or an array literal is passed as the binding expression, the equality check is done by value (using the angular.equals function). It's also possible to watch the evaluated value shallowly with $watchCollection: use =* or =*attr
If you've used Angular 2+ (what a failed naming scheme, btw. Why not "Angular.IO" after the website or just NGular or something so that googling this jive would be easier?), you can already see our steady progression to banana boxes.

But for now, what a mess.
  1. If a scope property is a string that starts with @, then we'll initialize that scope property with the directive's DOM attribute that matches what comes after the @.
    • That is, this is a one-way process. Props down, and set up events if changes need to go back up.
  2. If the prop starts with an =, we're doing the equivalent of Angular.IO's banana boxing, it appears.
    • So if we change whatever's in here, the parent is changing too.
Back to our original example now... We had a cryptic (to me) $scope.$watch('inLinks', function (value)..., and now we know what that means. @ means we've got a prop that $watch can check for changes, which, in this case, means:

  • The listener is called only when the value from the current watchExpression and the previous call to watchExpression are not equal (with the exception of the initial run, see below). Inequality is determined according to reference inequality, strict comparison via the !== Javascript operator, unless objectEquality == true (see next point)
  • When objectEquality == true, inequality of the watchExpression is determined according to the angular.equals function. To save the value of the object for later comparison, the angular.copy function is used. This therefore means that watching complex objects will have adverse memory and performance implications.

Guess that works. Let's ignore the =* shallow comparison jive for now. All we've really got is one prop called inLinks that will be passed... um... "in" that we'll update whenever it changes, probably after the page finishes loading and making some resource calls. And sure enough...

<li ng-repeat="subLink in link.SubLinks|filter: {ShowInNav: true}" ...

That means once we've loaded the correct navigational links for this context, we'll start pushing them into the DOM.

And our code makes sense. Much rejoicing.



* I actually kinda like AngularJS. It's one clean step away from the inefficient, overly-engineered land of enterprise development Bjarnason discusses and that at least the social convention surrounding Angular 2+ seems to require. But there are a few stupid* conventions like this one that really do AngularJS in conceptually.

* Sorry, I've tried to come up with a good synonym, but "stupid" fits here. My earlier use of "hipster" was my kind attept at an alternative, but this stuff really feels a little too much like a first-pass solution to be in a nice, mature templating library.

Labels: , ,


posted by ruffin at 1/15/2020 05:32:00 AM

<< Older | Newer >>


Support freedom
All posts can be accessed here:


Just the last year o' posts:

URLs I want to remember:
* Atari 2600 programming on your Mac
* joel on software (tip pt)
* Professional links: resume, github, paltry StackOverflow * Regular Expression Introduction (copy)
* The hex editor whose name I forget
* JSONLint to pretty-ify JSON
* Using CommonDialog in VB 6 * Free zip utils
* git repo mapped drive setup * Regex Tester
* Read the bits about the zone * Find column in sql server db by name
* Giant ASCII Textifier in Stick Figures (in Ivrit) * Quick intro to Javascript
* Don't [over-]sweat "micro-optimization" * Parsing str's in VB6
* .ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture); (src) * Break on a Lenovo T430: Fn+Alt+B
email if ya gotta, RSS if ya wanna RSS, (?_?), ยข, & ? if you're keypadless


Powered by Blogger etree.org Curmudgeon Gamer badge
The postings on this site are [usually] my own and do not necessarily reflect the views of any employer, past or present, or other entity.