Showing posts with label git. Show all posts
Showing posts with label git. Show all posts

Sunday, February 15, 2015

From the Notebook: An Ember Bootstrap Modal Dialog and How to Deal with state in Ember controllers

Bootstrap Modal Dialog for Ember

To create a Bootstrap styled modal dialog for an Ember app at work, I mostly followed the Ember cookbook entry on modal dialogs. One difference is using Bootstrap for the modal dialog, of course. Here are the steps I took.

  1. Added a modal outlet to the application template

    {{outlet}}
    {{outlet modal}}
    
  2. On my ApplicationRoute.js I have the following:

    actions: {
        openModal: function(modalName, model) {
    
            return this.render(modalName, {
    
                into: 'application',
                outlet: 'modal',
                model: model
            });
        },
    
        closeModal: function() {
    
            return this.disconnectOutlet({
    
                outlet: 'modal',
                parentView: 'application'
            });
        }
    }
    

    This allows me to pass a model to the modal. One implication of this is that the modal view needs to have a corresponding Controller so that Ember can set the model on that Controller. It appears that by default Ember Routes will supply an ArrayController or ObjectController for routes that don't have one defined, but the same doesn't hold for these non-route views.

  3. I then defined a modal-dialog component, but also split out modal-body and modal-footer components since those are the ones I'm expecting to customize from modal dialog to modal dialog.

    <script type="text/x-handlebars" data-template-name="components/modal-dialog">
        <div class="modal fade" tabindex="-1" role="dialog" aria-labelledby="dialogTitle" aria-hidden="true">
            <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal"
                        aria-label="Close"> <span
                        aria-hidden="true">&times;</span></button>
                    <h4 class="modal-title" id="dialogTitle">{{title}}</h4>
                </div>
                {{yield}}
            </div>
            </div>
        </div>
    </script>
    
    <script type="text/x-handlebars" data-template-name="components/modal-body">
        <div class="modal-body">
            {{yield}}
        </div>
    </script>
    
    <script type="text/x-handlebars" data-template-name="components/modal-footer">
        <div class="modal-footer">
            {{yield}}
        </div>
    </script>
    
  4. For the component code I need to call .modal() on the modal dialog to pop it up. I also wanted to handle the Bootstrap modal closed event and then remove the modal from the DOM (i.e., disconnect the outlet). Removing the modal from the DOM is not strictly necessary I guess, but I think it is nice to remove it from the DOM when it is no longer needed. If it is rendering a model, there's no point for the hidden modal to re-render when that model changes.

    App.ModalDialogComponent = Ember.Component.extend({
        sendCloseAction: function(){
            this.sendAction('close');
        },
        didInsertElement: function() {
            this.$('.modal').modal();
            this.$('.modal').on('hidden.bs.modal', this.sendCloseAction.bind(this));
        },
        willDestroyElement: function() {
            this.$('.modal').off('hidden.bs.modal');
        }
    });
    
  5. To use it, just need to create a view that uses the modal-dialog component.

    <script type="text/x-handlebars" data-template-name="myModal">
        {{#modal-dialog close="closeModal" title=title}}
            {{#modal-body}}
                {{message}}
            {{/modal-body}}
            {{#modal-footer}}
                <button type="button" class="btn btn-default" {{action 'save'}}>Save</button>
                <button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
            {{/modal-footer}}
        {{/modal-dialog}}
    </script>
    

    One problem I ran into was that Components can only send actions based on names that they are given. My ModalDialogComponent can't call this.sendAction('closeModal') and have that be handled by the ApplicationRoute. Instead ModalDialogComponent calls this.sendAction('close') where the actual named action to send is specified in the view using the component. Here in the myModal template I'm specifying that for close the component should send the closeModal action. See Sending Actions from Components to Your Application for more details.

Ember Controllers Are Singletons

One surprising thing about Ember for me as I've been learning over the past couple months is that Controllers are (kind of, sort of) singletons. Ember instantiates a controller for a route once. So any state in a Controller is sticky. But often times you'll want to reset the state in your Controller when switching from one model to another. So what to do?

Found a couple of interesting blogs regarding this problem:

Both suggest resetting the controller in Route's setupController as one way to address this problem. (Note: Ember.Route also has a resetController hook; haven't used it but seems to cover exactly this need.) Balint Erdi's post has an interesting idea about having the reset logic in the Controller itself and having it observe some property that can be used to trigger a reset.

When thinking about Controller state and Routes, and where to put this state, it occurs to me that there are 3 kinds of view state. A view might want to take advantage of all 3 types of view state.

  1. Transient view state

    This is view state that you don't want to be sticky at all. Maybe expanding/collapsing an accordion type view, or form validation error display.

    Basically, if the model changes, you want to reset this the transient state. For this you can define a resetState function that is called whenever the model changes.

    resetState: function(){
        // reset transient stuff here
    }.observes("model")
    
  2. Sticky state, but not serialized to the URL

    This would be view state that you want to be sticky so that when the user moves from view to view this state remains. However, for whatever reason, you don't want to serialize this state to a Route URL. This is the default way that Ember works.

    Can't actually think of a good example here. Maybe a sub-view that you expand or show and as you move through different models you want to keep showing that sub-view?

  3. Sticky state that should be serialized to the URL

    If you have some view state that you want to be persistent, then you should really think about moving that to the Route and serializing to the URL. If that works for your use case, then you can do that and move that view state out of the Controller entirely.

To me, #1 is a more common type of view state than #2, so it seems weird that the default for Ember Controllers is #2. However, the fact that Ember Controllers are singletons makes #2 possible and then one just needs to reset state to make #1 work. If Ember Controllers weren't singletons, its hard to see where #2 style view state would be stored.

Odds and Ends

  • Console2: a better Windows console. I played around with setting up Console2 to get a better console window than cmd.exe. The following were helpful resources

    Here's what I like about Console2:

    • You can configure it to copy on select and paste with a right click.
    • You can resize the window (but you have to configure it to have more columns first, which is kind of weird). I sometimes like to have the console take up a full screen.
    • It is tabbed.
    • You can configure it to start a cmd shell or Git bash. I have Git bash as a default and hitting Ctrl+Shift+T opens a new Git bash tab.
  • Ember dot notation. Learned that in Ember instead of doing

    this.get('foo').get('bar').get('baz');
    

    You can do

    this.get('foo.bar.baz');
    

    If bar is not defined, then the first one would fail but the second would return undefined.

  • Git: counting words in a specific revision of a file. I wanted to be able to count how many words are in a previous version of a blog post. There might be a better way to do this, but here's how I did it.

    1. Get the file's blob hash. You can do

      git log --raw -- path/to/file
      

      Seems the easiest way. This prints the before and after blob hash for each revision

      ...
      :100644 100644 c5d00fe... 2403611... M  path/to/file
      ...
      

      Where 2403611 is the blob hash for this revision and c5d00fe is the blob hash for the previous revision.

    2. cat the blob and count the words. Getting the blob hash was the hard part, now we can simply do

      git cat-file -p 2403611 | wc -w
      

Sunday, June 01, 2014

Git client setup

These are some settings and configuration I've found useful so far while working with Git.

Git global configuration

git config --global user.name "Marcus Christie"
git config --global user.email marc.christie@gmail.com

Can also set an editor to use for commit messages

git config --global core.editor gvim

Or with MacVim on OS X

git config --global core.editor 'mvim -f'

However, I think I prefer to edit commit messages in the terminal.

You can also use gvimdiff as a merge tool, but I haven't tried this yet.

Excludes file

Set up a global excludes file in ~/.config/git/ignore. My entries so far:

*.swp
.DS_Store
Session.vim

Whitespace

On Windows you want

git config --global core.autoclrf true

I think this is the default on Windows also, so this may be unnecessary.

Credential helper

This tip is mainly for when you are working with a repo over HTTPS, for example, with GitHub, and you authenticate with a username and password. You don't want to have to type passwords over and over. For this you can specify a credential helper that will securely store your password. On OS X:

git config --global credential.helper osxkeychain

On Windows, download git-credential-store. Then start up Git Bash, go to the directory where you downloaded the executable and then run it from the command line

$ git-credential-winstore.exe

Aliases

Shortcuts so you can run git ci -m "fixed bug" instead of typing out commit

git config --global alias.ci commit
git config --global alias.st status
git config --global alias.co checkout

Bash completions

On Windows, Git Bash comes with these already configured. On OS X, I downloaded git-completion.bash, dropped it in ~/bin and in .bash_profile I have

## Git completions
source ~/bin/git-completion.bash

Web viewer

On OS X:

git config --global instaweb.httpd webrick

Now you can start up a web viewer with

git instaweb

As a Mercurial refugee, I got used to hg serve so I like to have a web view.

Haven't tried doing this on Windows yet.

push.default

Set push.default to simple:

git config --global push.default simple

This setting affects what happens when you do a git push without specifing a remote or branch to push. With simple git will push the current branch to the upstream tracked branch. This mode also is the new default in Git 2.0.

Make sure that when you first push a branch to a remote server that you also set it as the upstream tracked branch. You can do that with the -u option:

git push -u origin mybranch

From then on every push can be done simply with

git push

merge.defaultToUpstream

Set merge.defaultToUpstream to true

git config --global merge.defaultToUpstream true

This setting affects what happens when you do a git merge without specifying a branch to merge from. With this set to true, git will merge in changes from the upstream remote tracking branch, which is generally what you want.

Anything else?

Are there any other things you do to configure Git on a new machine? Let me know in the comments or on Twitter.

Sunday, May 04, 2014

Git: How to delete a branch

How do you create a temporary branch to do some work in (e.g, a feature branch), and then when you have merged it to master and are done with it, how do you delete it, both locally and remotely?

Let's create a simple repo to experiment with. We'll add just one file to it.

mkdir myprj
cd myprj
git init
vim README.md
git add README.md
git commit -m "adding a README"

Okay, this will be our "central repo". Let's clone it, create a branch and do some work on it.

git clone myprj myprj2
cd myprj2
git branch install_notes
git checkout install_notes
vim README.md
git commit -a -m "Adding some installation notes"
git push origin install_notes
# Oops, forgot to set upstream
git push --set-upstream origin install_notes

The branch install_notes now exists locally and in the "central repo". Let's merge it and then delete it locally and also remotely. Before we get started though, here are all of our local and remote branches:

marcus$ git branch -avv
    * install_notes                6a5ed4c [origin/install_notes] Adding some installation notes
    master                       27c8a05 [origin/master] adding a README
    remotes/origin/HEAD          -> origin/master
    remotes/origin/install_notes 6a5ed4c Adding some installation notes
    remotes/origin/master        27c8a05 adding a README

First, let's switch back to master and try to delete install_notes:

marcus$ git checkout master
    Switched to branch 'master'
    Your branch is up-to-date with 'origin/master'.
marcus$ git branch -d install_notes
    warning: deleting branch 'install_notes' that has been merged to
            'refs/remotes/origin/install_notes', but not yet merged to HEAD.
    Deleted branch install_notes (was 6a5ed4c).

Hmm. It gave a warning, but proceeded to delete the branch anyways. Well, let's try to check it back out.

marcus$ git checkout -b install_notes origin/install_notes
    Branch install_notes set up to track remote branch install_notes from origin.
    Switched to a new branch 'install_notes'
marcus$ git checkout master
    Switched to branch 'master'
    Your branch is up-to-date with 'origin/master'.
marcus$ git merge install_notes
    Updating 27c8a05..6a5ed4c
    Fast-forward
    README.md | 4 ++++
    1 file changed, 4 insertions(+)

Okay, now we can delete the branch.

marcus$ git branch -d -r origin/install_notes
Deleted remote branch origin/install_notes (was 6a5ed4c).
marcus$ git branch -d install_notes
Deleted branch install_notes (was 6a5ed4c).

And now my branches look like this

marcus$ git branch -avv
    * master                6a5ed4c [origin/master: ahead 1] Adding some installation notes
    remotes/origin/HEAD   -> origin/master
    remotes/origin/master 27c8a05 adding a README