familysearch reference client
DESCRIPTION
Talk given at the FamilySearch Developers Conference in September 2014TRANSCRIPT
THE FAMILYSEARCHREFERENCE CLIENT
Open-source implementation of the Family Tree UI
Written as a Single-Page Application in Javascript
using the REST API
DALLAN QUASSLYNN MONSON
DOVY PAUKSTYS
TOUR
SELECT PERSON
PERSON
PERSON (SCROLL DOWN)
EDIT NAME
CHINESE
ADD EVENT
ADD SOURCE
SOURCE BOX
WHY WAS ITDEVELOPED?
1. Make it easy for partners to allow their customers to accessthe FamilySearch tree using an easily-extensible framework
2. Provide a set of re-usable components for use by partners3. A real-world example of accessing the FamilySearch Tree
using the FamilySearch Javascript SDK
DISCLAIMERSNot official - not an official FamilySearch projectNot supported - code is provided as-isNot maintained - everything currently works...
WHAT CAN I DO WITH IT?1. Have you ever thought you could improve upon the
FamilySearch UI?Fork this project and extend it
2. Do you want to allow people to edit names and are intimidatedby the complexity?
Use the name edit component from this project3. Would you like to understand better how to use the
FamilySearch REST API?Review the source for this project
HOW CAN I GETSTARTED?
Install pre-requisites
Install node.js (nodejs.org)npm install -g bower install bowernpm install -g grunt-cli install gruntInstall PhantomJS (phantomjs.org)
HOW CAN I GETSTARTED?
Get an App Key
Contact FamilySearch developer support if you don't alreadyhave one
Ask FamilySearch developer support to add as an OAuth redirect URLhttp://localhost:9000/#!/auth
HOW CAN I GETSTARTED?
Run the code
clone github.com/rootsdev/familysearch-reference-clientnpm install install build dependenciesbower install install client dependenciesgrunt watch launch a server and watch for changesVisit Sign in using your sandbox username and password
http://localhost:9000
ANGULARJS
ANGULARJS FRAMEWORKModelsViewsControllers Directives (components)FiltersServices dependency injectioneasy unit tests
index.htmlhead> <!-- include stylesheets, fonts, and javascripts --></head>body> <div class="navbar navbar-default navbar-static-top"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="/">FamilySearch Reference Client</a> <span ng-show="busy" style="float: left; line-height: 50px"><i class="fa fa-spinner fa-spin" </div> <div class="collapse navbar-collapse header-collapse"> </div> </div> </div> <div id="themeContainer" class="familysearch_theme"> <div fs-growl=""></div> <div fs-re-authenticate=""></div> <div class="container" ui-view=""></div> </div></body>
app.jsangular.module('fsReferenceClient', [ 'fsReferenceClientShared', 'templates-app', 'templates-common' .config(function($stateProvider, $urlRouterProvider) { $urlRouterProvider.otherwise('/'); }) .config(function(fsApiProvider) { fsApiProvider .setClientId('WCQY-7J1Q-GKVV-7DNM-SQ5M-9Q5H-JX3H-CMJK') .setEnvironmentName('sandbox') .setRedirectUri('http://localhost:9000/#!/auth'); }) .config(function(fsLocationProvider) { var prefix = '/#'; fsLocationProvider.configure({ getPersonLocation: function(personId) { return { prefix: prefix, path: '/person/'+personId }; }, ... }); })
app.js (continued).controller('AppController', function ($scope) { $scope.environment = 'Sandbox'; $scope.$on('$stateChangeStart', function(event, toState) { if (toState.resolve) { $scope.busy = true; } }); $scope.$on('$stateChangeSuccess', function() { $scope.busy = false; }); $scope.$on('$stateChangeError', function() { $scope.busy = false; }); });
person.jsangular.module('fsReferenceClient') .config(function ($stateProvider) { $stateProvider.state('person', { url: '/person/:personId', controller: 'PersonController', templateUrl: 'person/person.tpl.html', data: { pageTitle: 'Person' }, resolve: { person: ['$stateParams','fsApi',function($stateParams, fsApi) { return fsApi.getPerson($stateParams.personId).then(function (response) { return response.getPerson(); }); }], sources: ['_','$q','$stateParams','fsApi',function(_, $q, $stateParams, fsApi) return fsApi.getPersonSourcesQuery($stateParams.personId).then(function(response) return _.map(response.getSourceRefs(), function(sourceRef) { return { ref: sourceRef, description: response.getSourceDescription(sourceRef.$sourceDescriptionId id: sourceRef.id }; }); }); }] } });
person.js (continued).controller('PersonController', function ($scope, $state, $rootScope, person, sources, fsApi, fsUtils, fsCurrentUserCache) var sections = ['vitalFacts', 'otherInfo', 'familyMembers', 'sources', 'discussions' $scope.states = {}; sections.forEach(function(section) { $scope.states[section] = {value: 'open'}; }); $scope.person = person; $scope.sources = sources; sources.forEach(function(source) { fsUtils.mixinStateFunctions($scope, source); });
var unbindRestored = $rootScope.$on('restored', function() { fsApi.getPerson($scope.person.id).then(function (response) { fsUtils.refresh($scope.person, response.getPerson()); }); }); $scope.$on('$destroy', unbindRestored);
$scope.$on('delete', function(event, person, changeMessage) { event.stopPropagation(); person._busy = true; person.$delete(changeMessage).then(function() { person._busy = false; fsCurrentUserCache.getUser().then(function(user) { $state.go('person', { personId: user.personId });
person.tpl.htmldiv fs-person-profile="" person="person"></div>div class="mainContent"> <div class="row"> <div class="col-md-9"> <div fs-vital-facts-section="" person="person" sources="sources" state="states <div fs-other-info-section="" person="person" state="states['otherInfo']"></div <div fs-family-members-section="" person="person" state="states['familyMembers <div fs-sources-section="" person="person" sources="sources" state="states['sources <div fs-discussions-section="" person="person" state="states['discussions']"></ <div fs-notes-section="" person="person" state="states['notes']"></div> </div> <div class="col-md-3 sidebar"> <div fs-changes-section="" person="person"></div> <div fs-tools-section="" person="person"></div> </div> </div></div>
fsPersonProfile.jsangular.module('fsReferenceClientShared') .directive('fsPersonProfile', function (fsLocation) { return { templateUrl: 'fsReferenceClientShared/fsPersonProfile/fsPersonProfile.tpl.html' scope: { person: '=' }, link: function(scope) { scope.treeHref = fsLocation.getTreeUrl(scope.person.id); } }; });
fsPersonProfile.tpl.htmldiv class="profileHeaderContainer"> <div class="tree-family jumbotron"> <div class="personWrapper"> <div class="nameWrapper"> <div class="row"> <div class="col-lg-2 col-md-2"> <i class="icon-rounded hidden-xs" ng-class="{'fs-icon-male': person._isMale(), 'fs-icon-female': !person._isMale()}"> </i> </div> <div class="col-lg-10 col-md-10"> <div class="pull-right"><span ng-show="person._busy"> <i class="fa fa-spinner fa-spin"></i></span></div> <h1>{{person.$getDisplayName()}}</h1> <span class="lifeSpan">{{person.$getDisplayBirthDate()}} - {{person.$getDisplayDeathDate()}}</span> <span class="pid">{{person.id}}</span> <div class="personActions"> <ul class="list-collapsed"> <li class="pull-left"><a ng-href="{{treeHref}}"> <i class="fs-icon-male fs-icon-pedigree"> </i>View Tree</a></li> </ul> </div> </div> </div> </div> </div>
EXTENDING
RESULT
RE-USE COMPONENTS
FORKFork
create a directory for your components; e.g.,src/common/dqComponents
https://github.com/rootsdev/familysearch-reference-client
dqPedigreeMini.jsangular.module('dqPedigreeMini', ['loDash', 'fsReferenceClientShared', 'dqPersonMini' .directive('dqPedigreeMini', function (_, fsApi) { return { templateUrl: 'dqComponents/dqPedigreeMini.tpl.html', scope: { personId: '@' }, controller: function($scope) { fsApi.getAncestry($scope.personId, {generations: 2}).then(function(response) $scope.persons = _.filter(response.getPersons(), function(person) { return person.$getAscendancyNumber() >= 1 && person.$getAscendancyNumber() <= 7; }); }); } }; });
dqPedigreeMini.tpl.html<div class="panel panel-info"> <div class="panel-heading"> <h4 class="panel-title">Mini Pedigree</h4> </div> <div class="panel-body dq-pedigree-mini-outer"> <div ng-repeat="person in persons"> <div dq-person-mini person="person" is-focus="{{person.$getAscendancyNumber() == 1}}" class="dq-pedigree-mini-person" ng-class="'dq-pedigree-mini-pos'+person.$getAscendancyNumber()"> </div> </div> </div></div>
dqPersonMini.jsangular.module('dqPersonMini', []) .directive('dqPersonMini', function () { return { templateUrl: 'dqComponents/dqPersonMini.tpl.html', scope: { person: '=', isFocus: '@' } }; });
dqPersonMini.tpl.htmldiv class="dq-person-mini"> <a href="" fs-person-popover="" person="person" popover-placement="left"> <span class="dq-person-mini-name" ng-style="{'font-weight': isFocus === 'true' ? 'bold' : 'normal', 'color': isFocus === 'true' ? '#333' : '#0051c4'}"> {{person.$getDisplayName()}} </span> </a></div>
INJECT INTO PERSON<div class="col-md-3 sidebar"> <div fs-changes-section="" person="person"></div> <div fs-tools-section="" person="person"></div> <div dq-pedigree-mini="" person-id="{{person.id}}"></div></div>
THE ENDSlides are at https://github.com/DallanQ/fs-reference-client-
2014-slides